Merchant API
References

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

CodeTypical HTTPMerchant meaning
api_key_missing401No Authorization header was sent.
api_key_malformed401The header was present but the value was not a valid key.
api_key_invalid401The key was not recognised.
api_key_revoked401The key was valid but has been revoked. Rotate.
merchant_inactive401The merchant is disabled or soft-deleted.
ip_not_allowed403The request IP address is not in the merchant IP allowlist.
rate_limited429Too many requests on this key. Back off and retry.
validation_failed400The request body or query failed schema validation. The message names the offending field.
currency_not_enabled422The currency is not enabled for this Merchant.
country_not_enabled422The country is not enabled for this Merchant.
payment_surface_not_allowed403Team policy denies direct Payment or Hosted Checkout creation.
payment_configuration_unavailable422The known method or option has no configured eligible execution, or the Provider did not accept the platform credential for it.
payment_method_unavailable422 or 503422: 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_unavailable422Payout is unavailable for this Merchant configuration.
balance_unavailable503Authoritative Balance data is unavailable; no synthetic value was returned.
insufficient_available_balance422The 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_hours422The payout is outside the configured operating window.
amount_below_minimum422Amount is below the configured per-transaction minimum.
amount_above_maximum422Amount exceeds the configured per-transaction maximum. With reason: quota_exceeded, the transaction volume limit for the period is reached.
quota_exceeded422The transaction count limit for the period is reached.
duplicate_merchant_reference409An intent with this merchant_reference already exists for the merchant.
idempotency_conflict409The Idempotency-Key was reused with a different request body, or the original request is in flight.
payment_method_invalid422The Provider declined the payment method. reason gives the cause.
invalid_state422The intent is no longer in the state required for this action (for example cancelling a terminal Payout).
not_found404The resource does not exist, or it belongs to another merchant or environment.
internal_error500The 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.

StatusRetry?What to do
503YesWait for Retry-After, then retry with the same Idempotency-Key and body.
500YesRetry with the same key and body after a back-off.
429YesBack off, then retry.
422 insufficient_available_balanceAfter a top-upRetry with the same key.
422 outside_operating_hours, quota_exceeded, amount_above_maximum with reason: quota_exceededLaterSend again when the window opens or the limit resets, with a new key.
Other 4xxNoFix the request or the configuration. A retry with the same key replays the same error for 24 hours.
409 idempotency_conflictNoDo 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.

On this page