Errors

One shape, so you can branch on it.
json
{
  "error": {
    "type": "card_error",
    "code": "card_declined",
    "decline_code": "insufficient_funds",
    "message": "The card has insufficient funds.",
    "request_id": "req_8fK2mN4pQ6rS"
  }
}

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.
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 was unreachable. Safe to retry.

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.

Decline codes

insufficient_funds, expired_card, incorrect_cvc, lost_card, do_not_honor and others. Anything we cannot map returns generic_decline.

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.

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.