Every error from /v1/* follows a single envelope shape. Decline codes follow ISO-8583 conventions so they map cleanly to existing processor flows.
Every non-2xx response (and every declined 200 response on the auth endpoints) carries an error object — or, for declines, a top-level reasonCode + reasonMessage. Both use the same numeric / string codes.
{
"error": {
"code": "54", // stable machine identifier
"message": "Card expired", // human-readable
"field": "panToken", // optional, when validation
"details": { ... } // optional context
}
}
Always switch on error.code (or reasonCode) for your handling logic — messages are subject to wording changes. Codes are stable.
| Status | error.code | Meaning |
|---|---|---|
| 400 | bad_request | Malformed request — missing required header, invalid JSON, etc. |
| 400 | missing_field | A required body field is missing. error.field names it. |
| 400 | invalid_field | A body field fails validation. error.field names it. |
| 401 | unauthorized | Missing / unknown X-API-Key or invalid bearer. |
| 401 | invalid_signature | HMAC mismatch or timestamp drift > 300s (signing-enabled tenants). |
| 403 | forbidden | Tenant not active, or source IP outside the allowlist. |
| 403 | (no body) | API Gateway rejection — key not in usage plan or daily quota exhausted. |
| 404 | not_found | Resource doesn’t exist or isn’t owned by this tenant. |
| 409 | conflict | Generic state conflict. |
| 409 | invalid_state | Mutation attempted on an authorization in the wrong state (e.g. capturing a declined auth). |
| 409 | fully_captured | Trying to capture more than the remaining hold. |
| 409 | nothing_to_reverse | Trying to reverse an already-fully-reversed authorization. |
| 409 | idempotency_race | Idempotency-Key collision under concurrent submit. Retry once. |
| 422 | 14 | Invalid panToken — token not found, inactive, or wrong type. |
| 429 | rate_limited | Tenant rate limit exceeded. |
| 500 | server_error | Internal error. Retry-safe with the same Idempotency-Key. |
When an authorization is declined, status: "declined" is returned with a reasonCode. These are the same numeric values defined in ISO-8583 plus a handful of RMO-specific ones.
| Code | Title | When it fires |
|---|---|---|
| 14 | Invalid card number | panToken not found, inactive, or wrong type. |
| 54 | Expired card / code | Card past expiration, or payment code past expiresAt. |
| 61 | Exceeds limit | Limit-engine breach (daily cap, per-tx cap, etc.), or amount > payment-code maxAmount. |
| 62 | Restricted | Card status isn’t Active; or payment code is revoked / locked to another merchant. |
| 65 | Exceeds withdrawal frequency | Velocity-limit breach. |
| 75 | PIN tries exceeded / invalid PIN | Wrong PIN on payment-code redemption. After lockoutThreshold bad attempts, the code is locked. |
When you reverse an authorization, you can set reason in the request body. Default is RV02.
| Code | Meaning |
|---|---|
| RV01 | Merchant-initiated reversal (no longer needed). |
| RV02 | Customer-requested void / refund. |
| RV03 | Fraud / chargeback reversal. |
A decline is a 200 response with status: "declined" — the network reached a decision and you should show the customer the reason. An error is a 4xx/5xx with an error object — your request itself was malformed or your tenant misconfigured. Don’t conflate the two.
5xx responses are retry-safe as long as you reuse the exact Idempotency-Key. If we already committed the side effect, you’ll get the original response back; if not, your retry creates it cleanly.
A 400 / 401 / 403 / 404 / 409 will always produce the same result on retry — fix the request first. 429 is the exception: back off then retry with the same key.
error.field on validation failuresIt tells you exactly which body field needs to change. Surface it directly to whatever called your code — this turns ambiguous “something went wrong” bugs into precise field-level errors.