Skip to main content

Message & Error Codes

Read first: Payment Flows

This is the reference catalog of the codes and statuses that travel across the Payments integration API v1 contract: the operation outcomes (status) you return on every call, the HTTP statuses payments-service interprets as infrastructure errors, the PaymentErrorCode values you attach to non-success outcomes, and the status-inquiry statuses you report for reconciliation. Field-level definitions live in the spec — the IntegrationStatus, PaymentErrorCode, IntegrationOperationResponse, and IntegrationStatusInquiryResponse schemas in the Payments API specification — this page adds the meaning and the recommended handling.

1. The golden rule: business outcome vs infrastructure error

The contract has two failure surfaces, and they live on different layers. Keep them separate.

  • A business outcome is always HTTP 200 with an IntegrationOperationResponse body whose status field carries the result (SUCCESS / PENDING / DECLINED / RETRIABLE / UNKNOWN). A business decline — insufficient funds, fraud, expired card, wrong CVV — is not an HTTP error: return 200 with status: DECLINED and an errorCode.
  • An HTTP 4xx / 5xx signals an infrastructure error only — a malformed request, an auth failure, your service crashed. payments-service does not read a status body from these; it derives the outcome from the HTTP status alone (see §3).

Warning: Never return a business decline as a 4xx. A 4xx is read as a non-retriable infrastructure failure (UNKNOWN), and a 5xx is read as a transient failure and retried — so a "declined" sent as 402/400 either loses its decline reason or gets re-attempted. For a transient problem use a 5xx or 200 RETRIABLE; for a terminal decline use 200 DECLINED.

2. The codes the contract actually uses

2.1 Operation outcome statuses (IntegrationStatus)

Every payment operation returns one of five status values in its 200 body. payments-service branches on this value. Per-value meaning is on the IntegrationStatus enum in the spec; the table restates it for reference.

statusTerminal?Meaning
SUCCESSYes (success)Operation completed. Return transactionReference.
PENDINGNoRequires async completion (3DS / redirect / webhook). Return transactionReference, redirectUrl, and ideally expectedTtlSeconds; resolve later via the result callback.
DECLINEDYesBusiness-level decline (insufficient funds, card blocked, …). Terminal — do not retry. Set errorCode.
RETRIABLENoTransient error, safe to retry (network blip, PSP timeout). Set errorCode.
UNKNOWNNo (reconcile)Ambiguous outcome; manual investigation / reconciliation may be needed. Set errorCode.

Note: transactionReference is required on SUCCESS and PENDING — payments-service stores it, matches the async result callback back to the suspended payment with it, and uses it as the reference for later capture / refund / reverse. Make it stable and unique. errorCode is what you attach to DECLINED / RETRIABLE / UNKNOWN (see §2.2); reason is free human-readable text (safe to log — never include PAN or secrets).

2.2 Payment error codes (PaymentErrorCode)

On a DECLINED, RETRIABLE, or UNKNOWN outcome you SHOULD attach a standardized errorCode. This is the full catalog — all values of PaymentErrorCode. The Responsible column tells you who owns the failure; payments-service uses it for reporting, CRM, and alerting.

Responsible parties: User — cardholder input or account issue · Issuer Bank — issuing bank declined or system error · Payment Gateway — PSP-level error · LP — internal system / configuration error.

IDCodeResponsibleDescription
3CARD_EXPIREDUserCard expired
4INVALID_CARD_STATUSIssuer BankCard is inactive, blocked, or otherwise invalid
5DECLINED_TRANSACTION_BY_ISSUERIssuer BankTransaction declined by issuer
6NOT_ENOUGH_MONEYUserInsufficient funds
7EXCEEDED_CARD_LIMITUserCard usage limit exceeded
8ANTIFRAUDIssuer BankDeclined due to issuer's antifraud system
9SYSTEM_ERRORLPInternal payment processing error
10FAILURE_3DS_CHECKUser3DS authentication failure
11FAILURE_SECRET_CODE_CHECKUserWrong CVV2/CVC2
12TIMEOUT_GATEWAYPayment GatewayTimeout in payment gateway
13BLACKLISTPayment GatewayCard or BIN is blacklisted
15BAD_REQUESTLPMalformed request
16GATEWAY_ERRORPayment GatewayGeneric payment gateway error (also the fallback for any unmapped code)
17INVALID_AMOUNTLPInvalid transaction amount
19WRONG_CARD_NUMBERUserWrong card number
20TRANSACTION_NOT_PERMITTEDIssuer BankTransaction not permitted
21INVALID_EXPIRY_DATEUserInvalid expiration date
22INVALID_CARDHOLDER_NAMEUserInvalid cardholder name
23ISSUER_ERRORIssuer BankIssuer bank system error
24TIMEOUT_ISSUERIssuer BankTimeout during communication with issuer bank
25UNSUPPORTED_CARD_TYPEPayment GatewayCard type not supported
26DUPLICATE_TRANSACTIONLPDuplicate transaction detected
27INVALID_SETTINGSLPInvalid configuration (e.g. Merchant ID)
28CURRENCY_NOT_SUPPORTEDPayment GatewayUnsupported transaction currency
29RESTRICTED_COUNTRYPayment GatewayRestricted card country or IP
30CARD_NOT_SUPPORTED_FOR_MERCHANTPayment GatewayCard scheme or type not supported by merchant configuration

Note: The list is a standardized allow-list but not frozen to your PSP's vocabulary. If your PSP returns a proprietary code that doesn't map to any of the above, pass it through as-is — payments-service falls back to GATEWAY_ERROR for any value it doesn't recognize (and also for a blank/absent errorCode). Sending a precise standard code only ever improves the recorded reason; an unmappable one is never rejected.

Warning — wire value vs Java constant: The wire value is FAILURE_3DS_CHECK, but the generated Java enum constant is PaymentErrorCode.FAILURE_3_DS_CHECK (the getValue() string is still FAILURE_3DS_CHECK). Always send the wire value. This is the one code whose constant name and wire value differ.

Tip (SDK users): The SDK's ErrorCodeMapper.fromPspCode(...) maps the common ISO 8583 numeric codes ("51"NOT_ENOUGH_MONEY, "54"/"33"CARD_EXPIRED, …) and widespread PSP string codes ("insufficient_funds", "expired_card", "incorrect_cvc", …) to the codes above, returning GATEWAY_ERROR for anything unrecognized. Use it instead of hand-mapping.

Three rejection bodies — a terminal decline, a transient error, and an ambiguous outcome. Each is an HTTP 200; the status distinguishes them and the errorCode is the stable contract (reason is free text).

{ "status": "DECLINED",  "errorCode": "NOT_ENOUGH_MONEY", "reason": "Insufficient funds" }
{ "status": "RETRIABLE", "errorCode": "TIMEOUT_GATEWAY", "reason": "PSP timed out; retry" }
{ "status": "UNKNOWN", "errorCode": "GATEWAY_ERROR", "reason": "No definitive response from PSP" }

2.3 Status-inquiry statuses (inquire-status)

When payments-service reconciles a payment it cannot otherwise resolve, it calls POST /v1/inquire-status and you return the PSP's current status. This is a different vocabulary from the operation outcome in §2.1 — it describes where the transaction is at the PSP, not the result of a single call. Implement it if you declare STATUS_POLLING.

Inquiry statusMeaning
AUTHORIZEDFunds held, not yet captured.
CAPTUREDPayment completed (settled).
VOIDEDAuthorization reversed.
REFUNDEDPayment refunded.
DECLINEDPayment declined by PSP / issuer.
EXPIREDAuthorization expired.
PENDINGPayment still in progress (3DS, async).
NOT_FOUNDTransaction not found at the PSP.

Note: Echo the PSP's raw status string in rawStatus for debugging, alongside the mapped value above.

2.4 Infrastructure error body (IntegrationErrorResponse)

For genuine infrastructure errors — the 4xx / 5xx cases — return an IntegrationErrorResponse body with a human-readable error (and optional details). There is no code enum here: the HTTP status is the contract, and the body is diagnostic text only (do not put a PaymentErrorCode here — those belong on a 200 business outcome).

3. How payments-service maps your HTTP status

For any response that is not a 200-with-status-body, payments-service derives the outcome from the HTTP status alone — it does not read an errorCode. This is the authoritative mapping.

Your HTTP statusInterpreted asRetried?What you should do
200 + status bodythe business outcome you sentper statusAlways the preferred shape for any business result, including declines.
400, 401, 403, 404non-retriable failure (UNKNOWN)NoUse only for real request/auth problems — never for a business decline. A byte-identical retry would fail identically, so payments-service does not retry.
408, 429, 500, 502, 503, 504transient failure (RETRIABLE)YesUse for genuinely transient infrastructure problems (overload, upstream timeout). payments-service retries with backoff.
501operation not implemented (UNKNOWN)NoReturn for an operation your integration doesn't support (e.g. sale on an authorize+capture-only integration). The SDK returns it automatically for un-overridden methods.

Note: A timeout with no response is treated like a 5xx — transient and retriable. So the rule is symmetric: anything you want retried must be a 5xx / timeout / 200 RETRIABLE; anything terminal must be a 200 DECLINED or a 4xx/501 infrastructure failure.

4. What payments-service does on each outcome

How payments-service reacts — and therefore what your integration should do — depends on the outcome you return.

Outcome you returnWhat payments-service doesWhat you should do
200 SUCCESSRecords the payment as charged/authorized against your transactionReference and continues the pipeline.Always include a stable, unique transactionReference.
200 PENDINGSuspends the payment and waits for the result callback (bounded by expectedTtlSeconds); may reconcile via inquire-status.Return redirectUrl + transactionReference, then POST the final result to the callback. Don't leave a PENDING unresolved.
200 DECLINEDTreats the payment as terminally failed with your errorCode; does not retry.Send the most precise PaymentErrorCode you can; this is what surfaces in CRM / reporting.
200 RETRIABLE / 5xx / timeoutTreats the failure as transient and retries with backoff.Return this only for genuinely transient problems, or payments-service will hammer a permanent failure.
200 UNKNOWNTreats the outcome as ambiguous — does not assume success or failure; flags for reconciliation.Implement inquire-status (STATUS_POLLING) so the real state can be recovered.
400/401/403/404Non-retriable infrastructure failure (UNKNOWN); does not retry.Fix the request/auth issue — a retry fails identically. Don't use these for declines.
501Marks the operation unsupported (UNKNOWN), non-retriable.Declare only the capabilities you implement so payments-service never calls an unsupported operation.

Note: payments-service's resumption is idempotent by transactionReference — re-delivering a result the payment has already resumed on (a callback retry, or a poll/callback overlap) is a safe no-op. Include an Idempotency-Key on the result callback; there is no extra de-duplication key for you to invent.


Next: Integration Checklist →