Decline codes

When a card payment fails, the failed charge carries a decline code describing why. Frame normalizes the raw codes from card networks and issuing banks into a developer-friendly enum — so you don't have to learn dozens of issuer-specific strings to know whether to prompt the customer to retry or surface a generic error.

The decline code lives on the failure_code field of the underlying Charge record (which the transfer wraps). The parent Transfer carries a more general failure_reason string that points at the broader failure category. For programmatic handling of decline scenarios, read the Charge's failure_code and branch on the categorical reason.

Soft vs hard declines

The first cut on every decline code: was the failure transient (try again, maybe with a tweak) or permanent (this card isn't going to work)?

  • Soft decline — temporary, retryable. Insufficient funds, issuer reachability problems, requires 3D Secure authentication. The customer can take an action (top up the card, complete an authentication step) and a retry may succeed.
  • Hard decline — permanent, do not retry. Stolen card, fraud, expired card, invalid account. The customer needs to use a different payment method.

Frame's recommended response for each code is summarized in the table below, but the merchant judgement is: soft codes are worth one retry attempt with appropriate UX; hard codes should immediately route the customer to a different payment method.

Categories worth knowing

Behind the long enum is a smaller set of categories that drive the right merchant response:

CategoryBehaviorRepresentative codes
Authentication requiredSoft. Try again with 3DS challenge.authentication_required
Insufficient fundsSoft. Customer needs to top up or use different card.insufficient_funds, card_velocity_exceeded, withdrawal_count_limit_exceeded
Data errorSoft. Customer mis-entered card data; retry with corrected info.incorrect_number, incorrect_cvc, incorrect_zip, invalid_expiry_month, invalid_expiry_year, invalid_amount
Card-state errorHard. Expired, invalid, or restricted card.expired_card, invalid_account, restricted_card, card_not_supported, currency_not_supported, new_account_information_available
Fraud / risk signalHard. Don't disclose the reason to the customer — surface as generic_decline.fraudulent, lost_card, stolen_card, pickup_card, merchant_blacklist, security_violation
Issuer-side unknownSoft-ish. Issuer flagged the charge without telling Frame why; customer should contact their bank.call_issuer, do_not_honor, generic_decline, no_action_taken, try_again_later, issuer_not_available
Processing errorSoft. Transient platform-side failure; retry.processing_error, reenter_transaction
Test-modeN/A in production.testmode_decline

The merchant rule of thumb: anything in the "fraud / risk signal" row should be surfaced to the customer as a generic error. Telling the customer "your card was reported stolen" is bad for two reasons: it's customer-hostile (they may not know, and your platform isn't the right place to find out), and it tips off actual fraudsters about which signals tripped them.

Full code list

The complete enum, with Frame's recommended next step:

Decline codeDescriptionNext step
account_closedCustomer's bank account has been closedCustomer uses a different payment method
authentication_requiredTransaction requires 3DS authenticationWhen using frame-js, the auth flow typically triggers automatically — let the customer authenticate and retry
approve_with_idPayment can't be authorizedRetry once; if still failing, customer contacts issuer
call_issuerDeclined for an unknown reasonCustomer contacts their card issuer
card_not_enrolledCard failed 3DS authentication enrollmentCustomer uses a different payment method, or completes 3DS challenge if available
card_not_supportedCard doesn't support this purchase typeCustomer contacts issuer to verify capabilities
card_velocity_exceededCustomer exceeded a velocity limitCustomer contacts issuer
cardholder_name_mismatchCardholder name doesn't match the name on the accountRetry with corrected name
currency_not_supportedCard doesn't support the specified currencyCustomer should verify currency support with issuer
debit_not_authorizedCustomer notified their bank the payment was unauthorizedCustomer must use a different payment method; do not retry
do_not_honorDeclined; reason undisclosed by issuerCustomer contacts issuer
do_not_try_againDeclined; do not retryCustomer must use a different payment method
duplicate_transactionIdentical recent transaction detectedCheck for an existing recent payment before retrying
expired_cardCard has expiredCustomer uses a different card
fraudulentSuspected fraudSurface as generic_decline to customer; do not disclose
generic_declineDeclined for an unknown reasonCustomer contacts issuer
incorrect_numberCard number entered incorrectlyRetry with correct number
incorrect_cvcCVC entered incorrectlyRetry with correct CVC
incorrect_pinIncorrect PIN (card-reader payments only)Retry with correct PIN
incorrect_zipPostal code incorrectRetry with correct billing postal code
insufficient_fundsCard has insufficient fundsCustomer uses alternative payment method
invalid_accountCard or account invalidCustomer contacts issuer
invalid_amountAmount invalid or exceeds limitIf amount is correct, customer checks with issuer
invalid_cvcIncorrect CVCRetry with correct CVC
invalid_expiry_monthInvalid expiration monthRetry with correct expiration
invalid_expiry_yearInvalid expiration yearRetry with correct expiration
invalid_numberIncorrect card numberRetry with correct number
invalid_pinIncorrect PINRetry with correct PIN
issuer_not_availableCard issuer unreachableRetry payment; persistence means customer contacts issuer
lost_cardCard reported lostSurface as generic_decline
merchant_blacklistMatches a value on the platform's block listSurface as generic_decline
new_account_information_availableCard or account invalid; new info existsCustomer contacts issuer
no_accountBank account couldn't be locatedCustomer uses a different payment method
no_action_takenDeclined; reason undisclosedCustomer contacts issuer
not_permittedPayment not permittedCustomer contacts issuer
offline_pin_requiredCard requires PINCustomer retries with card inserted + PIN
online_or_offline_pin_requiredCard requires PINIf reader supports online PIN, prompt without new transaction; else retry with PIN
pickup_cardCard may be reported lost/stolenCustomer contacts issuer; surface as generic_decline
pin_try_exceededPIN attempts exceededCustomer uses different payment method
processing_errorError processing the cardRetry; if persistent, try again later
reenter_transactionIssuer couldn't process for unknown reasonRetry once; persistence means customer contacts issuer
restricted_cardCard may be reported lost/stolenCustomer contacts issuer; surface as generic_decline
revocation_of_all_authorizationsDeclined; reason undisclosedCustomer contacts issuer
revocation_of_authorizationDeclined; reason undisclosedCustomer contacts issuer
security_violationSecurity check failedCustomer contacts issuer
service_not_allowedService type not allowedCustomer contacts issuer
stolen_cardCard reported stolenSurface as generic_decline
stop_payment_orderStop-payment order on the accountCustomer contacts issuer
testmode_declineTest card used outside test modeUse a real card
transaction_not_allowedTransaction type not allowedCustomer contacts issuer
try_again_laterDeclined for an unknown reason; retry possibleRetry; persistence means customer contacts issuer
unsupported_card_networkCard network not supported by the merchant's processorCustomer uses a different card
withdrawal_count_limit_exceededBalance or credit limit exceededCustomer uses alternative payment method

Customer-facing display

When a decline involves fraud or card-loss signals (fraudulent, lost_card, stolen_card, pickup_card, merchant_blacklist, restricted_card), always show the customer a generic error message like "We couldn't process your card. Please try a different payment method." Don't expose the underlying decline code or its specific reason. Two reasons:

  1. Customer experience — telling a customer their card is reported stolen via your checkout flow is harsh and likely wrong context (they may not know yet, or the report may be erroneous).
  2. Fraud prevention — fraudsters use decline reasons to learn which signals to evade. Generic errors are slightly more friction for them.

For all other decline codes, the description is safe to surface directly — "insufficient funds," "invalid CVC," "card expired" — these help the customer understand what to fix.

Reading the code

Decline codes appear on the underlying Charge record (which the failed transfer wraps):

{
  "id": "charge_...",
  "status": "failed",
  "failure_code": "insufficient_funds",
  "failure_message": "Your card has insufficient funds."
}

The parent Transfer surfaces a coarser failure_reason string for general categorization; the Charge's failure_code is the specific normalized decline.

Listen for transfer.failed and charge.failed webhook events to react to declines in real time. The payload references both the transfer and the parent charge so you can pull failure_code from the latter without an extra round-trip.

The full enum lives in code at app/models/concerns/charge/failure_messages.rb — it carries the canonical list (currently ~53 entries; the set evolves as Frame's processors normalize new issuer behavior). The table above covers the most commonly encountered codes; consult the openapi spec or the Charge schema for the current complete list.

Gotchas

Symptom: you keep retrying a charge after do_not_honor and the customer is getting frustrated. Why: do_not_honor is the issuer's way of saying "we're not telling you why, but stop." Persistent retries against a do_not_honor decline can land the merchant on the issuer's velocity-limit watchlist or trigger a fraud flag. Fix: one retry is reasonable; multiple retries on the same card with the same code is not. Route the customer to an alternative payment method.

Symptom: the decline code is authentication_required but you don't have 3DS configured. Why: some issuers require 3D Secure for higher-risk transactions and will decline non-3DS attempts. Frame.js handles this automatically when you're using its Card element — without frame-js, you'd need to integrate 3DS yourself. Fix: use frame-js for card collection, or accept that some transactions will fail without 3DS configured. See 3D Secure.

Symptom: sandbox transactions return testmode_decline. Why: you're using a sandbox key against a non-test card number, or a production key against a test card. Fix: match the key environment to the card. Sandbox key + test card numbers from Testing; production key + real cards.

Reference

For charge-flow troubleshooting, see POST/v1/transfers and Accept a payment for the integration-side patterns.