Skip to main content

Decoding Stripe's last_payment_error for Subscription Recovery August 2026

16 min read
Decoding Stripe's last_payment_error for Subscription Recovery August 2026

When a subscription payment fails, Stripe gives you a structured error object with enough detail to make a confident recovery decision, but only if you know which fields to read and in what order. The decline_code alone separates a soft decline you can retry tomorrow from a hard decline that will never authorize again. Here's how to read the whole picture.

TLDR:

  • decline_code inside last_payment_error separates retryable soft declines from permanent hard declines; without it, card_declined tells you nothing actionable.
  • Retrying a hard decline (stolen_card, lost_card) suppresses your authorization rates over time and triggers Mastercard fees up to $0.50 per violation.
  • Ambiguous codes like do_not_honor require the Charge object's outcome.network_advice_code field before you can make a confident retry decision.
  • Stripe clears last_payment_error on any PaymentIntent update, so it cannot serve as a full audit trail across a multi-week retry cycle.
  • Slicker extends last_payment_error into a recovery engine, weighing over 40 variables per transaction, with performance measured via AABB (A/B/B testing) on your own data before any payment is owed.

What last_payment_error Is and When It Gets Set

last_payment_error is a nested object on a Stripe PaymentIntent that records the most recent failure when a confirmation attempt does not succeed. When a charge is declined, the field populates automatically with structured data about why the attempt failed.

Two primary moments expose it: the payment_intent.payment_failed webhook event, and the PaymentIntent object returned directly after a failed confirm call via the API. In both cases, the field holds the same structured error object.

One behavior worth noting early: Stripe clears last_payment_error on any subsequent update to the PaymentIntent. If you update the payment method or amount before reading the error, the failure context is gone.

The Anatomy of last_payment_error: Subfields Every Engineer Should Know

The object surfaces six primary fields, nested as follows:

  • type: the top-level error category. Common values are card_error, invalid_request_error, api_error, and idempotency_error. Most subscription declines land under card_error.
  • code: a machine-readable identifier within that type, such as card_declined. This is your first branch point in retry logic.
  • decline_code: the issuer-specific sub-reason beneath card_declined, for example insufficient_funds or do_not_honor. Only present when code is card_declined.
  • message: a human-readable description intended for logs, not customer-facing UI.
  • param: populated only on invalid_request_error types, naming the offending request field.
  • payment_method: the full payment method object as it existed at the moment of failure, including card brand, funding type, and last four digits.

Both access paths return the same structure: from a webhook, read data.object.last_payment_error on the payment_intent.payment_failed event; from a direct API call, the field sits at the top level of the returned PaymentIntent after a failed confirm.

decline_code does the heaviest lifting for recovery decisions. Without it, card_declined tells you almost nothing actionable. It is what separates a retryable failure from a permanent one.

Stripe Error Types and What Each Means for Recovery

The type field is your first routing decision. Before reading decline_code, you need to know whether the failure is customer-side, code-side, or Stripe-side, because each requires a completely different response.

  • card_error: what almost every subscription decline produces. Despite the name, Stripe uses this type for non-card payment methods too, a historical quirk worth knowing so you don't build card-only handling logic around it. Inspect decline_code next.
  • invalid_request_error: a malformed or misconfigured API call. The customer's card is not the problem; your request is. Check param to find the offending field and fix the integration before retrying anything.
  • api_error: Stripe could not process the request on their end. Log it, wait briefly, and retry once. These are rare and typically transient.
  • idempotency_error: a conflicting request was made with the same idempotency key. A code-level issue, not a payment failure.

For subscription recovery, card_error is where nearly all your logic lives. The other three types should route to alerting or integration fixes, not customer-facing retry flows.

Soft Declines vs. Hard Declines: The Classification That Decides Your Retry Path

The divide between soft and hard declines is the most consequential classification in subscription payment recovery. Get it wrong and you either leave recoverable revenue on the table or burn retry attempts on cards that will never authorize again.

Soft declines are temporary. The card is valid; the transaction failed for a situational reason. Common decline_code values in this category include insufficient_funds, processing_error, and try_again_later. These are retryable, though timing matters.

Hard declines are permanent. The issuer is telling you the card itself cannot be charged. stolen_card, lost_card, expired_card, and card_not_supported fall here. No retry will succeed; the customer must act before any payment can proceed.

Retrying a hard decline is not neutral. Repeated attempts on a stolen or closed card signal poor transaction hygiene to issuers, which can suppress authorization rates on your legitimate transactions over time. The damage is silent and cumulative.

The Ambiguous Middle: do_not_honor

The tricky cases are codes like do_not_honor, which Stripe surfaces as a catch-all when the issuer declines without a specific reason. This code appears in both retryable and non-retryable scenarios depending on what the issuer actually meant. Classifying it correctly requires additional signal layers: the network decline code, any Merchant Advice Code (MAC) present, the card's funding type, and prior retry history on that instrument. do_not_honor alone is not enough information to make a confident retry decision.

Stripe Decline Codes vs. Network Decline Codes: Understanding the Two Layers

Stripe's decline_code values are Stripe's own normalized taxonomy sitting on top of raw issuer messaging. When a card network returns ISO 8583 code 51, Stripe translates that into insufficient_funds. The mapping makes decline codes more readable, but it abstracts away information that can matter for recovery decisions.

The raw network signal lives one level deeper, on the Charge object's outcome field. Within that field, outcome.network_decline_code carries the raw two-digit code the issuer returned, and outcome.network_advice_code carries any Merchant Advice Code (MAC) the network attached to that response.

The same Stripe decline_code can map to different network codes with different recovery implications. A do_not_honor might carry network code 05 alongside MAC 26 (retry in two days) for one transaction, and MAC 03 (do not retry) for another. The Stripe-level code looks identical; the correct recovery action is completely different.

Querying only last_payment_error means working with a summary. For ambiguous codes like do_not_honor, the network-level context on the Charge object is what turns an unclear code into an actionable decision.

Merchant Advice Codes and the network_advice_code Signal

Merchant Advice Codes (MACs) tell you what to do after a decline, going beyond the reason it failed. They surface on the Charge object at outcome.network_advice_code, one level below what last_payment_error exposes.

Key codes for subscription engineers:

MAC

Issuer Instruction

Action for Subscription Billing

01

Update account information

Do not retry until the customer provides new card details; the issuer requires updated data first

02

Retry later

Retry is permitted; no fixed window is specified by Mastercard, so use payday-cycle timing as the guide

03

Do not retry

Stop all retry attempts immediately; Mastercard charges $0.10 per attempt if you ignore this code

21

Stop recurring charges

Cancel the recurring billing arrangement on this account entirely

24

Retry after 1 hour

Schedule next attempt no sooner than 1 hour after decline

25

Retry after 24 hours

Schedule next attempt no sooner than 24 hours after decline

26

Retry after 2 days

Schedule next attempt no sooner than 2 days after decline

27

Retry after 4 days

Schedule next attempt no sooner than 4 days after decline

28

Retry after 6 days

Schedule next attempt no sooner than 6 days after decline

29

Retry after 8 days

Schedule next attempt no sooner than 8 days after decline

30

Retry after 10 days

Schedule next attempt no sooner than 10 days after decline; if this exceeds your dunning window, attempt once before it closes

Visa takes a different approach, grouping declines into four retry-eligibility categories instead of numbered codes.

Following MACs blindly creates its own failure mode. If MAC 30 prescribes a 10-day wait but your dunning window closes in five days, compliance means missing your last recovery window entirely.

Card Network Retry Limits and the Penalty Fees Attached to Violations

Retry limits are hard rules, not guidelines. Visa's 15-retry cap per card applies within any 30-day window; Mastercard allows 10 within any 24-hour window on soft declines. Stripe enforces the Visa cap directly, blocking further attempts once the threshold is hit.

The penalties are material. Mastercard charges a $0.50 TPE fee per violation (per Congrify). Visa charges $0.10 per excessive retry on domestic transactions and $0.15 cross-border. Fees accrue on every excess attempt regardless of whether the retry succeeds, so a merchant pays the penalty on recovered revenue too.

A retry strategy that queues attempts without reading decline_code will trigger these fees at volume. Hard declines, ambiguous codes needing Merchant Advice Code context, and exhausted soft declines all look identical to a calendar-based scheduler. The penalties are the financial consequence of treating last_payment_error as optional reading.

Reading last_payment_error in Subscription Webhook Workflows

In a Stripe Billing subscription workflow, two webhook events are relevant: payment_intent.payment_failed and invoice.payment_failed. For subscription engineers, invoice.payment_failed is the more useful entry point. It fires when Stripe's billing cycle fails to collect payment, carrying the invoice object directly. From there, retrieve the associated PaymentIntent ID via invoice.payment_intent and fetch the PaymentIntent to access last_payment_error.

The branching logic follows a consistent pattern:

  1. Read last_payment_error.type first. If it's anything other than card_error, route to alerting, not retry logic.
  2. Check last_payment_error.code. If card_declined, proceed to decline_code.
  3. Branch on decline_code: soft declines schedule a retry; hard declines halt automation and trigger failure reason dunning cadence when customer action can resolve the issue; ambiguous codes like do_not_honor require the Charge object's outcome fields before deciding.

One pitfall catches engineers early: last_payment_error reflects only the most recent attempt. If Stripe has already retried the invoice before your webhook fires, you're seeing the latest failure, not the full history. To reconstruct the complete sequence, query the Charge list on the PaymentIntent. Each Charge carries its own outcome object, giving you every decline code and network response across the retry cycle.

Decline-Code-Aware Retry Scheduling for Subscription Payments

Reading decline_code without acting on it is the most common retry mistake in subscription billing. The code tells you whether to retry, when, and how many times before handing off to dunning.

The first gate is whether to retry at all. Hard decline codes (stolen_card, lost_card, card_not_supported) stop the sequence immediately. Further automation is wasted and potentially penalized.

For retryable soft declines, timing is the primary lever. Insufficient funds accounts for roughly 40.5% of subscription payment failures, making payday-cycle alignment especially high value:

  • US (biweekly payroll): retry 2 to 3 days after the 1st or 15th, or the Friday following a pay period
  • Western Europe and UK (monthly payroll): retry within 48 hours of the last working day of the month
  • Australia (weekly or fortnightly): a 3 to 5 day retry interval catches most pay cycles

Where Merchant Advice Codes (MACs) are present, use them as a floor. MAC 24 through 30 provide explicit time windows (1 hour through 10 days); schedule your next attempt at the prescribed interval. If the MAC window exceeds your remaining dunning period, attempt once before it closes instead of waiting it out and losing the subscriber entirely.

Two to three retry attempts on a soft decline is a reasonable threshold before triggering customer-facing dunning. Beyond that, continued automation without customer action produces diminishing returns and erodes your retry budget against network caps.

Where last_payment_error Hits Its Limits for Subscription Recovery

last_payment_error gives you the gateway-level code Stripe received, not the issuer's actual reason. Most issuers categorize a wide range of declines as generic do_not_honor in place of returning a specific code, so the field reflects what Stripe was told, which is often a summary.

Identical decline_code values also carry different soft decline retryability profiles depending on card funding type, issuing bank, and geography. A do_not_honor on a US consumer debit card from a regional bank behaves differently than the same code on a European corporate card. A static code-to-action map treats both identically, which is an approximation at best.

The third gap is architectural. Stripe clears last_payment_error on any PaymentIntent update, so across a multi-week retry cycle involving payment method changes or amount adjustments, the field is not a reliable audit trail. Reconstructing the full decline history requires querying every Charge on the PaymentIntent separately and logging each outcome as it fires.

The field is the right starting point. It just cannot carry the full weight of a production recovery strategy on its own.

How Slicker Extends last_payment_error Into a Recovery Engine

last_payment_error is where Slicker starts, not where it stops. The field feeds into a broader signal set that includes raw network decline codes, Merchant Advice Codes, BIN data, card funding type, issuing bank behavior, and geographic payday patterns. Slicker's ensemble of AI models weighs over 40 variables per transaction before making a smart retry decision.

That depth directly resolves the limits generic codes create. Ambiguous codes like do_not_honor become classifiable when the AI can cross-reference network advice codes, funding type, and historical outcomes on similar instruments from the same issuer. Hard declines stop immediately when network codes confirm no recovery path, keeping merchants inside Visa and Mastercard retry limits. For soft declines, timing is calculated at the hour level, accounting for payday cycles by geography and issuer-specific authorization windows.

Where Stripe retries on the originally charged instrument by default, Slicker can route to alternate cards on file when the primary method fails, expanding recovery without adding retry volume to a single card.

Setup requires no engineering work. Slicker connects to an existing Stripe Billing integration in under five minutes via API keys, and recovery flows through Stripe's own infrastructure unchanged. Performance is measured through AABB testing against each merchant's own transaction data, with statistical significance confirmed before any payment is owed. The dashboard separates Slicker-recovered revenue from the merchant's own retry recoveries, giving finance teams clean attribution: not aggregated totals that obscure what actually drove the result.

Final Thoughts on Stripe's last_payment_error and What It Takes to Act on It

Reading last_payment_error correctly is a meaningful step forward, but subscription recovery at scale requires more signal than any single field carries. Hard declines need to stop your retry sequence immediately, soft declines need timing logic tied to real payday patterns, and ambiguous codes need network-level context before you commit to a path. The gap between reading a decline code and acting on it well is where most subscription revenue gets left behind. Get in touch with the Slicker team to see what a signal-complete recovery setup looks like for your billing workflow.

FAQs

What is the difference between soft declines and hard declines in recurring billing?

Soft declines are temporary failures where the card is valid but the transaction failed for a situational reason, such as insufficient_funds or processing_error, making them retryable with correct timing. Hard declines signal that the card itself cannot be charged, covering codes like stolen_card, lost_card, and card_not_supported, where no retry will succeed and the subscriber must take action before any payment can proceed. Misclassifying a hard decline as retryable is not neutral: repeated attempts on a closed or stolen card erode your authorization rates over time and can trigger card network penalties.

How do merchant advice codes affect when and whether to retry a failed subscription payment?

Merchant Advice Codes (MACs) sit one level below what last_payment_error exposes, on the Charge object at outcome.network_advice_code, and tell you what action to take after a decline, going beyond simply why it failed. MAC 03 instructs you not to retry at all, with Mastercard charging $0.10 per attempt if you ignore it; MACs 24 through 30 prescribe timed retry windows from 1 hour to 10 days. The critical constraint: if a MAC prescribes a 10-day wait but your dunning window closes in five days, following the MAC blindly means losing the subscriber entirely, so the code should function as a floor, not a ceiling.

How do I avoid Visa and Mastercard retry fees when retrying failed subscription payments?

Visa caps retry attempts at 15 per card within 30 days; Mastercard allows 10 within any 24-hour window on soft declines, with Mastercard's Transaction Processing Excellence fee reaching $0.50 per violation per current Mastercard network rules. The fees accrue on every excess attempt regardless of whether the retry succeeds, so a calendar-based scheduler that queues attempts without reading decline_code will trigger penalties even on recovered revenue. Reading last_payment_error before each retry, stopping immediately on hard declines, and respecting MAC guidance keeps attempts within network limits and protects merchant account standing.

What does an AI-powered payment recovery platform do differently from the built-in retry logic in Stripe or Chargebee?

Stripe's Smart Retries and billing-platform built-ins apply rule-based schedules that treat all soft declines similarly, retry on the originally charged instrument, and send dunning emails tied 1:1 to retry attempts. Slicker's ensemble of AI models weighs over 40 variables per transaction, including card funding type, issuing bank behavior, geographic payday patterns, and raw network advice codes, to calculate retry timing at the hour level, not the calendar day. Where Stripe does not natively switch between multiple cards on file during a retry cycle, Slicker can route to alternate payment methods when the primary instrument fails, adding recovery opportunities without adding retry volume to a single card.

How does Slicker integrate with Stripe Billing technically, and what happens to Stripe as the system of record?

Slicker connects to an existing Stripe Billing setup in under five minutes using API keys, and all recovered payments flow back through Stripe's own infrastructure unchanged, so finance and reconciliation workflows see no difference from regular payments. One implementation requirement applies: Stripe's default subscription cancellation grace period must be extended to align with Slicker's multi-week retry windows, or Stripe will close subscriptions before Slicker can complete its retry sequence. Note that Slicker does not currently write an audit trail of its actions, such as retry attempts or billing cycle resets, back into Stripe as native records, so teams using Stripe as their canonical system of record should account for this gap when building internal reporting.

Stop losing revenue to failed payments

Join leading subscription businesses using Slicker to recover failed payments automatically.

Get Started

Cookie preferences

Your privacy matters

We use analytics to understand how you use our site and improve your experience. Privacy Policy