Skip to Content
View as Markdown

Errors

Every 4xx/5xx response carries the same JSON envelope:

{ "code": 400, "message": "invalid_input", "details": [ { "field": "items[0].unit_price", "message": "must be a positive decimal string" } ] }
  • code — the numeric HTTP status (400, 401, 404, …). Useful for generic HTTP-layer handling, but for branching logic switch on message instead — code won’t disambiguate between, say, invalid_input and payment_method_not_supported (both 400).
  • message — the lower-snake-case sentinel name, the sentinel name in lower snake case (e.g. INVALID_INPUT becomes "invalid_input"). Stable across releases — switch on this.
  • details — populated on validation errors. Array of { field, message } objects so the client can pin errors to inputs. Omitted otherwise.

Error bodies don’t include a trace ID or timestamp. To report a problem, send support the response Date header, the X-RateLimit-* headers, your merchant ID, the endpoint, and the approximate time of the request.

Authentication failures use a different shape. The envelope above is what the API returns for most errors. Requests rejected before they are processed (a missing or invalid X-Signature, an unknown X-Client-ID, or a stale X-Timestamp on a /b2b/v1/* call) come back as { "error": "...", "message": "..." }, where error is a coarse slug (unauthorized / bad_request / service_unavailable) and message carries the specifics. There is no numeric code and no details. Branch on the HTTP status first, then read message. Example (401):

{ "error": "unauthorized", "message": "invalid signature" }

HTTP status → typical codes

HTTPTypical code valuesWhat it means
400INVALID_INPUT, MISSING_REQUIRED, INVALID_FORMAT, INVALID_LENGTH, INVALID_VALUE, PAYMENT_METHOD_NOT_SUPPORTED, AMOUNT_BELOW_MINIMUMBad request — look at details
401INVALID_CREDENTIALS, INVALID_TOKEN, TOKEN_EXPIRED, INVALID_SIGNATURE (B2B); SESSION_EXPIRED (dashboard only)Auth failed — bad key, expired timestamp, wrong signature. Dashboard sign-in codes aren’t relevant to B2B integrations.
402INSUFFICIENT_CREDITMerchant prepaid balance ran out — top up before retrying
403FORBIDDEN, IP_BLOCKEDKey is valid but lacks the scope/IP allowance for this call
404NOT_FOUND, RECORD_NOT_FOUNDResource doesn’t exist (or doesn’t exist for this merchant)
409ALREADY_EXISTSIdempotency replay with a different body, or state machine refused the transition
429TOO_MANY_REQUESTS, TOO_MANY_ATTEMPTSRate limit hit; back off and retry
500INTERNAL_SERVER_ERROR, EXTERNAL_SERVICE_ERROROur fault; safe to retry with backoff.
503SERVICE_UNAVAILABLEA downstream dependency is down. Retry with backoff

Full code reference

The code values you may see:

Authentication (401)

  • INVALID_CREDENTIALS — API key combination rejected
  • INVALID_TOKEN — token unparseable or tampered
  • TOKEN_EXPIRED — token expired
  • SESSION_EXPIRED — dashboard session expired
  • INVALID_SIGNATURE — HMAC signature mismatch on B2B / webhook calls

Authorization (403)

  • FORBIDDEN — authenticated, but you aren’t allowed to perform this action
  • IP_BLOCKED — requests from this IP address are blocked

Not found (404)

  • NOT_FOUND — generic
  • RECORD_NOT_FOUND — row missing for the given ID

Conflict (409)

  • ALREADY_EXISTS — generic

Validation (400)

  • INVALID_INPUT — generic; check details
  • MISSING_REQUIRED — a required field was absent
  • INVALID_FORMAT — value didn’t match the expected format (e.g. UUID, URL, email)
  • INVALID_LENGTH — value too short or too long
  • INVALID_VALUE — value out of allowed enum/range
  • PAYMENT_METHOD_NOT_SUPPORTED — provider/asset combo isn’t enabled for the merchant
  • AMOUNT_BELOW_MINIMUM — order amount below the per-network or env-level floor. The response’s details.floor_usd carries the configured floor (USD) so you can surface it directly; the message text states it too.
  • INSUFFICIENT_BALANCE — the buyer’s wallet doesn’t hold enough of the pay asset to cover the transfer.
  • INSUFFICIENT_GAS — the buyer’s wallet lacks the native token to pay the network fee (gas) for the transfer.

Payment / billing (402)

  • INSUFFICIENT_CREDIT — merchant prepaid balance can’t cover the network fee (gas) and platform fee. Top up via the dashboard, then retry

Rate limiting (429)

  • TOO_MANY_REQUESTS — IP rate limit exceeded
  • TOO_MANY_ATTEMPTS — repeated failed attempts on the same resource (e.g. OTP) tripped a throttle

Server errors (500)

  • EXTERNAL_SERVICE_ERROR — a third-party provider failed
  • INTERNAL_SERVER_ERROR — unexpected error; contact support with the response Date header and X-RateLimit-* headers

Availability (503)

  • SERVICE_UNAVAILABLE — the service is temporarily unavailable; retry with backoff

Validation errors (400)

When the issue is a malformed request body, details is an array so you can map errors back to fields:

{ "code": 400, "message": "invalid_input", "details": [ { "field": "items[0].unit_price", "message": "must be a positive decimal string" }, { "field": "success_url", "message": "must be a valid https URL" } ] }

Auth errors (401)

INVALID_SIGNATURE has three common causes:

  • Wrong secret key (re-check env var)
  • Timestamp drift > 5 min (sync NTP)
  • Wrong canonical string (most often: forgot \n separators, or signed a parsed-then-re-stringified body that differs from what you sent)

See Authentication for the exact signing algorithm.

Rate limits (429)

SurfaceLimit
All API routes (incl. /b2b/v1/*)Per IP address — 500 req/min, shared bucket. Not per-merchant.
Public checkout (/checkout/*)Stricter sub-bucket — 20 req/min per IP

X-RateLimit-Limit and X-RateLimit-Remaining are included on every rate-limited response, not just successful ones. Treat them as the live budget for your IP.

Rate-limited responses include a Retry-After header (seconds until the window resets) — honor it. As a fallback, back off with jitter — 1s base + exponential to 30s.

Rate limits may change. If you reach them with legitimate traffic (for example, reconciling a large historical range), contact support.

Insufficient credit (402)

INSUFFICIENT_CREDIT (HTTP 402 Payment Required) means your prepaid balance can’t cover the network fee (gas) and platform fee for the operation you tried to perform, typically settling a crypto payment or an on-chain action whose network fee is sponsored. Top up from the merchant dashboard (Billing → Add credit), then retry the operation. Operations already in progress continue automatically once the balance is topped up.

Server errors (5xx)

A 500 means we couldn’t process the request. Retry with backoff — your idempotency_key ensures you won’t double-charge if the original request did partially succeed.

If retries don’t recover within a minute, surface a generic “payment temporarily unavailable” to the buyer and reach out to support with the failing endpoint, your merchant ID, the response Date header and X-RateLimit-* values, and the approximate request time — that’s enough for us to find the request.

Webhook delivery errors

Webhook deliveries are a separate failure channel — they don’t surface as API errors because your server isn’t the one calling. When a delivery returns a non-2xx (or times out), it’s retried with exponential backoff at 0s, 1min, 5min, 15min, 1h, 6h (six attempts total, the same schedule as Webhooks → Overview). The full per-delivery history shows up under Developers → Webhooks → [endpoint] → Delivery log in the dashboard. After the sixth attempt the delivery is marked “Failed” in the dashboard, and you can replay it manually once your server is healthy.