Skip to content

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.

json
{
  "success": false,
  "code": "INVALID_SIGNATURE",
  "message": "X-Signature did not match expected value",
  "data": null
}
FieldTypeNotes
successbooleantrue on 2xx, false otherwise.
codestringMachine-readable. SUCCESS on 2xx, otherwise one of the codes below.
messagestringHuman-readable. Safe to log. Do not show to end users untranslated.
dataobjectOperation-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.

StatusMeaningTypical causeRetry?
200OKQuery succeeded. code is SUCCESS.No need.
201CreatedOrder or session created. code is SUCCESS.No need.
400Bad RequestBusiness rule rejected the request, or the JSON body could not be parsed.No. Fix the request or surface to operator.
401UnauthorizedAPI key missing, invalid, expired, signature wrong, or timestamp out of window.No. Fix credentials or signing logic.
403ForbiddenMerchant inactive, or missing can_deposit / can_withdraw.No. Contact platform operator.
404Not FoundOrder not found, or order belongs to a different merchant.No. Verify the identifier.
409ConflictResource conflict, for example a duplicate merchant_order_no.No. Resolve the conflict before retrying.
422Unprocessable EntityRequest shape is valid JSON, but field-level validation failed.No. Fix the payload.
429Too Many RequestsRate limit exceeded. Reserved; not currently enforced on all endpoints.Yes, with exponential backoff.
500Internal Server ErrorUnhandled server error. Always envelope code is INTERNAL_ERROR.Yes, with backoff. Open a ticket if persistent.
502Bad GatewayUpstream channel or payout provider returned an invalid response.Yes, with backoff.
503Service UnavailableService is starting, draining, or temporarily down.Yes, with backoff.
504Gateway TimeoutUpstream 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

codeHTTP statusMeaningMerchant action
SUCCESS200, 201Operation succeeded.Continue. Read data for the operation payload.

Authentication and authorization

codeHTTP statusMeaningMerchant action
AUTHENTICATION_ERROR401Generic authentication failure.Verify X-API-Key, signing logic, and clock.
INVALID_SIGNATURE401X-Signature did not match the server-computed signature.Recompute the signature. Check parameter sorting and api_key appending.
INVALID_TIMESTAMP401X-Timestamp is missing, malformed, or outside the 5-minute window.Sync your clock to NTP. Resend with current Unix timestamp.
AUTHORIZATION_ERROR403Authenticated, 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

codeHTTP statusMeaningMerchant action
VALIDATION_ERROR400, 422Request 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

codeHTTP statusMeaningMerchant action
INVALID_STATE400The 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.
CONFLICT409A 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_ERROR400A 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

codeHTTP statusMeaningMerchant action
INSUFFICIENT_BALANCE400Generic insufficient balance on the source ledger.Top up or reduce the amount. Do not retry as-is.
INSUFFICIENT_MERCHANT_BALANCE400Merchant 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

codeHTTP statusMeaningMerchant action
NOT_FOUND404The 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

codeHTTP statusMeaningMerchant action
BUSINESS_ERROR400A 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_ERROR400, 502Upstream channel or payout provider returned an error.Retry with backoff and the same merchant_order_no for idempotency.
MERCHANT_API_ERROR400Parent 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

codeHTTP statusMeaningMerchant action
INTERNAL_ERROR500Unhandled 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 statusRetry safe?Strategy
200, 201N/ADone. Persist the result.
400NoAbort. Fix the request before sending again.
401NoAbort. Credentials or signing are wrong. Retrying will not fix this.
403NoAbort. Operator action is required.
404NoAbort. The identifier is wrong or the resource does not exist.
409NoAbort. Resolve the conflict, for example by querying the existing order first.
422NoAbort. The payload is malformed.
429YesExponential backoff with jitter. Cap at, for example, 5 attempts over 60 seconds.
500YesExponential backoff. Always use the same merchant_order_no to stay idempotent.
502YesExponential backoff. Upstream channel may recover.
503YesExponential backoff. Service may be restarting.
504YesExponential 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.
  • code and request_id if the response carries one.
  • Your own merchant_order_no and any internal trace ID.
  • Outbound X-Timestamp and the timestamp skew you observed.

Do not log:

  • X-API-Key value. Log only a short prefix or a hash.
  • X-Signature value. 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.

  1. Treating HTTP 200 as success without reading the envelope. The merchant API returns HTTP 200 for some query responses where success is still false. Always read both the status and the envelope code.

  2. 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.

  3. Hammering 429 without backoff. When rate limiting is enforced, repeated requests without backoff will keep you locked out. Use exponential backoff with jitter.

  4. 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 by merchant_order_no to see the real state before deciding.

  5. Branching on message text. The message field is not part of the API contract. New wording, translations, or detail changes will silently break your client. Branch on code only.

  6. 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.

  7. Reusing merchant_order_no across different orders. This produces a 409 CONFLICT. Generate a fresh merchant_order_no for each new order intent. Reuse the same merchant_order_no only when retrying the same order.

  8. Omitting merchant_order_no retry semantics. Without a stable idempotency key, a network retry after a 5xx can create a duplicate order. The server cannot deduplicate for you.

  9. Clock drift on X-Timestamp. The server accepts a 5-minute window on X-Timestamp. A client clock more than 5 minutes off produces INVALID_TIMESTAMP. Use NTP. Do not "fix" it by sending the server's clock back; sign with your own current Unix time.

  10. 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.

  11. 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.

  12. 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, and X-Signature are 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.