This chapter is the canonical error reference for the Payment Gateway merchant API. Use it to map HTTP status codes and business error codes to merchant action: retry, fix the request, or surface to the operator.
Response envelope
Every merchant API response, success or failure, uses the same JSON envelope. Read the code field, not the HTTP status alone, to decide what to do.
{
"success": false,
"code": "INVALID_SIGNATURE",
"message": "X-Signature did not match expected value",
"data": null
}| Field | Type | Notes |
|---|---|---|
| success | boolean | true on 2xx, false otherwise. |
| code | string | Machine-readable. SUCCESS on 2xx, otherwise one of the codes below. |
| message | string | Human-readable. Safe to log. Do not show to end users untranslated. |
| data | object | Operation-specific payload on success. null on errors. |
Some error responses also include a details object with field-level information (for example, Pydantic validation errors). Treat details as optional and advisory.
Always branch on code. Do not branch on message: message text is not part of the API contract and may change.
HTTP status codes
The merchant API uses the following HTTP statuses. The "Retry?" column states whether the merchant client should retry the same request without operator action.
| Status | Meaning | Typical cause | Retry? |
|---|---|---|---|
| 200 | OK | Query succeeded. code is SUCCESS. | No need. |
| 201 | Created | Order or session created. code is SUCCESS. | No need. |
| 400 | Bad Request | Business rule rejected the request, or the JSON body could not be parsed. | No. Fix the request or surface to operator. |
| 401 | Unauthorized | API key missing, invalid, expired, signature wrong, or timestamp out of window. | No. Fix credentials or signing logic. |
| 403 | Forbidden | Merchant inactive, or missing can_deposit / can_withdraw. | No. Contact platform operator. |
| 404 | Not Found | Order not found, or order belongs to a different merchant. | No. Verify the identifier. |
| 409 | Conflict | Resource conflict, for example a duplicate merchant_order_no. | No. Resolve the conflict before retrying. |
| 422 | Unprocessable Entity | Request shape is valid JSON, but field-level validation failed. | No. Fix the payload. |
| 429 | Too Many Requests | Rate limit exceeded. Reserved; not currently enforced on all endpoints. | Yes, with exponential backoff. |
| 500 | Internal Server Error | Unhandled server error. Always envelope code is INTERNAL_ERROR. | Yes, with backoff. Open a ticket if persistent. |
| 502 | Bad Gateway | Upstream channel or payout provider returned an invalid response. | Yes, with backoff. |
| 503 | Service Unavailable | Service is starting, draining, or temporarily down. | Yes, with backoff. |
| 504 | Gateway Timeout | Upstream channel timed out. | Yes, with backoff and idempotency key. |
Retry guidance applies to the same request as sent. For 5xx on a create call, always use an idempotency key (the merchant_order_no) so the retry does not produce a duplicate order.
Business error codes
The following codes are surfaced in the code field of the envelope. Codes are stable. Status pairings reflect the current backend behavior.
Success
| code | HTTP status | Meaning | Merchant action |
|---|---|---|---|
SUCCESS | 200, 201 | Operation succeeded. | Continue. Read data for the operation payload. |
Authentication and authorization
| code | HTTP status | Meaning | Merchant action |
|---|---|---|---|
AUTHENTICATION_ERROR | 401 | Generic authentication failure. | Verify X-API-Key, signing logic, and clock. |
INVALID_SIGNATURE | 401 | X-Signature did not match the server-computed signature. | Recompute the signature. Check parameter sorting and api_key appending. |
INVALID_TIMESTAMP | 401 | X-Timestamp is missing, malformed, or outside the 5-minute window. | Sync your clock to NTP. Resend with current Unix timestamp. |
AUTHORIZATION_ERROR | 403 | Authenticated, but not allowed: inactive merchant or missing operation permission. | Contact the platform operator. Do not retry. |
The auth dependency rejects bad keys, bad signatures, and stale timestamps before reaching any business logic. Treat all 401 responses as terminal for the current request.
Validation
| code | HTTP status | Meaning | Merchant action |
|---|---|---|---|
VALIDATION_ERROR | 400, 422 | Request payload failed validation. 422 is returned for Pydantic field errors with a details.errors array. 400 is returned when a service rejects the payload at business boundary. | Fix the payload. Do not retry without changes. |
State and conflict
| code | HTTP status | Meaning | Merchant action |
|---|---|---|---|
INVALID_STATE | 400 | The target resource is not in a state that allows this operation, for example cancelling a completed order. | Re-query the order to see the current status. Do not retry blindly. |
CONFLICT | 409 | A uniqueness constraint was violated, typically a duplicate merchant_order_no. | Treat as duplicate. Query the existing order before deciding to retry with a new merchant_order_no. |
OPTIMISTIC_LOCK_ERROR | 400 | Extremely rare. Row-lock waiters on the same merchant balance row timed out (lock_timeout) after backend retries. The code name is kept for historical compatibility; it is not a version optimistic-lock conflict. | Tiny-bound retry 1–2 times; then query by merchant_order_no. Escalate persistent hits — do not treat as an everyday race. |
OPTIMISTIC_LOCK_ERROR may still be retried on the same request within a tiny bound for historical compatibility, but it is no longer "the one safe everyday business-layer retry code". If the envelope exposes details.reason, reason=lock_timeout means row-lock timeout.
Balance
| code | HTTP status | Meaning | Merchant action |
|---|---|---|---|
INSUFFICIENT_BALANCE | 400 | Generic insufficient balance on the source ledger. | Top up or reduce the amount. Do not retry as-is. |
INSUFFICIENT_MERCHANT_BALANCE | 400 | Merchant available balance is too low to cover a withdrawal. | Wait for deposits to settle, then retry with same amount, or reduce the amount. |
Not found
| code | HTTP status | Meaning | Merchant action |
|---|---|---|---|
NOT_FOUND | 404 | The order does not exist, or it exists but belongs to a different merchant. The two cases are intentionally indistinguishable. | Verify the identifier you sent. |
Business and channel
| code | HTTP status | Meaning | Merchant action |
|---|---|---|---|
BUSINESS_ERROR | 400 | A business rule rejected the request, for example "order has expired" or "no available deposit account". Read message for context. | Fix the request or surface to operator. Do not retry the same request. |
EXTERNAL_SERVICE_ERROR | 400, 502 | Upstream channel or payout provider returned an error. | Retry with backoff and the same merchant_order_no for idempotency. |
MERCHANT_API_ERROR | 400 | Parent code for merchant-API-specific failures. Usually you will see a more specific subclass code such as INVALID_SIGNATURE. | Treat as terminal unless a more specific code says otherwise. |
System
| code | HTTP status | Meaning | Merchant action |
|---|---|---|---|
INTERNAL_ERROR | 500 | Unhandled server error. The message is generic in production. | Retry with backoff. If persistent, contact platform team. |
Retry strategy
Use this matrix when deciding how to handle a response in your client.
| HTTP status | Retry safe? | Strategy |
|---|---|---|
| 200, 201 | N/A | Done. Persist the result. |
| 400 | No | Abort. Fix the request before sending again. |
| 401 | No | Abort. Credentials or signing are wrong. Retrying will not fix this. |
| 403 | No | Abort. Operator action is required. |
| 404 | No | Abort. The identifier is wrong or the resource does not exist. |
| 409 | No | Abort. Resolve the conflict, for example by querying the existing order first. |
| 422 | No | Abort. The payload is malformed. |
| 429 | Yes | Exponential backoff with jitter. Cap at, for example, 5 attempts over 60 seconds. |
| 500 | Yes | Exponential backoff. Always use the same merchant_order_no to stay idempotent. |
| 502 | Yes | Exponential backoff. Upstream channel may recover. |
| 503 | Yes | Exponential backoff. Service may be restarting. |
| 504 | Yes | Exponential backoff. Use the same merchant_order_no because the order may have been created. |
Special case: on OPTIMISTIC_LOCK_ERROR (HTTP 400), you may still retry within a tiny bound (1–2 attempts) even though the status is 400. This code is extremely rare; persistent hits mean a backend anomaly (long-held balance-row lock) — query the order and escalate rather than treating it as a common 400 exception to hammer.
A sensible default backoff is min(2^attempt * 250ms + random_jitter, 8s) capped at 5 attempts.
Logging guidance
Log enough to debug, not enough to leak.
Log on every error:
- HTTP method, path, status.
codeandrequest_idif the response carries one.- Your own
merchant_order_noand any internal trace ID. - Outbound
X-Timestampand the timestamp skew you observed.
Do not log:
X-API-Keyvalue. Log only a short prefix or a hash.X-Signaturevalue. Log only that it failed; the value has no debug use.- Full bank account numbers, card numbers, or end-user PII. Log masked values.
- Request bodies that contain the items above.
For 5xx errors, capture request_id from response headers (when present) and the timestamp. Include both when contacting platform support.
Treat the envelope message as informational. Do not pattern-match on it to branch logic. Branch on code.
Common pitfalls
The following mistakes are common in merchant integrations. Each one corresponds to a real failure mode in the API.
Treating HTTP 200 as success without reading the envelope. The merchant API returns HTTP 200 for some query responses where
successis stillfalse. Always read both the status and the envelopecode.Retrying 401. A 401 means your key, signature, or timestamp is wrong. Retrying the same payload will produce the same 401. Fix the client and re-send a fresh signed request.
Hammering 429 without backoff. When rate limiting is enforced, repeated requests without backoff will keep you locked out. Use exponential backoff with jitter.
Ignoring 5xx on order create. A 5xx on create does not mean the order was not created. The server may have committed the order before failing. Retry with the same
merchant_order_no, then query bymerchant_order_noto see the real state before deciding.Branching on
messagetext. Themessagefield is not part of the API contract. New wording, translations, or detail changes will silently break your client. Branch oncodeonly.Confusing 400 with 422. 400 means the business layer rejected an otherwise well-formed request (for example, "order has expired"). 422 means the request shape itself failed field validation. The fix is different in each case: 400 may be operational, 422 is always a client bug.
Reusing
merchant_order_noacross different orders. This produces a 409CONFLICT. Generate a freshmerchant_order_nofor each new order intent. Reuse the samemerchant_order_noonly when retrying the same order.Omitting
merchant_order_noretry semantics. Without a stable idempotency key, a network retry after a 5xx can create a duplicate order. The server cannot deduplicate for you.Clock drift on
X-Timestamp. The server accepts a 5-minute window onX-Timestamp. A client clock more than 5 minutes off producesINVALID_TIMESTAMP. Use NTP. Do not "fix" it by sending the server's clock back; sign with your own current Unix time.Treating
OPTIMISTIC_LOCK_ERRORas an everyday race. This code means balance-row lock timeout (backend anomaly) and is extremely rare. Tiny-bound retry 1–2 times, then query bymerchant_order_no; escalate persistent hits. Never branch onmessagetext.Treating 404 as "definitely does not exist". A 404 may also mean the order belongs to a different merchant. If you are sure you created the order, recheck your API key and tenant before assuming the order is gone.
Logging the API key or signature. These are credentials. A leaked log file becomes a leaked credential. Log only prefixes or hashes.
Cross-references
- Authentication — how
X-API-Key,X-Timestamp, andX-Signatureare built and verified. - Callbacks — how server-to-merchant callbacks signal state changes that the synchronous API may have missed during a 5xx retry.
- Sandbox — how to reproduce each error code in a controlled environment before going live.