Skip to content

Payment API error handling: error codes, error types, and which errors are safe to retry

How to handle payment API errors: read the error envelope, branch on codes not messages, know which errors are safe to retry, and debug with request_id.

Published 5 min read

Hand-drawn illustration of an error response card showing code 400, error and param fields, sorted into retry and fix-first trays, next to a person pointing at it
On this page
  1. What does a payment API error look like?
  2. Branch on error codes, not messages
  3. Error types and whether to retry them
  4. The payment API error codes you will meet most
  5. Never treat a timeout as "the order doesn't exist"
  6. Errors are not payment results
  7. Debug faster with request_id
  8. A payment API error handling checklist

Payment API error handling is the code that decides what your integration does when a request does not succeed: fix the request, retry it, or stop and tell someone. Getting it right matters more for payments than for most APIs, because the two classic mistakes are expensive: retrying an error that will never succeed, and not retrying — or retrying unsafely — an error whose outcome is unknown, which is how orders get lost or created twice.

This guide explains how to read a payment API error, why you should branch on error codes rather than messages, which errors are safe to retry, and how to debug a failed call quickly. The examples use KukoPay's API, and the principles apply to any payment provider.

What does a payment API error look like?

Every KukoPay endpoint returns errors in the same envelope:

{
  "code": 400,
  "error": "invalid_request",
  "type": "invalid_request_error",
  "message": "…",
  "param": "amount",
  "doc_url": "https://kukopay.com/docs/api/errors",
  "request_id": "req_9f2c1a7b4e8d0356c1ab77de90f4b215"
}
FieldUse it for
codeThe HTTP status, repeated in the body
errorThe stable, machine-readable code. Branch on this.
typeThe broad category. Stays the same when new codes are added, so it is good for coarse decisions like "retry or not"
messageA human-readable description. Wording can change — show it, log it, never branch on it
paramThe request field at fault, or null
doc_urlWhere the code is documented
request_idThis request's unique id, also in the X-Request-Id header

Branch on error codes, not messages

Messages are written for people and get reworded — and on KukoPay they are rendered in the language of the request, so the same error can arrive in English or Chinese. Code like if (message.includes("amount")) breaks the day the wording changes. Use error for specific handling and type for the general rule:

switch (body.error) {
  case "payment_limit_exceeded":
    return showBuyer("This amount is above what we can accept right now.")
  case "conflict":
    return reloadOrder(outTradeNo) // the order changed — read its latest state
  default:
    return handleByType(body.type)
}

Error types and whether to retry them

typeMeaningRetry?
authentication_errorMissing or invalid key; the request was not linked to any merchantAfter fixing the key
permission_errorAuthenticated, but not allowed to do thisNo
invalid_request_errorThe request itself is wrong: a bad field, an object that does not exist, a state conflictAfter fixing the request
idempotency_errorThe same Idempotency-Key was used for a different requestAfter fixing the request
rate_limit_errorOver the rate limitYes, after Retry-After
api_connection_errorThe upstream channel failed; your balance was not changedYes
api_errorAn internal platform errorYes

The practical rule:

  • Timeouts, 429, 502 and 500 — retry with the same Idempotency-Key and exponential backoff. The outcome is unknown or temporary; the key makes the retry safe.
  • 400, 401, 403 — do not loop. Fix the parameters, credentials or permissions first.
  • 404 — check the identifier, and that the key's environment and app match the object's.
  • 409 — read the order, refund or dispute again and decide from its current state.

How the key protects a retry is covered in what is an idempotency key, and the 429 case in API rate limits and 429 Too Many Requests.

The payment API error codes you will meet most

HTTPerrorWhat it meansWhat to do
401missing_api_key / invalid_api_keyNo key, or a wrong or revoked oneCheck the X-Api-Key header survives your proxy; update the key — see API key rotation
403merchant_not_approvedA live key before the account is approvedKeep building in the sandbox until verification completes
403ip_not_allowedThe request came from outside the app's IP allowlistCheck your server's outbound IP
403app_disabledThe key's app is disabled and cannot create ordersRe-enable the app or use another app's key
400invalid_requestA field is invalidFix the field named in param
400payment_limit_exceededOver the account's payment limitShow the buyer a clear message; see the limits in onboarding
400refund_window_expiredThe payment method's refund deadline has passedSettle with the buyer directly — see partial refunds
404order_not_found and other *_not_foundNo such object for this key's environment and appCheck the number, the environment and the app
409conflictState changed, an open dispute, or a reused out_trade_no with different contentRead the object again; use a new order number for a new order
429rate_limitedOver the per-minute quotaWait Retry-After seconds
502processor_errorThe channel failed; nothing changed on your balanceRetry with the same idempotency key
500internal_errorSomething failed on the platformRetry with the same key; if it persists, send the request_id

The full catalogue is in errors. It is generated from the same list the API uses, so the documented codes are the ones you receive.

Never treat a timeout as "the order doesn't exist"

A timeout is not an error response — you have no response at all. The order may well have been created. Do not generate a new order number and try again: that is how buyers end up with two orders. Either retry the same request with the same out_trade_no (and the same Idempotency-Key), which returns the existing order if there is one, or look the order up by out_trade_no first.

Errors are not payment results

A successful POST /v1/orders does not mean the buyer paid, and an error from your server does not mean they did not. Payment outcomes — paid, declined, expired — arrive through the order's status and webhooks, not as API errors. A card decline, for example, never reaches your API call: the buyer sees it at checkout and can try again (card decline codes explained). Decide fulfilment on the verified payment.succeeded webhook — see return_url vs notify_url.

Debug faster with request_id

Every response, success or failure, carries a request_id in the body and the X-Request-Id header. It is always issued by the platform and matches its own logs one to one.

  • Log it on every call in your HTTP client, next to your own order number.
  • Look the call up yourself: the request logs keep each call's request and response, with secrets redacted, for 30 days — filter by request_id, by status=4xx, by path or by error code.
curl -G "https://api.kukopay.com/v1/logs" \
  -H "X-Api-Key: $KUKOPAY_API_KEY" \
  -d status=4xx \
  -d limit=20
  • Send it to support when you need help: with a request_id the exact call can be found at once; without it, someone has to search by order number and time.

See API request logs.

A payment API error handling checklist

  • Branch on error, fall back on type, never on message;
  • retry timeouts, 429 and 5xx with backoff and the same Idempotency-Key;
  • fix 400/401/403 instead of retrying them;
  • on 409, re-read the object;
  • never answer a timeout with a new order number;
  • log request_id on every call;
  • decide payment outcomes from webhooks and order status, not from API errors.

Try it in the sandbox

Create a merchant account and request a test key. The sandbox needs no business verification.