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 must be a positive integer string"
}
}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.
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. |
merchant_kyb_not_approved | 403 | KYB is not in approved state. Money operations are gated until approval. |
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. |
invalid_currency | 400 | The currency is not enabled for this merchant. |
invalid_country | 400 | The country is not enabled for this merchant. |
routing_unavailable | 422 | No payment route is available to serve this request right now. |
insufficient_available_balance | 422 | The merchant's available balance cannot cover amount + fee. |
approval_required | 422 | The requested action depends on the payout approval state; for cancellation, the payout is outside the cancellable approval window. |
amount_below_minimum | 422 | Amount is below the configured per-transaction minimum. |
amount_above_maximum | 422 | Amount exceeds the configured per-transaction maximum. |
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 | 400 | The payment method hint is unknown, retired, or ineligible for this currency, country, or amount. |
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
Two areas 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.
- P2P actions are not part of the public beta contract.
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":
case "invalid_currency":
case "invalid_country":
case "payment_method_invalid":
return "Request shape is wrong. Fix and retry.";
case "amount_below_minimum":
case "amount_above_maximum":
case "insufficient_available_balance":
case "approval_required":
case "routing_unavailable":
return "Business rule blocked the request.";
case "duplicate_merchant_reference":
case "idempotency_conflict":
return "Conflict. Reconcile and retry with a fresh ref.";
case "merchant_inactive":
case "merchant_kyb_not_approved":
return "Merchant state blocks this operation.";
case "not_found":
return "Not found in this environment.";
case "internal_error":
return "Platform error. Retry with the same idempotency key.";
}
}The compiler will tell you when a new code lands.