Merchants API
Provision merchants programmatically. Create, list, and get merchants under your partner.
Overview
The Merchants API lets your server provision merchants programmatically instead of routing every user through JoPay's interactive onboarding. It opens Stripe-Connect-style integrations — PSPs, marketplaces, Shopify-class plugins — where a merchant exists in your system before they've touched JoPay at all.
Creating a merchant via the API produces a shell merchantwith onboarding_status: "incomplete". The response includes an onboarding_url you redirect the user to; they finish wallet setup and TOS acceptance there.
onboarding_url, not in your server-side create call.Create a merchant
POST /api/v1/merchants
Required scope: merchants:write. Rate limit: 30 requests per minute per API key.
curl -X POST https://<partner-slug>.jopay.app/api/v1/merchants \
-H "Authorization: Bearer jo_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"email": "alice@acmecafe.example",
"display_name": "Alice",
"business_name": "Alice'\''s Cafe",
"country": "NL",
"account_type": "business",
"display_currency": "EUR"
}'Request body
Every field is optional. What your partner policy requires of merchants at onboarding is separate from what this endpoint accepts.
| Field | Type | Description |
|---|---|---|
email | string (≤ 254 chars) | Must match basic email format if supplied. |
display_name | string (≤ 120 chars) | Person/handle shown in the merchant dashboard header. |
business_name | string (≤ 120 chars) | Legal/business name shown on receipts and the pay page. |
country | string (ISO 3166-1 alpha-2) | Two uppercase letters, e.g. NL, US, BR. |
phone | string (≤ 32 chars) | Free-form phone contact. |
website | string (≤ 200 chars) | Business website URL. |
instagram | string (≤ 200 chars) | Instagram handle or URL. |
industry | string (≤ 80 chars) | Free-form industry label. |
display_currency | string (ISO 4217) | Merchant's display currency for fiat amounts. Falls back to the partner's configured currency if omitted. |
account_type | string | "consumer" (default) or "business". Gates which dashboard surface the merchant sees after onboarding. |
wallet_address | string (EVM) | 0x + 40 hex chars. Stored lowercased. Optional but enables idempotent create (see Idempotency). Most integrations leave this blank and let the user provision a wallet in onboarding. |
Response
{
"ok": true,
"merchant_id": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
"email": "alice@acmecafe.example",
"display_name": "Alice",
"business_name": "Alice's Cafe",
"country": "NL",
"phone": null,
"website": null,
"instagram": null,
"industry": null,
"account_type": "business",
"onboarding_status": "incomplete",
"wallet_address": null,
"display_currency": "EUR",
"home_portal": "<partner-slug>",
"disabled": false,
"created_at": "2026-04-17T12:00:00.000Z",
"test_mode": false,
"onboarding_url": "https://app.jopay.app/merchant/onboarding"
}Onboarding handoff
The returned onboarding_url is the deep-link your user needs to open in a browser to finish setup (wallet, TOS). Typical flow:
- Your server creates a shell merchant via this endpoint, gets a
merchant_idandonboarding_url. - You redirect the user's browser to
onboarding_url(or email them the link). - They complete wallet connection and TOS acceptance in JoPay. On success,
onboarding_statusflips to"complete"andonboarding_urlon subsequent reads isnull. - You can now create payment requests for that merchant via POST /api/v1/requests/create. Creating a request for a merchant still in
onboarding_status: "incomplete"returns400 merchant_onboarding_incomplete.
Idempotency
Create is idempotent on wallet_address. If you POST a merchant with a wallet_address that already belongs to a merchant on your partner and matches your key's mode, the existing merchant is returned with idempotent_replay: true instead of a duplicate being created.
If the wallet belongs to a merchant on a different partner, or to a merchant in a different mode (live vs test), you get 409 wallet_address_conflict — wallet addresses are globally unique across the platform.
wallet_address on create. The user provisions a wallet in the onboarding flow, and retries of your create call are rare enough that explicit idempotency isn't needed. Include it only if you know the wallet up-front (e.g. you already imported the user's wallet in your own system).Get a merchant
GET /api/v1/merchants/:id
Required scope: merchants:read. Rate limit: 60 requests per minute per API key.
curl https://<partner-slug>.jopay.app/api/v1/merchants/aaaaaaaa-... \
-H "Authorization: Bearer jo_live_xxx"Returns the same shape as Create response. Returns 404 not_found if the merchant doesn't exist, isn't assigned to your partner, or is in a different mode than your key (live vs test). JoPay never discloses cross-tenant or cross-mode existence — a 404 from another partner's id looks identical to a 404 from a typo.
List merchants
GET /api/v1/merchants
Required scope: merchants:read. Rate limit: 60 requests per minute per API key.
Query parameters
| Param | Type | Description |
|---|---|---|
onboarding_status | string | "complete" or "incomplete". |
disabled | bool | true or false. |
limit | int | Max 100. Default 50. |
offset | int | For pagination. Default 0. |
Response
{
"ok": true,
"merchants": [ /* array of merchant objects, same shape as get */ ],
"has_more": true,
"limit": 50,
"offset": 0
}Response field reference
Every Merchants API endpoint returns the same shape.
| Field | Type | Notes |
|---|---|---|
merchant_id | uuid | Use this when creating payment requests. |
email, display_name, business_name, country, phone, website, instagram, industry | string | null | Echo back what the merchant supplied (either via this API or during onboarding). |
account_type | string | "consumer" or "business". |
onboarding_status | string | "complete" or "incomplete". |
wallet_address | string | null | The merchant's receiving address. null until they set one in onboarding. |
display_currency | string | null | Merchant's preferred fiat display currency. |
home_portal | string | Your partner slug. Used for building branded URLs. |
disabled | bool | true if the merchant is disabled by partner or admin. Disabled merchants can't create new payment requests but their dashboard remains accessible. |
created_at | ISO-8601 | Timestamp of merchant creation. |
test_mode | bool | Frozen at create based on the key prefix used. A live key never sees test merchants and vice versa. |
onboarding_url | string | null | Deep-link to the onboarding flow. null when onboarding_status === "complete". |