Merchant onboarding

How a user becomes a merchant — the registration form, the two independent status fields, the KYC gate, the four keys issued up front, and the exact point at which an API key starts working.

5 min readUpdated 3 August 2026merchants, kyc, api-keys, verification

Any signed-in user can apply to become a merchant. One user gets one merchant account — the registration endpoint refuses a second. From your side this is a two-decision review: is this account allowed to trade at all (status), and have you checked who they say they are (verification). They are separate columns, they are set by separate admin actions, and they gate different things.

The two fields, and what each one blocks

status verificationStatus
Values PENDING · ACTIVE · SUSPENDED · REJECTED UNVERIFIED · PENDING · VERIFIED
Set at registration PENDING (usually) PENDING, always
Set by Merchants → Status Merchants → Verify
Blocks Every API call. Authentication rejects any key whose merchant is not ACTIVE with a 403. The checkout also refuses to confirm a payment for a non-ACTIVE merchant. Issuing new API keys, and editing the merchant's own identity fields.

Setting a merchant ACTIVE makes their keys work. It does not set verificationStatus. A merchant left at PENDING verification can take payments perfectly well but cannot create an additional API key — the endpoint answers "Merchant account must be verified to create API keys", which reads like a bug and is a policy. Do both, or expect the support ticket.

What registration does

The merchant fills the form at /gateway/register. Business name and email are required; website, description, business type, phone and a postal address are optional.

Registers the current user as a merchant and mints their first four API keys

Behind the form, in order:

  1. The gateway must be enabled. If gatewayEnabled is off, registration fails with "Payment gateway is currently disabled".
  2. KYC, if you require it. With gatewayRequireKyc on, the applicant must pass the shared KYC feature gate for use_gateway. A refusal is a 403 with the reason, not a generic error.
  3. Duplicate check. One merchant per user, by userId.
  4. A URL slug is generated from the business name and made unique. The form shows a slug field; it is ignored — the model's hook derives it. Same for the tax ID field, which has no column behind it.
  5. The phone is normalised to + plus digits only, truncated to 15 characters. Anything a user types that is not a digit is stripped.
  6. Defaults are copied from your platform settings. Fee percentage, fixed fee, payout schedule, payout threshold, daily and monthly limits, and the per-transaction limit are all snapshotted from the gateway settings at the moment of registration. Changing a platform setting later does not move an existing merchant — you edit that merchant's row.
  7. Wallet types and currencies are derived from gatewayAllowedWalletTypes: every enabled type, and the first three currencies of each. If your map is empty they fall back to FIAT / USD, which the platform check then rejects.
  8. Four keys are createdpk_live_, sk_live_, pk_test_, sk_test_ — all with the wildcard permission *.
  9. testMode is set to true on the merchant row.

The response is the only time the full key values and the webhook secret are ever shown. They are stored as SHA-256 hashes; the platform genuinely cannot show them again.

Four API keys and the webhook secret come back in one JSON body and are never retrievable. A merchant who closes that screen has to rotate every key and cannot recover the webhook secret at all — it is only re-shown on the merchant dashboard's settings view, and it is masked in demo mode.

Auto-approval needs two switches, not one

gatewayAutoApproveVerified on its own does nothing. A merchant is created ACTIVE only when both gatewayAutoApproveVerified and gatewayRequireKyc are on — the logic reads "auto-approve people we made pass KYC". With KYC off, the auto-approve switch is inert and every merchant lands in your queue.

Even under auto-approval, verificationStatus is still written as PENDING. So an auto-approved merchant can transact immediately with the four keys they were given, and still cannot mint a fifth until a human verifies them.

Editing the profile after registration

Updates the merchant's own profile

Fields split into two groups.

  • Always editable: description, logo, test mode, allowed currencies, allowed wallet types, default currency.
  • Frozen once verificationStatus is VERIFIED: name, email, phone, website, business type, address, city, state, country, postal code. A verified merchant who needs one of these changed has to come to you, and you change it from the admin merchant screen.

Note that a webhookUrl, successUrl or cancelUrl sent to this endpoint is accepted and then dropped — the merchant table has no such columns. Redirect and webhook destinations are per payment, set in the body of the create-payment call. See API integration.

API keys

Lists the merchant's keys, masked to their last four characters
Creates a public/secret pair
Updates a key's permissions, IP allowlist and URLs
Issues a new value for a key, invalidating the old one
Deletes a key

Rules worth knowing before a merchant asks:

  • Keys are created in pairs. One request produces both a pk_ and an sk_ key in the chosen mode, named <name> (Public) and <name> (Secret).
  • Ten keys per merchant, total — that is ten rows, so five pairs, and the four created at registration already count. The eleventh request is rejected.
  • Only secret keys do anything. Creating a payment, cancelling one and refunding all require an sk_. A public key can only call the validate endpoint and read.
  • Permissions are filtered, not validated. Anything outside payment.create, payment.read, payment.cancel, refund.create, refund.read and * is silently dropped; if that empties the list it becomes ["*"]. A typo therefore grants more access than intended, not less.
  • The IP allowlist only applies to secret keys, and only when it is non-empty. It accepts plain IPv4, CIDR ranges and the literal *. IPv6 entries are compared as strings — the CIDR maths is IPv4-only, so an IPv6 range will not match.
  • Per-key successUrl, cancelUrl and webhookUrl are stored and never read. They round-trip through the API and appear in the UI, but the create-payment call uses the URLs in its own body. Do not tell a merchant to configure a webhook on the key.

The onboarding sequence you should publish to merchants

  1. Register at /gateway/register and save the four keys and the webhook secret from the confirmation screen.

  2. Wait for approval. Until an admin sets the account ACTIVE, every API call returns 403 Merchant account is not active.

  3. Test with the sk_test_ key. Test-mode payments walk the whole flow — wallet selection, exchange rates, webhooks — and move no money. A test key can only see test payments; a live key can only see live ones. Looking up a live payment with a test key returns 404, deliberately.

  4. Point a webhook at your server by including webhookUrl on each create-payment call, and verify the signature. See Webhooks.

  5. Switch to the sk_live_ key and place one small real order before you open the till.

Next: API integration.