Skip to content

Decline reasons

The closed decline_reason vocabulary and safe copy to show customers.

decline_reason is a closed, finite vocabulary — every value it can ever carry is listed on this page. Match on it exhaustively (with a fallback for redacted) instead of pattern-matching or substring-checking the string: the set only grows by an explicit change to this catalog, never silently.

Where it appears

decline_reason rides two places, both only ever set on a decline:

It's absent on an approval. On a stood-in decline (stip: true — the authorizer couldn't reach a normal decision in time and fail-closed rather than risk an unauthorized approval) it's one of the stand-in tokens below.

Because the set is closed at the wire, not just documented, a decision that carries a reason this catalog hasn't caught up with does not reach you labelled with something unlisted — it's held back for review instead. You will only ever see one of the tokens below.

One field named decline_reason is not part of this vocabulary: the one on a sandbox simulation record. It carries raw network wire codes instead — see Sandbox wire vocabulary before you assert on it.

Card state

Facts about the card itself.

TokenMeaningTypical causeSuggested copy
card_not_foundNo card matches the identity presented.A malformed, retired, or nonexistent card number reached the issuer."This card can't be used right now. Please check the card details and try again."
card_inactiveThe card exists but hasn't been activated.The cardholder hasn't completed activation yet."This card hasn't been activated yet. Activate it in the app, then try again."
card_restrictedThe card is administratively suspended.A programme admin or a fraud control suspended the card."This card is temporarily restricted. Contact support for help."
card_closedThe card has been permanently closed.The cardholder or a programme admin closed the card."This card has been closed and can no longer be used."
card_lostThe card was reported lost.The cardholder reported the card lost."This card has been reported lost. Order a replacement in the app."
card_stolenThe card was reported stolen.The cardholder reported the card stolen."This card has been reported stolen. Order a replacement in the app."
card_expiry_mismatchThe expiry submitted with the transaction doesn't match our record.A mistyped expiry, or a cloned/stale card number."We couldn't verify this card's details. Please check the card and try again."
card_expiry_unknownWe have no expiry on record to check against.A data gap on our side, never something a cardholder can trigger."We couldn't verify this card's details. Please try again or contact support."
card_expiredThe card is past its expiry date.The card genuinely expired."This card has expired. Order a replacement in the app."
card_currency_unknownWe couldn't establish which currency the card is denominated in, so we declined rather than assume one.A data gap on our side, never something a cardholder can trigger."We're having trouble processing this right now. Please try again in a few minutes."
programme_config_staleThe card's programme configuration wasn't fresh enough to authorize safely, so we declined rather than guess.An internal configuration-propagation delay."We're having trouble processing this right now. Please try again in a few minutes."
delegated_auth_unavailableThis programme delegates authorization decisions elsewhere, and that path isn't live yet.The programme is configured for delegated authorization ahead of that capability shipping."This card doesn't support this type of transaction yet."
programme_ledger_disabledThe programme's ledger module is switched off, so its cards have no funding account to authorize against.A programme configured for managed authorization with the ledger module off — cards are only given a funding account when it is on."This card isn't set up for payments yet. Please contact support."

card_expiry_mismatch, card_expiry_unknown and card_expired deliberately share one token family and answer the same way on the network — telling them apart on the wire would let a stolen card number be probed for its real expiry a few dozen guesses at a time. Don't build UI that implies they're distinguishable; the copy above works for all three.

Controls

A programme-level rule matched, or a required allow-list rule didn't.

TokenMeaningTypical causeSuggested copy
mcc_deniedThe merchant category is explicitly blocked.A merchant-category deny rule matched."This type of purchase isn't allowed on this card."
mcc_not_allowedThe merchant category isn't on the required allow-list.An allow-list control didn't include this category."This type of purchase isn't allowed on this card."
country_deniedThe transaction's country is explicitly blocked.A country deny rule matched."Purchases from this location aren't allowed on this card."
country_not_allowedThe transaction's country isn't on the required allow-list.An allow-list control didn't include this country."Purchases from this location aren't allowed on this card."
channel_deniedThis transaction channel (POS, ecommerce, ATM, MOTO) is explicitly blocked.A channel deny rule matched."This type of transaction isn't allowed on this card."
channel_not_allowedThis channel isn't on the required allow-list.An allow-list control didn't include this channel."This type of transaction isn't allowed on this card."
entry_mode_deniedThe way the card was presented (e.g. a contactless tap) is blocked.A contactless/entry-mode deny rule matched."Tap to pay isn't enabled on this card. Try inserting or swiping instead."
amount_over_capThe amount is over this card's per-transaction cap.A spend-limit control."This purchase is over your card's spending limit."
control_currency_mismatchRetired — no longer sent. A spend-limit control was set in a currency the transaction wasn't in, so it couldn't be evaluated safely.Was a configuration issue on the programme's controls. Spend limits are now a plain number in the card's own currency, so this can no longer happen."We're having trouble processing this purchase. Please try again or contact support."

control_currency_mismatch is retired but not removed. No new decline will carry it: a spend limit is now a plain number denominated by the card, so a limit and a card can no longer disagree about currency.

It stays listed because this vocabulary only ever grows — a value that disappeared would break any consumer matching it exhaustively, and decisions already recorded with it keep their meaning. Leave your existing branch in place: it is no longer reached for new declines, but a decision recorded before this change still carries it, so anything replaying or reporting over history will keep meeting it.

Velocity

The card hit a rolling window limit.

TokenMeaningTypical causeSuggested copy
velocity_count_exceededToo many transactions in the tracked window.The card's transaction-count limit was reached."You've made too many purchases in a short time. Please try again later."
velocity_amount_exceededToo much spent in the tracked window.The card's spend-amount limit was reached."You've reached your spending limit for this period."

Funding

The ledger side of the decision — whether the money is there and postable.

cross_currency is the one token listed here that can also be answered earlier, straight from the card record and before any ledger call. The meaning, the network response code and the copy are identical either way, so nothing consuming it needs to tell the two apart.

TokenMeaningTypical causeSuggested copy
insufficient_fundsNot enough available balance for the amount.A genuine shortfall on the funding account."Insufficient funds. Add funds to your account and try again."
cross_currencyThe transaction currency doesn't match the currency the card is denominated in.A foreign-currency purchase on a single-currency card."This card can't be used for purchases in this currency."
refund_not_supportedRefunds aren't processed on this rail yet.A merchant attempted to credit the cardholder."Refunds aren't currently supported for this card."
balance_inquiry_not_supportedBalance-inquiry transactions aren't processed.An ATM or terminal balance check was attempted."Balance inquiries aren't supported for this card."
authorization_conflictThe same transaction identity was submitted twice with different content.A retried request that changed something about the original."Something went wrong processing this purchase. Please try again."
funding_account_missingThis card has no funding account configured.A provisioning gap."This card isn't ready to use yet. Please try again shortly or contact support."
funding_account_not_balance_checkedThe funding account's overdraft posture means it isn't balance-checked.An internal provisioning/account-configuration state."We're having trouble processing this purchase. Please try again or contact support."
funding_account_limit_unknownThe account is balance-checked but its overdraft ceiling is unbounded or couldn't be established.An internal provisioning or data gap — never a cardholder condition."We're having trouble processing this purchase. Please try again or contact support."
funding_account_not_foundThe funding account record referenced by the card doesn't exist.An internal data inconsistency."We're having trouble processing this purchase. Please try again or contact support."
verification_amount_mismatchA zero-amount account-verification request's flag didn't match its amount.A producer/request inconsistency, not reachable from normal card use."We're having trouble processing this purchase. Please try again or contact support."
zero_amount_not_declaredA zero-amount request wasn't flagged as a verification.Same as above, the other direction."We're having trouble processing this purchase. Please try again or contact support."
amount_precision_unsupportedThe amount carries more decimal places than the currency has minor units.A caller sent a sub-cent amount — e.g. 10.005 in EUR. Terminals cannot produce this; it reaches us from an API caller, the simulator or the console."We couldn't process that amount. Please try again with a whole number of cents."
refund_float_not_configuredThe programme has no account configured to draw refund credits from.A provisioning gap: refunds were attempted on a programme whose refund float was never set. Not a cardholder condition."We couldn't process this refund. Please contact support."
refund_float_exhaustedThe programme's refund float has no funds left to credit from.The float needs topping up — every refund on the programme declines until it is. The cardholder is being credited here, so nothing about their balance is the cause."We couldn't process this refund right now. Please contact support."
refund_posting_unconfirmedA refund credit was submitted to the ledger and we could not confirm whether it posted.An internal fault on our side. Retrying the same message will not resolve it — the recorded decision is replayed — so it needs a reconciler, not a retry."We couldn't confirm this refund. Please contact support before trying again."
funding_request_invalidThe ledger rejected the hold request for an unclassified reason.An internal inconsistency between the authorizer and the ledger."Something went wrong processing this purchase. Please try again."
hold_state_unverifiableA replayed transaction's hold state couldn't be confirmed.An internal inconsistency during a retried/replayed decision."Something went wrong processing this purchase. Please try again."
hold_amount_mismatchA replayed transaction's hold was sized differently than expected.An internal inconsistency during a retried/replayed decision."Something went wrong processing this purchase. Please try again."
hold_not_activeA hold this transaction needed is no longer active.An internal inconsistency during a retried/replayed decision."Something went wrong processing this purchase. Please try again."
sandbox_control_live_modeA test-mode-only control was invoked outside test mode.Test configuration reached a live-mode request — not reachable in production use."Something went wrong processing this purchase. Please try again or contact support."
sandbox_partial_invalidA malformed sandbox partial-approval amount.Test-only tooling misuse — not reachable in production use."Something went wrong processing this purchase. Please try again or contact support."

Engine

Not a fact about the card, the cardholder, the funds, or a stand-in — the decision pipeline itself misbehaved.

TokenMeaningTypical causeSuggested copy
pipeline_incompleteThe decision pipeline reached the end of its stage list without any stage settling a verdict.A wiring bug in the authorizer, not something a request can trigger deliberately."Something went wrong processing this purchase. Please try again or contact support."

Stand-in (stip_*)

When the authorizer can't reach a normal decision before its deadline — a dependency is down, the request budget ran out, or its own configuration isn't warm — it fails closed with stip: true rather than risk an unauthorized approval. decline_reason on a stand-in decline is always prefixed stip_, and every value below is one this build emits today:

TokenMeaning
stip_config_coldStood in because the programme's configuration wasn't ready.
stip_idempotency_in_flightStood in because an identical request was already being processed concurrently.
stip_ledger_unavailableStood in because the ledger couldn't be reached.
stip_controls_timeoutStood in because the controls check ran out of time.
stip_dependency_unavailableStood in because a required dependency was unreachable.
stip_deadline_in_pastStood in because the request had already missed its processing deadline on arrival.
stip_deadline_exceededStood in because processing ran past its allotted time budget.
stip_card_unresolvedStood in because the card couldn't be resolved before the deadline.

Every stand-in reason answers the same for a consumer — it's a transient, our-side failure, not a fact about the card, the cardholder, or the funds:

"We couldn't process this purchase right now. Please try again in a moment."

redacted

A reason existed on the decision, but its detail wasn't safe to publish verbatim — or, on a build ahead of this catalog, wasn't in this vocabulary at all. Treat it exactly like any other decline with no more specific information available:

"We couldn't complete this purchase. Please try again or contact support."

Building consumer copy

Match on decline_reason exhaustively, with redacted — and the case where decline_reason is absent on a declined transaction — folding into the same generic fallback. That combination is what the closed vocabulary buys you over substring-matching the field: every branch you write is a real, listed token, and anything the switch doesn't recognise is provably out of date rather than a token you can't reproduce.

const COPY: Partial<Record<string, string>> = {
  insufficient_funds: "Insufficient funds. Add funds to your account and try again.",
  card_expired: "This card has expired. Order a replacement in the app.",
  amount_over_cap: "This purchase is over your card's spending limit.",
  // ...the rest of the table above.
};

const FALLBACK = "We couldn't complete this purchase. Please try again or contact support.";

function copyForDecline(declineReason: string | undefined): string {
  if (declineReason === undefined) return FALLBACK;
  return COPY[declineReason] ?? FALLBACK;
}

Don't infer meaning from a decline reason's shape (a _ count, a prefix other than stip_, or its length) — new tokens are added to categories over time, and a shape-based guess breaks silently the day that happens. Read the full event catalog for how decline_reason sits inside the rest of the authorization.decided payload.

Sandbox wire vocabulary

There are two decline surfaces, and they speak different languages.

Transactions and webhooks — everything above — publish the closed vocabulary: a token that names the decision (mcc_denied, entry_mode_denied, card_lost). It is a contract. It is finite, it is reviewed, and a reason outside it never reaches you.

Simulation recordsGET /v1/simulated-spends/{id} — publish something else entirely: the raw network wire code, translated only as far as its standard ISO 8583 meaning. The sandbox simulator sits on the far side of the wire from the authorization engine and deliberately never learns why a decision was made; all it can see is the DE39 response code that came back, so all it reports is what that code means (do_not_honor, lost_card, stolen_card, insufficient_funds, not_permitted_to_cardholder, and so on — an unrecognised code is passed through as declined_<code>). That set is open-ended by design: it tracks the network, not our catalog.

Two consequences worth internalising before you write an assertion against a simulation record:

The mapping is many-to-one. A single wire code carries several distinct published reasons. mcc_denied and entry_mode_denied both leave the engine as DE39 57, so the simulation record for either one reads not_permitted_to_cardholder. You cannot recover which control actually fired from the record's decline_reason — read the transaction or the authorization.decided event for that.

The spellings near-miss. The wire token for a lost card is lost_card. The published token is card_lost. Same two words, opposite order, different surfaces — and a switch written against one vocabulary will fall straight through to its default on the other while looking perfectly correct in review. This is the single most likely way to get this wrong.

So: to assert on the decision in an integration test or a verification runbook, read the transaction or the authorization.decided event and match against the vocabulary on this page. Read a simulation record's decline_reason only for what it is — evidence of what came back over the simulated wire — and never match it against the tokens above.