Skip to main content
Download Markdown

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: errorCode is a closed enumeration — a proprietary value there fails schema validation. If your PSP returns a code that maps to none of the above, classify it with the closest standard code (GATEWAY_ERROR when nothing fits) and send the raw one in vendorErrorCode: payments-service never interprets that field, but keeps it in the recorded failure reason, so the vendor's own code stays available for diagnostics. A blank or absent errorCode becomes GATEWAY_ERROR. A proprietary value that does end up in errorCode is not rejected either — payments-service degrades it to GATEWAY_ERROR before deserialization and moves the raw value into vendorErrorCode.

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 →