Skip to content
On this page

Developers › Building on the API

Errors

One shape, so you can branch on it.

Verified 6 November 2026Explain this to your team →

View as Markdown
json
{
  "error": {
    "type": "card_error",
    "code": "card_declined",
    "decline_code": "insufficient_funds",
    "message": "The card has insufficient funds.",
    "request_id": "req_8fK2mN4pQ6rSaB3c"
  }
}
A 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

TypeMeaning
invalid_request_errorThe request was wrong. Fix it; retrying will not help.
authentication_errorThe API key is missing, wrong, revoked or expired.
permission_errorValid key, but not allowed to do this — e.g. live before approval. insufficient_permissions names the missing permission in required_capability.
card_errorThe card was refused. decline_code says why.
idempotency_errorThe key was reused with a different body, or is still in flight.
rate_limit_errorToo many requests. Back off and retry.
api_errorOur 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

Statustype / codeWhen
400invalid_request_error / parameter_invalidA field is missing or wrong, or the body is not valid JSON. param names the field, when there is one.
401authentication_error / api_key_invalidNo key, or a wrong, revoked or wrong-mode one.
404invalid_request_error / resource_missingNo such object — usually a mistyped id, or an id from the other mode. The message names it.
404invalid_request_error / unknown_endpointNo such path, asked with a valid key.
402card_error / card_declinedThe card was refused. decline_code says why.
402card_error / payment_blockedA risk rule refused the payment before the bank was asked. decline_code is blocked.
400idempotency_error / idempotency_key_in_useThe Idempotency-Key was already used for a different request.
409idempotency_error / idempotency_in_progressThe first request with this key is still running. Retry the same request in a few seconds.
409invalid_request_error / payment_in_progressThe customer may be paying; see below.
400invalid_request_error / refund_not_yet_possibleA partial refund before the bank has settled the payment. Retry after settlement with a new Idempotency-Key — safe for this code only; see refunds.
429rate_limit_error / rate_limit_exceededMore than 100 requests a minute from one live key, or 60 from all of a business's test keys together, its Sandboxes included.
503api_error / gateway_unavailableThe bank could not be reached. Nothing was sent to it.
503api_error / email_not_sentWe could not send an email right now. Retry it.
503api_error / service_unavailableOur 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.
400invalid_request_error / email_recipient_rejectedThe address was refused. Retrying will not help; reach the customer another way.
500api_error / outcome_unknownThe 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.
json
{
  "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

ts
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_codeWhat it means
generic_declineRefused, and the issuer gave no reason we can pass on.
insufficient_fundsNot enough money on the card.
withdrawal_count_limit_exceededThe card has hit a limit on how much or how often it can be used.
expired_cardThe card has expired.
invalid_expiryThe expiry date entered is wrong.
incorrect_cvcThe security code is wrong.
incorrect_numberThe card number is wrong.
restricted_cardThe card cannot be used for this payment.
lost_card / stolen_cardReported lost or stolen. The customer is told only that it was declined.
transaction_not_allowedThe issuer does not allow this kind of payment on the card, online payments included.
call_issuerThe issuer wants the cardholder to contact them.
issuer_unavailableThe card issuer could not be reached. A retry shortly may succeed.
processing_errorNot the card: the payment could not be processed. Another card will not help; trying again may.
authentication_requiredThe card needs 3-D Secure and the attempt did not complete it.
authentication_failed3-D Secure was attempted and did not succeed.
blockedNot 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 beginsWhat 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.