Two different things can go wrong with a payment, and they look nothing alike.
- The request failed. The API returns a
4xxor5xxwith an error body. Nothing was charged. - The payment was declined. The API returns
200with a transaction whoseresultisDECLINED. The request worked; the issuer said no.
This page is about the first. For the second, see Handle declines.
Every failure returns the same four fields.
{
"error_code": "INVALID_REQUEST_DATA",
"error_message": "The API request contains invalid data.",
"details": {
"amount": "Invalid amount",
"payment_method.pan": "Malformed PAN"
},
"source": {
"system": "TSYS",
"code": "13",
"message": "Invalid amount"
}
}| Field | What it carries |
|---|---|
error_code | The stable code to branch on. Always present |
error_message | That code in words, for a log |
details | Field name to problem, when the failure was about specific fields. Otherwise null |
source | The processor's own code and text, passed through unchanged. Null fields when the gateway failed before reaching a processor |
Branch on error_code, never on error_message. The message is prose and may be reworded. The code is part of the contract.
source is for your support and reconciliation, not for your control flow. It tells you what TSYS or VeriCheck said, in their vocabulary, so a support ticket can quote it.
Seven, and that is the whole list.
| Status | error_code | What happened | Safe to retry |
|---|---|---|---|
400 | INVALID_REQUEST_DATA | The request is malformed or a field is invalid. details names the fields | No — fix the request |
401 | NOT_AUTHENTICATED | Api-Key or Client-Id is missing or wrong | No — check both headers |
403 | UNAUTHORIZED_ACTION | Authenticated, but not allowed to do this | No |
404 | RESOURCE_NOT_FOUND | No such transaction, client, or integration | No — check the ID |
409 | DUPLICATE_ACTION | The action conflicts with the resource's current state | No — read the resource first |
500 | SYSTEM_ERROR | A fault inside the gateway | Yes, with backoff |
502 | DOWNSTREAM_ERROR | The gateway reached the processor and could not use its reply | Yes, but read the next section first |
A 401 does not distinguish a missing header from a wrong key. If you get one, check that both Api-Key and Client-Id are present before assuming the key is bad — a missing Client-Id produces the same answer.
Not every operation can return every code. 404 is declared on 11 of the 19 operations, 409 on 7, and 502 on only 4: creating, capturing, and canceling a transaction, and closing a batch.
This is the distinction that matters when money is involved.
- A
500is a fault inside the gateway. The processor was never reached, so no transaction exists. - A
502means the gateway reached the processor and could not use what came back. The processor may have seen the transaction.
Both are safe to retry in the sense that the API will accept the request again. Only one is safe to retry blindly.
After a 502 on a payment, reconcile before retrying. Call GET /v1/transactions filtered to the window in question and look for a transaction matching what you sent. The API has no idempotency key, so a retry that the first attempt already completed creates a second charge.
Retry 500 and 502. Do not retry 400, 401, 403, 404, or 409 — the request will fail the same way until you change it.
Use exponential backoff with jitter: wait a short interval, double it each attempt, and add a small random offset so a fleet of clients does not retry in lockstep. Three or four attempts is usually enough; past that, the failure is not transient.
There is no rate limit response today. If you are sending enough traffic to worry about one, talk to your implementation contact rather than designing around a 429 the API does not return.
Successful or not, every response includes an x-request-id header holding a GUID.
x-request-id: 73039bd9-c380-4186-bffe-259125144a56Log it with every request you send. It is what lets support trace one specific call through the gateway. A support ticket without it usually turns into a request for it.
Do not put error_code, response_code, or anything from source in front of a cardholder. "Do not honor" and 51 mean nothing to them and sometimes imply something untrue about their account.
Map to a short, actionable sentence — "That card was declined. Try another card" — and keep the codes in your logs, where they are useful.