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.

The same two surfaces also carry a decision's evidence trace, and there the split is sharper than a difference in location:

Decision evidence appears under two names by surface convention: the REST transaction carries trace (camelCase entries, durationMs); the authorization.decided webhook event carries decision_trace (snake_case, duration_ms). They are the same provenance — pin your parser to the surface you consume, not the other one's field name.

Card state

Facts about the card itself.

TokenMeaningTypical causeSuggested copy
card_not_foundNo card matches the identity presented. You never receive this value: a card we can't find belongs to no tenant, so no transaction and no authorization.decided event is created to carry it. It is listed only because the vocabulary is closed.A malformed, retired, or nonexistent card number reached the issuer.None needed — this value never reaches you.
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 decides its own authorizations, and we couldn't reach that decision at all.Either the delegated path isn't switched on for the programme yet; or its endpoint isn't fully provisioned — no signing secret, no stand-in policy, or no URL registered; or we refused to call the URL you registered. That last one is the cause to check first on a programme that is already live: we won't send an authorization to an address that resolves inside a private network, to a hostname whose DNS answer changes between our check and the call, or to a certificate that doesn't match the one pinned for you. The trace records delegated:blocked. Because a refused URL is a configuration fault rather than an outage, your stand-in policy is deliberately not consulted — we never approve a transaction we can't prove reached you."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."
delegated_declinedYour own authorization endpoint declined this transaction.You decide authorizations for this programme, and your endpoint answered "decline". Your own decline_reason is never put on the card network in your name, and it does not appear on the trace you can query: the published trace records delegated:decline and stops there. A reason that is a single token starting with a lowercase letter, then up to 47 more lowercase letters, digits or underscores (for example insufficient_funds) is retained on our internal record, where support can quote it back to you. Anything else — prose, punctuation, capitals, or a leading digit — is discarded rather than reshaped, and our internal record then reads delegated:decline, indistinguishable from a decline that carried no reason at all. Your decline still stands exactly as you sent it. Because we keep no marker for a discarded reason, support cannot confirm whether your endpoint populated the field: treat your own logs, not ours, as the place your decline reasons are readable."This payment was declined. Please contact your card issuer."
delegated_response_invalidYour endpoint answered something we couldn't relay to the acquirer, so we declined rather than guess.A full approve carrying an approved_amount that isn't an echo of the amount we asked about; a partial approval on a transaction whose terminal never offered partial approval, or on a channel other than card-present POS; a partial in a different currency from the transaction; or a partial that isn't below the amount requested, including one with more decimal places than the currency has. Note that a partial with a missing, zero or negative approved_amount is not in this set — that body fails validation outright, which we treat as no answer at all and hand to your stand-in policy, so it can end in an approval."We couldn't complete this payment. Please try again or use another card."
delegated_verification_unsupportedWe don't currently ask your endpoint about account verifications, so we decline them rather than guess your answer.An account verification — a zero-amount "is this card good?" check — on a programme that decides its own authorizations. The webhook payload we send you has no way to say "this is a verification, not a payment", and we won't send you a zero-amount authorization that looks like one: you'd be approving something with no amount and no movement behind it. Rather than answer on your behalf, we decline. Note this differs from programmes we authorize for, where a verification is approved on card validity alone. If you need verifications on a delegated programme, tell us — it needs a new field on the webhook payload and an agreement about what your approval would mean."We couldn't verify this card right now. Please try a different card."
delegated_stand_in_declinedYour endpoint couldn't be reached, so we stood in on your behalf — and the answer was a decline. Usually that means your stand-in policy refused it, though cause (7) below declines before the policy is consulted at all.Your endpoint timed out, refused the connection, or answered unusably, and then one of these happened: (1) the transaction exceeded the flat per-transaction cap in your stored stand-in policy, or hit an MCC it denies; (2) your programme's cumulative exposure budget for this outage is spent — we add up every stand-in approved while you're unreachable and stop at max_total_exposure, so a long outage declines everything once the budget runs out and keeps declining until your endpoint answers again, at which point the session closes and the sum resets — note that changing the policy's declared currency mid-outage also resets it, starting a fresh budget under the new currency; (3) no balance has ever been declared for this card — a stand-in cannot approve against a ceiling that was never set, so an undeclared card auto-declines rather than falling back to the flat programme cap alone; (4) a balance was declared, but it's below this transaction's amount — the common case once you're declaring balances day to day, and the one to check first if a card that usually approves suddenly doesn't; (5) the transaction's currency doesn't match the one your exposure budget is denominated in — we decline rather than convert; (6) the declared-balance store or the exposure counter itself couldn't be read, which fails closed rather than approving blind; or (7) the amount carries more decimal places than its currency has minor units (19.999 EUR, say) — a stand-in cannot express it on the wire, so it declines before your policy is consulted at all. Cause (7) is the one to suspect when every other explanation is demonstrably false: caps present and unspent, balance declared and sufficient, currencies matching, nothing unavailable. A policy missing either max_amount or max_total_exposure cannot stand in at all — both are optional on the wire, so a policy carrying only one stores successfully and then declines everything (incomplete_policy) during your first real outage; check both before looking anywhere else. See Stand-in and declared balances for how the three ceilings (per-transaction cap, outage exposure budget, and declared balance) interact — including who bears the loss when a stand-in approval turns out unfunded. The decision is flagged stip: true."This payment was declined. Please try again shortly."

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."
cvv2_mismatchThe card verification value the merchant collected doesn't match.A presented CVV2 failed verification."The security code (CVV) doesn't match. Please check the back of your card and try again."
cvv2_verify_errorWe couldn't check the card verification value — the check itself failed on our side, not the code.Our verification service was unreachable, too slow, or answered unusably. This is our fault, not the cardholder's, and the security code may well have been correct. Unlike cvv2_mismatch it is safe to retry: a later attempt will usually succeed once the check recovers."Something went wrong on our end. Please try again in a moment."
cavv_mismatchThe 3-D Secure authentication value (CAVV) presented with the authorization failed cryptographic verification.A presented CAVV didn't verify. Treated as potential replay or tampering, not a missing authentication — retrying without a fresh 3-D Secure authentication won't succeed."This payment couldn't be verified. Please try again with 3-D Secure."
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."

Fraud and risk

A risk verdict about this transaction rather than a rule it broke. Both tokens are deliberately unspecific about what was noticed — a decline that named the signal it tripped would tell someone testing cards exactly what to change.

TokenMeaningTypical causeSuggested copy
fraud_suspectedRisk scoring judged this transaction too likely to be fraudulent to approve.Several weak signals together, not one rule — so this says nothing specific about the card, the cardholder or the merchant, and a later transaction on the same card can well be approved."This purchase couldn't be approved. Please contact your card issuer if you'd like to discuss it."
cvv2_attempts_exceededToo many failed security-code (CVV2) attempts on this card recently, so the card is temporarily refused.Repeated wrong security codes — a mistyped card, or someone guessing. The pause covers every purchase on the card, not only the ones that ask for a security code (see below). It lifts by itself once the window passes; it is not a card status change, and nothing about the card needs to be reissued."For your security we've paused purchases on this card for a short while. Please try again later."

While that pause is in force, every transaction on the card gets cvv2_attempts_exceeded — including one presenting the correct security code. That is deliberate: an attempt that broke through the pause would tell a guesser they had found the right code.

These two are produced by the platform's risk engine, which scores the authorizations we decide once the card, control and velocity checks have passed. On a programme that decides its own authorizations (Gateway mode), it is skipped before it reads anything — the decision is yours, so scoring it would spend your deadline on a verdict nobody would consult — and neither token can reach you. They were listed here before the first decision could carry one — for the reason this page's opening paragraph gives: the vocabulary is closed at the wire, so a consumer matching it exhaustively is better served by a token it has an arm for and has not yet seen than by one that shows up undocumented.

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."
single_message_product_mismatchA single-message (0200) transaction arrived for a card whose programme isn't configured to receive single-message traffic.The wire says single-message, but the programme's own configuration says dual-message, or is unclassified. This is a routing/configuration anomaly worth an operator's attention on this specific product — not something the transaction itself did wrong."We couldn't complete this transaction. Please contact support."
single_message_not_recognizedA single-message transaction for a properly single-message-configured product wasn't recognized as one we can post directly.This build doesn't yet recognize the specific DE3/Business Application Identifier pair needed to confirm this is a real incoming credit we can post (for example, a Visa Direct Original Credit Transaction) — the recognized-pairs table is empty pending scheme certification data, so this is the expected outcome for single-message traffic today, not an anomaly worth investigating per instance."We couldn't complete this transaction. Please contact support."
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 is not forbid, so this platform declines rather than spending against it. The reason's name is misleading and kept only because it is published: such an account is balance-checked by the ledger — against its posted balance and its own credit limit — but that limit is not one the authorization path can see, so it refuses rather than approving against a ceiling it does not know.An internal provisioning/account-configuration state — never a cardholder condition."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."
scheme_credit_exhaustedThe account that receives incoming scheme credits (Visa Direct Original Credit Transactions and similar) has reached its configured ceiling.An operator-set risk limit, not a fault — raise the limit, or wait for the scheme's own settlement to retire outstanding exposure. The cardholder is being credited here, so nothing about their own balance is the cause."We couldn't complete this transfer right now. Please contact support."
scheme_credit_currency_not_configuredThe account that receives incoming scheme credits hasn't been set up for this transaction's currency.A provisioning gap: only a tenant's first-ever scheme-credit currency is set up automatically, so a credit arriving in a different currency has nowhere configured to land. Not a cardholder condition — the destination card account itself was already verified."We couldn't complete this transfer right now. Please contact support."
scheme_credit_posting_unconfirmedAn incoming scheme credit was submitted to the ledger and we could not confirm whether it posted.An internal fault on our side. Retrying the identical message will not resolve it — the recorded decision is replayed — so it needs a reconciler, not a retry."We couldn't confirm this transfer. 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."
incremental_original_unknownAn incremental authorization's original transaction couldn't be resolved.The original never existed, is older than we retain authorization records for, or belongs to a different cardholder — these are deliberately indistinguishable on the wire, so a cross-tenant lookup can't be used to confirm a transaction exists."This purchase couldn't be processed. Please try again or contact support."
incremental_original_not_amendableAn incremental authorization's original is real, but its increment couldn't be reserved against it.Two distinct causes share this token, and they are not distinguishable from the response alone: (1) the original's hold has already moved on — captured, released or expired — so the stay or rental this incremental extends has settled or lapsed; or (2) a technical fault on our side left the increment's outcome uncertain while the original hold itself is still live."This purchase couldn't be extended. Please contact support before retrying — a new authorization may create a duplicate hold on this card."
incremental_increment_withdrawnAn earlier attempt at this same incremental raised the hold and then correctly undid it, so there is nothing left to build on.A technical fault on our side interrupted the first attempt; the cleanup succeeded, which is why we can say this precisely rather than falling back to incremental_original_not_amendable. Nothing is reserved and nothing is stranded — but this exact message can never succeed, because its increment carries a one-time identifier we have already used and undone. Unlike its neighbour above, a new authorization is safe here and is the remedy."This purchase couldn't be extended. Please try the extension again."
incremental_amend_invalidThe ledger refused the increment amount itself (e.g. zero or negative).An internal fault on our side, not a cardholder condition."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.
stip_incremental_original_budget_exhaustedStood in because resolving an incremental authorization's original transaction ran out of time.
stip_replay_authority_unreadableStood in because we couldn't read whether this transaction had already been decided.
stip_idempotency_store_unavailableStood in because we couldn't check whether this request was already being processed.
stip_idempotency_key_malformedStood in because the request could not be matched to a valid account scope.
stip_velocity_rules_unreadableStood in because we couldn't read the card's spending-limit rules, so we couldn't check them. Not a limit breach.
stip_fraud_unavailableStood in because we couldn't read the programme's risk settings, so the transaction couldn't be scored. Not a risk verdict — see below.

Almost 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."

One exception, and it matters if you retry automatically. stip_idempotency_key_malformed is not transient: it means the request could not be tied to an account scope at all, so an identical retry fails identically, every time. Treat it as you would a 400 — the cardholder message above is still the right thing to show, but retrying the same request will not help, and a retry loop on it will never terminate. If you see it, the request is malformed on our side or yours; tell us.

stip_fraud_unavailable is not a risk verdict, and the distinction is worth holding on to. It means the programme's risk settings could not be read, so the transaction was never scored at all — nobody judged this cardholder. It is transient and the standard message above is the right one; in particular, do not show the fraud copy from the Risk section, which would tell a cardholder something about themselves that we did not conclude.

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 records — GET /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.