ECASHPAYECASHPAYDocumentation
Back to site

Introduction

  • EcashPay API
  • Quickstart

Core concepts

  • Environments and API keys
  • Signing requests
  • Errors

Accept payments

  • Creating a payment
  • Payment methods
  • Confirming the outcome
  • Refunds, capture and cancel

Testing

  • Testing

API reference

  • API reference

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
{
  "success": true,
  "data": { "id": "b0f9c6d2-…", "status": "PENDING" },
  "meta": {
    "timestamp": "2026-08-13T09:00:00.000Z",
    "requestId": "3f1c8a52-…"
  }
}
Failure
{
  "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-After on 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.

CodeWhat it means
UNAUTHORIZED401No 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_RANGE401X-Ecash-Timestamp is missing, is not digits, or is more than 300 seconds from our clock. Sync against GET /v1/payments/server-time.
SIGNATURE_INVALID401The 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_REPLAYED401That nonce has been used within the last 300 seconds. Generate a fresh timestamp and nonce for every attempt, including retries.
PAYLOAD_DECRYPT_FAILED400The 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.

Debugging a stubborn SIGNATURE_INVALID

Print your pre-image and compare it character by character with "<timestamp>.<nonce>.<rawBody>". The usual culprits: milliseconds instead of seconds, signing the plaintext instead of the sealed body, a re-serialised body that no longer matches the bytes you sent, a missing trailing dot on a body-less request, or the wrong content type — a sealed body must go out as Content-Type: application/octet-stream. Sent as application/json it is rejected by the parser before the signature is checked at all; sent as anything else it reaches us as an empty body, which signs to a different digest than the one you sent.

Payment errors

These come from the payment itself, and each one tells you what to do next.

CodeWhat it means
DUPLICATE_REFERENCE_ID409You 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_REFUNDABLE409The payment is not in a state that can be refunded — it never succeeded, or it is already fully refunded.
NOTHING_LEFT_TO_REFUND409Earlier refunds have already returned the whole amount.
AMOUNT_EXCEEDS_REMAINING400The partial amount you asked for is larger than what is left after previous refunds.
REFUND_NOT_SUPPORTED400Refunds 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_UNREACHABLE502We could not reach the payment service. The payment may or may not have been created — read it back before retrying.
PAYMENT_SERVICE_TIMEOUT504The payment service did not answer in time. Treat exactly like the previous row: read, then decide.
PAYMENT_METHOD_UNAVAILABLE503That 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.

PreviousSigning requestsNextCreating a payment

On this page

  • The response envelope
  • Status codes
  • Request-security errors
  • Payment errors
  • Rate limits
  • Quoting a request id