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 | A concurrent update raced this request on the same merchant balance or order row. | Retry the same request up to a small bound (3 attempts) with a short jitter. |
OPTIMISTIC_LOCK_ERROR is the one business-layer code that is safe to retry without changes. Cap retries and add jitter to avoid thundering-herd.
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), retry up to 3 times with short jitter even though the status is 400.
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.Not handling
OPTIMISTIC_LOCK_ERROR. Concurrent updates on the same merchant balance can lose a race. Retry up to 3 times with short jitter. Do not surface the first conflict to the operator.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.