Errors
A closed union of error codes. Switch on the code, not the message.
Every Merchant API error response carries the same envelope:
{
"error": {
"code": "validation_failed",
"message": "amount: amount must be a positive integer string",
"issues": [{ "path": "amount", "message": "amount must be a positive integer string" }],
"request_id": "dZCmlkmVS5ushD7DVOLIQQ"
}
}The code field is a value in a closed union. Adding a code is a compatibility break that gets versioned and announced. You can — and should — write a switch over the union and cover every branch.
The message is human-readable detail for logs and dashboards. Do not parse it. Do not branch on it.
request_id is the same value as the x-request-id response header. Send it to support with the error. issues is present only with validation_failed and lists every field that failed. reason is optional detail for logs.
The closed union
| Code | Typical HTTP | Merchant meaning |
|---|---|---|
api_key_missing | 401 | No Authorization header was sent. |
api_key_malformed | 401 | The header was present but the value was not a valid key. |
api_key_invalid | 401 | The key was not recognised. |
api_key_revoked | 401 | The key was valid but has been revoked. Rotate. |
merchant_inactive | 401 | The merchant is disabled or soft-deleted. |
ip_not_allowed | 403 | The request IP address is not in the merchant IP allowlist. |
rate_limited | 429 | Too many requests on this key. Back off and retry. |
validation_failed | 400 | The request body or query failed schema validation. The message names the offending field. |
currency_not_enabled | 422 | The currency is not enabled for this Merchant. |
country_not_enabled | 422 | The country is not enabled for this Merchant. |
payment_surface_not_allowed | 403 | Team policy denies direct Payment or Hosted Checkout creation. |
payment_configuration_unavailable | 422 | The known method or option has no configured eligible execution, or the Provider did not accept the platform credential for it. |
payment_method_unavailable | 422 or 503 | 422: the requested Payment method is unavailable for these request facts. 503: no Provider account can take the method now; retry after Retry-After. |
payout_configuration_unavailable | 422 | Payout is unavailable for this Merchant configuration. |
balance_unavailable | 503 | Authoritative Balance data is unavailable; no synthetic value was returned. |
insufficient_available_balance | 422 | The Merchant available balance cannot cover the Payout amount and fees. |
approval_required | — | Reserved. Merchant API v1 does not send it. |
approval_not_required | — | Reserved. Merchant API v1 does not send it. |
outside_operating_hours | 422 | The payout is outside the configured operating window. |
amount_below_minimum | 422 | Amount is below the configured per-transaction minimum. |
amount_above_maximum | 422 | Amount exceeds the configured per-transaction maximum. With reason: quota_exceeded, the transaction volume limit for the period is reached. |
quota_exceeded | 422 | The transaction count limit for the period is reached. |
duplicate_merchant_reference | 409 | An intent with this merchant_reference already exists for the merchant. |
idempotency_conflict | 409 | The Idempotency-Key was reused with a different request body, or the original request is in flight. |
payment_method_invalid | 422 | The Provider declined the payment method. reason gives the cause. |
invalid_state | 422 | The intent is no longer in the state required for this action (for example cancelling a terminal Payout). |
not_found | 404 | The resource does not exist, or it belongs to another merchant or environment. |
internal_error | 500 | The platform failed in a way that is not the caller's fault. Safe to retry with the same idempotency key. |
Beta scope
Refunds are out of scope for the beta and have no public endpoint or error code:
- Refunds are deferred. There is no refund endpoint and no refund-specific error code in the beta. A succeeded payment intent cannot be reversed through the Merchant API. When these ship, their endpoints and any new codes are announced in the changelog and the OpenAPI spec ahead of release.
Closed-union promise
The set above is the contract. Removing or changing the meaning of an existing code is a major-version break. Adding a new code is a minor-version break that is announced in the changelog and the OpenAPI spec ahead of release.
In TypeScript, treat the union as exhaustive:
function explain(code: MerchantErrorCode): string {
switch (code) {
case "api_key_missing":
case "api_key_malformed":
case "api_key_invalid":
case "api_key_revoked":
return "Authentication problem. Check your key."
case "ip_not_allowed":
return "This network is not allowed for the merchant."
case "rate_limited":
return "Slow down. Retry after a backoff."
case "validation_failed":
return "Request shape is wrong. Fix it, then send with a new key."
case "payment_method_invalid":
return "The Provider declined the payment method. Ask the payer for another method."
case "currency_not_enabled":
case "country_not_enabled":
return "This currency or country is not enabled for the Merchant."
case "payment_surface_not_allowed":
return "Team policy blocks this checkout constraint."
case "payment_configuration_unavailable":
case "payment_method_unavailable":
case "payout_configuration_unavailable":
return "No configured execution supports this method or option."
case "invalid_state":
return "The intent is not in a state that allows this action."
case "amount_below_minimum":
case "amount_above_maximum":
case "insufficient_available_balance":
case "quota_exceeded":
case "outside_operating_hours":
return "Business rule blocked the request."
case "duplicate_merchant_reference":
return "This merchant_reference exists. Reconcile with the existing intent."
case "idempotency_conflict":
return "The key was used with other fields, or the first request is in flight. Do not resend with this key."
case "merchant_inactive":
return "Merchant state blocks this operation."
case "not_found":
return "Not found in this environment."
case "balance_unavailable":
case "internal_error":
return "Platform error. Wait for Retry-After on a 503, then retry with the same key."
case "approval_required":
case "approval_not_required":
return "Reserved. Merchant API v1 does not send it."
}
}The compiler will tell you when a new code lands.
Which errors to retry
The status tells you whether a retry can succeed.
| Status | Retry? | What to do |
|---|---|---|
| 503 | Yes | Wait for Retry-After, then retry with the same Idempotency-Key and body. |
| 500 | Yes | Retry with the same key and body after a back-off. |
| 429 | Yes | Back off, then retry. |
422 insufficient_available_balance | After a top-up | Retry with the same key. |
422 outside_operating_hours, quota_exceeded, amount_above_maximum with reason: quota_exceeded | Later | Send again when the window opens or the limit resets, with a new key. |
| Other 4xx | No | Fix the request or the configuration. A retry with the same key replays the same error for 24 hours. |
409 idempotency_conflict | No | Do not resend with this key. If the first request is in flight, wait and fetch the intent. |
Changes
2026-10-28. No Merchant received the codes invalid_currency and invalid_country in Live, so they changed name and status. They are now currency_not_enabled and country_not_enabled, both with status 422. approval_required and approval_not_required are reserved. quota_exceeded is now in this table; the API already sent it. A Provider refusal of the platform credential is now payment_configuration_unavailable, not payment_method_invalid. Every error body now has request_id, and validation_failed has issues.