Developers › Building on the API
Errors
Verified 6 November 2026Explain this to your team →
{
"error": {
"type": "card_error",
"code": "card_declined",
"decline_code": "insufficient_funds",
"message": "The card has insufficient funds.",
"request_id": "req_8fK2mN4pQ6rSaB3c"
}
}decline_code appears in an error response only when you charged the card synchronously, with confirm: true on a saved card. On the hosted checkout the cardholder is on the bank’s page when the decline happens, so there is no request of yours to fail: the same reason arrives on the payment itself, at last_payment_error.decline_code, and on the payment_intent.payment_failed event.Types
| Type | Meaning |
|---|---|
| invalid_request_error | The request was wrong. Fix it; retrying will not help. |
| authentication_error | The API key is missing, wrong, revoked or expired. |
| permission_error | Valid key, but not allowed to do this — e.g. live before approval. insufficient_permissions names the missing permission in required_capability. |
| card_error | The card was refused. decline_code says why. |
| idempotency_error | The key was reused with a different body, or is still in flight. |
| rate_limit_error | Too many requests. Back off and retry. |
| api_error | Our fault, or the card network could not be reached. Retry with the SAME Idempotency-Key — never a new one: see outcome_unknown below. |
The codes you will meet first
| Status | type / code | When |
|---|---|---|
| 400 | invalid_request_error / parameter_invalid | A field is missing or wrong, or the body is not valid JSON. param names the field, when there is one. |
| 401 | authentication_error / api_key_invalid | No key, or a wrong, revoked or wrong-mode one. |
| 404 | invalid_request_error / resource_missing | No such object — usually a mistyped id, or an id from the other mode. The message names it. |
| 404 | invalid_request_error / unknown_endpoint | No such path, asked with a valid key. |
| 402 | card_error / card_declined | The card was refused. decline_code says why. |
| 402 | card_error / payment_blocked | A risk rule refused the payment before the bank was asked. decline_code is blocked. |
| 400 | idempotency_error / idempotency_key_in_use | The Idempotency-Key was already used for a different request. |
| 409 | idempotency_error / idempotency_in_progress | The first request with this key is still running. Retry the same request in a few seconds. |
| 409 | invalid_request_error / payment_in_progress | The customer may be paying; see below. |
| 400 | invalid_request_error / refund_not_yet_possible | A partial refund before the bank has settled the payment. Retry after settlement with a new Idempotency-Key — safe for this code only; see refunds. |
| 429 | rate_limit_error / rate_limit_exceeded | More than 100 requests a minute from one live key, or 60 from all of a business's test keys together, its Sandboxes included. |
| 503 | api_error / gateway_unavailable | The bank could not be reached. Nothing was sent to it. |
| 503 | api_error / email_not_sent | We could not send an email right now. Retry it. |
| 503 | api_error / service_unavailable | Our database could not be reached before the request did any database work, so nothing was read or written. Retry after the Retry-After header's seconds, with the same Idempotency-Key. A database failure after work had begun is a 500 instead. |
| 400 | invalid_request_error / email_recipient_rejected | The address was refused. Retrying will not help; reach the customer another way. |
| 500 | api_error / outcome_unknown | The request reached the bank and we could not confirm what it did — it may have charged or refunded. With an Idempotency-Key: retry only with the same key, which replays this answer and never reaches the bank again; never retry under a new one. Without a key: do not repeat the request at all. Either way, list the payment's refunds or payment intents for its status; it settles on its own within minutes and its webhook follows. |
{
"error": {
"type": "invalid_request_error",
"code": "resource_missing",
"message": "No such payment intent: pi_3KqL8xQpR2sT7vW9yZ1aB4cX",
"request_id": "req_8fK2mN4pQ6rSaB3c"
}
}A mode mismatch reads as missing, deliberately: a test key cannot see live objects, so a live id asked for with a test key is resource_missing, not a permission error.
Handling one
import { SecureProcessingError } from "@secureprocessing/node";
try {
await sp.paymentIntents.create(params);
} catch (err) {
if (err instanceof SecureProcessingError) {
if (err.type === "card_error") showCustomer(err.message);
else if (err.isRetryable) scheduleRetry();
else report(err.requestId);
}
}Quote request_id to support and we can find the exact request. Every response carries the same value in its X-Request-Id header, successes included — that is where to read it when nothing went wrong. The one exception is a replay (Idempotent-Replayed: true): the body is the original response, so its request_id is the original request’s, while the header names the replay. Quote the one in the body — it is the request that did the work.
Decline codes
The codes a refused card can carry today. Anything we cannot map returns generic_decline, and new codes may be added, so treat one you do not recognise as generic_decline.
| decline_code | What it means |
|---|---|
| generic_decline | Refused, and the issuer gave no reason we can pass on. |
| insufficient_funds | Not enough money on the card. |
| withdrawal_count_limit_exceeded | The card has hit a limit on how much or how often it can be used. |
| expired_card | The card has expired. |
| invalid_expiry | The expiry date entered is wrong. |
| incorrect_cvc | The security code is wrong. |
| incorrect_number | The card number is wrong. |
| restricted_card | The card cannot be used for this payment. |
| lost_card / stolen_card | Reported lost or stolen. The customer is told only that it was declined. |
| transaction_not_allowed | The issuer does not allow this kind of payment on the card, online payments included. |
| call_issuer | The issuer wants the cardholder to contact them. |
| issuer_unavailable | The card issuer could not be reached. A retry shortly may succeed. |
| processing_error | Not the card: the payment could not be processed. Another card will not help; trying again may. |
| authentication_required | The card needs 3-D Secure and the attempt did not complete it. |
| authentication_failed | 3-D Secure was attempted and did not succeed. |
| blocked | Not the bank at all: one of your risk rules refused it (payment_blocked). |
A refused refund is 402 card_declined with decline_code: refund_failed — see refunds.
Not every attempt that fails is a decline. When the bank’s payment page expires, or the customer leaves it, before a card is tried, the payment goes back to requires_payment_method with a last_payment_error of type invalid_request_error and code payment_intent_payment_attempt_expired, and no decline_code. Nothing was charged and no card was refused. Do not tell the customer their card failed; they can simply try again.
Show the customer message rather than composing your own from the code. Some declines are deliberately vague — a card issuer will not always say why, and guessing on their behalf misleads the cardholder.
A payment the customer may be paying right now
Cancelling a payment asks the bank about it first. If the customer has already paid, the payment is settled and the cancel answers 400 — refund it instead. If they are completing 3-D Secure, or the bank’s payment page was opened less than 30 minutes ago, it answers 409 payment_in_progress and cancels nothing: the bank has no way to close a page it has handed out, so a cancel now could be followed by a payment. The message says when to try again. An unpaid payment is also cancelled automatically after a day, so you can simply leave it.
One distinction worth keeping
A 402 means the card was refused. A 503 gateway_unavailable means we could not reach the card network and no payment was taken. Telling a customer their card failed when it did not is both wrong and expensive, so we keep them apart and you should too.
Emails follow the same rule. 503 email_not_sent means we could not send it right now. Retry it. 400 email_recipient_rejected means the address was refused. Retrying will not help; send the customer the link another way. See invoicing.
And a third case that is not an error at all
Once a charge or a refund has been sent to the bank, we never answer with an error that a retry under a new key could act on — because the bank may have acted and lost only its reply, and a fresh request would charge or refund a second time. If its answer does not reach us, you usually get a success response describing where things stand: a payment intent with status: processing, a refund with status: pending, or a 202 from POST /v1/invoices/:id/pay. Each settles on its own within minutes, from the bank’s own records, and the webhook follows. Do not try again in the meantime.
When even that cannot be written, the answer is 500 outcome_unknown, and its message says which of three cases you are in:
| The message begins | What to do |
|---|---|
| This request reached the bank… Retrying with the same key returns this answer. | It is stored against your Idempotency-Key. Retry with the same key if you like — you get this answer back and the bank is not asked again. Never repeat it under a new key. |
| An earlier request with this Idempotency-Key reached the bank and has not recorded its result yet. | The first request is still running. Retry later with the same key: once it finishes you get its stored result. |
| This request reached the bank… Send an Idempotency-Key on requests that move money. | You sent no key, so there is nothing to replay. Do not repeat the request at all — a repeat could move money twice. |
In every case, look the payment intent or refund up rather than guessing: it settles on its own within minutes, from the bank’s own records, and its webhook follows. Retrying under a new key is the one way to pay or refund twice.
That is also why a 503 gateway_unavailable is safe to retry: it is only ever returned when nothing reached the bank.