Errors
One response envelope, one error shape, and what each code means.
The response envelope
Every response — success or failure, on every endpoint — has the same three top-level keys. Read success first, then data or error:
{
"success": true,
"data": { "id": "b0f9c6d2-…", "status": "PENDING" },
"meta": {
"timestamp": "2026-08-13T09:00:00.000Z",
"requestId": "3f1c8a52-…"
}
}{
"success": false,
"error": {
"code": "SIGNATURE_INVALID",
"message": "…",
"details": [ … ], // present on validation failures
"retryAfter": 30 // present on 429 only
},
"meta": {
"timestamp": "2026-08-13T09:00:00.000Z",
"requestId": "3f1c8a52-…"
}
}Do not branch on message. It is written for a human, it is translated — send Accept-Language: en or fil to choose — and it can be reworded in any release. error.code is the contract.
On a validation failure, details lists what was wrong field by field, which is usually enough to fix the call without reading anything else.
Status codes
- 200 / 201 — done. A created payment answers 201.
- 400 — the request was malformed: a missing field, a bad value, or a body that would not decrypt.
- 401 — the credential or the signature did not hold. Never retried as-is: fix the request.
- 404 — no such payment, transaction or payer in this environment. A live id read with a test key is a 404, not a 403.
- 409 — the request was well-formed but the state says no: a duplicate reference, a payment that cannot be refunded.
- 429 — rate limited. See below.
- 503 — the payment method you asked for is not chargeable right now. The request was correct: nothing to change, and nothing to retry on a timer — there is no
Retry-Afteron it. - 502 / 504 — the payment service failed or timed out. The only status codes where the right answer is to read the payment back rather than assume.
Request-security errors
These come from the layer described in Signing requests, before your request reaches any payment logic.
| Code | What it means |
|---|---|
UNAUTHORIZED401 | No X-Api-Key header, or a key that is unknown, revoked or expired. Also the answer when your account holds no signing key pair at all. |
SIGNATURE_TIMESTAMP_OUT_OF_RANGE401 | X-Ecash-Timestamp is missing, is not digits, or is more than 300 seconds from our clock. Sync against GET /v1/payments/server-time. |
SIGNATURE_INVALID401 | The HMAC did not match, or the header was malformed. Nine times out of ten the pre-image is wrong — check you signed the sealed base64 body and not the plaintext. |
SIGNATURE_REPLAYED401 | That nonce has been used within the last 300 seconds. Generate a fresh timestamp and nonce for every attempt, including retries. |
PAYLOAD_DECRYPT_FAILED400 | The signature was valid but the body would not open: wrong encryption key, the tag and IV in the wrong order, truncated framing, or plaintext that was not JSON. |
Payment errors
These come from the payment itself, and each one tells you what to do next.
| Code | What it means |
|---|---|
DUPLICATE_REFERENCE_ID409 | You have already created a payment with this referenceId. This is the double-charge guard working — read the existing payment instead of retrying with a new reference. |
STATUS_NOT_REFUNDABLE409 | The payment is not in a state that can be refunded — it never succeeded, or it is already fully refunded. |
NOTHING_LEFT_TO_REFUND409 | Earlier refunds have already returned the whole amount. |
AMOUNT_EXCEEDS_REMAINING400 | The partial amount you asked for is larger than what is left after previous refunds. |
REFUND_NOT_SUPPORTED400 | Refunds are not supported for this transaction — it was paid in a way that is returned outside EcashPay. Nothing to retry; contact us to arrange the return. |
PAYMENT_SERVICE_UNREACHABLE502 | We could not reach the payment service. The payment may or may not have been created — read it back before retrying. |
PAYMENT_SERVICE_TIMEOUT504 | The payment service did not answer in time. Treat exactly like the previous row: read, then decide. |
PAYMENT_METHOD_UNAVAILABLE503 | That payment method is switched off for your account right now. Your request was correct — nothing to change and nothing to retry on a timer; it comes back when it is switched on again. |
Rate limits
Requests are rate limited per caller. A 429 carries both a Retry-After header and error.retryAfter in the body, in seconds — they agree, so read whichever is easier.
Honour it: sleep for at least that long, then retry with a fresh timestamp, nonce and signature. Retrying immediately, or reusing the signature you already built, turns one 429 into a 429 followed by a SIGNATURE_REPLAYED. Add jitter if several of your workers can be throttled at once.
The hosted checkout and a handful of other public paths sit on a stricter tier than the authenticated data plane, so a 429 while polling and a 429 on checkout are separate budgets.
Quoting a request id
Every response carries meta.requestId, and it is also returned as a response header. Log it next to your own order reference on every call, successful or not.
When you contact support, that id is what lets someone find your exact request — including the body we decrypted from it — instead of asking you to reproduce the problem. It is by far the fastest thing you can put in a ticket.

