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
200with anIntegrationOperationResponsebody whosestatusfield carries the result (SUCCESS/PENDING/DECLINED/RETRIABLE/UNKNOWN). A business decline — insufficient funds, fraud, expired card, wrong CVV — is not an HTTP error: return200withstatus: DECLINEDand anerrorCode. - An HTTP
4xx/5xxsignals an infrastructure error only — a malformed request, an auth failure, your service crashed. payments-service does not read astatusbody from these; it derives the outcome from the HTTP status alone (see §3).
Warning: Never return a business decline as a
4xx. A4xxis read as a non-retriable infrastructure failure (UNKNOWN), and a5xxis read as a transient failure and retried — so a "declined" sent as402/400either loses its decline reason or gets re-attempted. For a transient problem use a5xxor200 RETRIABLE; for a terminal decline use200 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.
status | Terminal? | Meaning |
|---|---|---|
SUCCESS | Yes (success) | Operation completed. Return transactionReference. |
PENDING | No | Requires async completion (3DS / redirect / webhook). Return transactionReference, redirectUrl, and ideally expectedTtlSeconds; resolve later via the result callback. |
DECLINED | Yes | Business-level decline (insufficient funds, card blocked, …). Terminal — do not retry. Set errorCode. |
RETRIABLE | No | Transient error, safe to retry (network blip, PSP timeout). Set errorCode. |
UNKNOWN | No (reconcile) | Ambiguous outcome; manual investigation / reconciliation may be needed. Set errorCode. |
Note:
transactionReferenceis required onSUCCESSandPENDING— 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.errorCodeis what you attach toDECLINED/RETRIABLE/UNKNOWN(see §2.2);reasonis 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.
| ID | Code | Responsible | Description |
|---|---|---|---|
| 3 | CARD_EXPIRED | User | Card expired |
| 4 | INVALID_CARD_STATUS | Issuer Bank | Card is inactive, blocked, or otherwise invalid |
| 5 | DECLINED_TRANSACTION_BY_ISSUER | Issuer Bank | Transaction declined by issuer |
| 6 | NOT_ENOUGH_MONEY | User | Insufficient funds |
| 7 | EXCEEDED_CARD_LIMIT | User | Card usage limit exceeded |
| 8 | ANTIFRAUD | Issuer Bank | Declined due to issuer's antifraud system |
| 9 | SYSTEM_ERROR | LP | Internal payment processing error |
| 10 | FAILURE_3DS_CHECK | User | 3DS authentication failure |
| 11 | FAILURE_SECRET_CODE_CHECK | User | Wrong CVV2/CVC2 |
| 12 | TIMEOUT_GATEWAY | Payment Gateway | Timeout in payment gateway |
| 13 | BLACKLIST | Payment Gateway | Card or BIN is blacklisted |
| 15 | BAD_REQUEST | LP | Malformed request |
| 16 | GATEWAY_ERROR | Payment Gateway | Generic payment gateway error (also the fallback for any unmapped code) |
| 17 | INVALID_AMOUNT | LP | Invalid transaction amount |
| 19 | WRONG_CARD_NUMBER | User | Wrong card number |
| 20 | TRANSACTION_NOT_PERMITTED | Issuer Bank | Transaction not permitted |
| 21 | INVALID_EXPIRY_DATE | User | Invalid expiration date |
| 22 | INVALID_CARDHOLDER_NAME | User | Invalid cardholder name |
| 23 | ISSUER_ERROR | Issuer Bank | Issuer bank system error |
| 24 | TIMEOUT_ISSUER | Issuer Bank | Timeout during communication with issuer bank |
| 25 | UNSUPPORTED_CARD_TYPE | Payment Gateway | Card type not supported |
| 26 | DUPLICATE_TRANSACTION | LP | Duplicate transaction detected |
| 27 | INVALID_SETTINGS | LP | Invalid configuration (e.g. Merchant ID) |
| 28 | CURRENCY_NOT_SUPPORTED | Payment Gateway | Unsupported transaction currency |
| 29 | RESTRICTED_COUNTRY | Payment Gateway | Restricted card country or IP |
| 30 | CARD_NOT_SUPPORTED_FOR_MERCHANT | Payment Gateway | Card 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_ERRORfor any value it doesn't recognize (and also for a blank/absenterrorCode). 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 isPaymentErrorCode.FAILURE_3_DS_CHECK(thegetValue()string is stillFAILURE_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, returningGATEWAY_ERRORfor 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 status | Meaning |
|---|---|
AUTHORIZED | Funds held, not yet captured. |
CAPTURED | Payment completed (settled). |
VOIDED | Authorization reversed. |
REFUNDED | Payment refunded. |
DECLINED | Payment declined by PSP / issuer. |
EXPIRED | Authorization expired. |
PENDING | Payment still in progress (3DS, async). |
NOT_FOUND | Transaction not found at the PSP. |
Note: Echo the PSP's raw status string in
rawStatusfor 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 status | Interpreted as | Retried? | What you should do |
|---|---|---|---|
200 + status body | the business outcome you sent | per status | Always the preferred shape for any business result, including declines. |
400, 401, 403, 404 | non-retriable failure (UNKNOWN) | No | Use 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, 504 | transient failure (RETRIABLE) | Yes | Use for genuinely transient infrastructure problems (overload, upstream timeout). payments-service retries with backoff. |
501 | operation not implemented (UNKNOWN) | No | Return 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 a5xx/ timeout /200 RETRIABLE; anything terminal must be a200 DECLINEDor a4xx/501infrastructure 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 return | What payments-service does | What you should do |
|---|---|---|
200 SUCCESS | Records the payment as charged/authorized against your transactionReference and continues the pipeline. | Always include a stable, unique transactionReference. |
200 PENDING | Suspends 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 DECLINED | Treats 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 / timeout | Treats the failure as transient and retries with backoff. | Return this only for genuinely transient problems, or payments-service will hammer a permanent failure. |
200 UNKNOWN | Treats 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/404 | Non-retriable infrastructure failure (UNKNOWN); does not retry. | Fix the request/auth issue — a retry fails identically. Don't use these for declines. |
501 | Marks 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 anIdempotency-Keyon the result callback; there is no extra de-duplication key for you to invent.
Next: Integration Checklist →