Stripe Error: insufficient_funds — Insufficient Funds
try {
const intent = await stripe.paymentIntents.create({
amount: 9999,
currency: 'gbp',
payment_method: 'pm_card_visa_chargeDeclinedInsufficientFunds',
confirm: true,
});
} catch (err) {
// err.type === 'StripeCardError'
// err.code === 'card_declined'
// err.decline_code === 'insufficient_funds'
// err.message === 'Your card has insufficient funds.'
}
insufficient_funds is the most common reason a Stripe card_declined error fires. It means the issuing bank checked the customer’s available balance, found it less than the amount you tried to charge, and refused authorisation. Stripe didn’t decline — the bank did. Your job is to surface the right message and let the customer recover gracefully without losing the sale.
The trick with insufficient_funds is that it lies somewhere between fraud declines (where you should never retry) and processing errors (where you should retry quickly). Treat it as a temporary state on a valid card: tell the customer clearly, offer an alternative payment method on the same screen, and if they retry on their own initiative later, let them.
Why this happens
- Customer's available balance is below the charge amount. The most literal cause — the card's account simply doesn't hold enough money. Note this is *available* balance, not posted balance: pending transactions, holds, and authorisations all reduce the spendable total even if the customer's app shows a higher number.
- Daily or weekly spend cap on the card. Many debit cards (especially in the UK and EU) have configurable daily limits that the cardholder may not realise are set. The bank returns `insufficient_funds` even though the account has the money, because the limit is exhausted.
- Pending authorisation hold consuming the balance. An earlier authorisation (hotel deposit, fuel pump pre-auth, subscription pre-charge) is still holding funds. Stripe's charge can't go through until the prior auth is captured or released, which can take 7-30 days for some merchants.
- Currency conversion buffer leaving customer short. When the card is denominated in a different currency to the charge, the issuer reserves a buffer (usually 5-10%) on top of the converted amount to absorb FX moves. A customer with £100 may fail a £95 charge in EUR if the buffer pushes the reservation over £100.
- Prepaid or gift card with no top-up. Prepaid cards return `insufficient_funds` once the balance is spent. Unlike a regular debit card, they can't be auto-topped-up, so retrying is pointless until the customer reloads.
How to fix it
Fixes are ordered by likelihood. Start with the first one that matches your context.
1. Show a specific, actionable message — don't say 'Card declined'
The customer needs to know *why* the charge failed and *what to do next*. A vague "Card declined" message kills conversion. For `insufficient_funds` specifically, the right copy is honest about the cause and offers two clear next steps: try another card, or top up and retry.
function declineMessage(err) {
if (err.code !== 'card_declined') return 'Payment failed. Please try again.';
switch (err.decline_code) {
case 'insufficient_funds':
return "Your card doesn't have enough available balance for this purchase. Try another card, or add funds and retry.";
case 'do_not_honor':
return 'Your bank declined the charge. Contact your bank or use another card.';
default:
return 'Your card was declined. Please try a different card.';
}
}
2. Offer an alternative payment method on the same checkout
Customers who hit `insufficient_funds` are 3-5x more likely to convert if you immediately offer a second payment method (Apple Pay, Klarna, BACS, another card) on the same screen. The Stripe Payment Element supports multiple methods natively — enable at least 2-3 alternatives in your Dashboard's Payment Methods settings.
const elements = stripe.elements({ clientSecret });
const paymentElement = elements.create('payment', {
layout: 'tabs',
// Stripe automatically shows alternatives based on country + currency
paymentMethodOrder: ['card', 'apple_pay', 'google_pay', 'klarna'],
});
paymentElement.mount('#payment-element');
3. Treat insufficient_funds as a soft decline — allow customer-initiated retry
Unlike `lost_card` or `stolen_card` (hard declines, never retry), `insufficient_funds` is a soft decline: the card is fine, the balance just isn't there right now. Allow the customer to retry whenever they want, but never auto-retry without their consent — chargebacks for unauthorised re-attempts are a real risk.
4. For subscriptions, enable Stripe Smart Retries
Stripe Billing's Smart Retries machine-learn the optimal retry schedule per card and per decline code. For `insufficient_funds`, Stripe typically retries 3-5 days later when payday cycles are most likely. Enable in Dashboard → Billing → Subscriptions → Recovery → Smart Retries.
5. Test with Stripe's `insufficient_funds` test card
Use card number `4000000000009995` in test mode to reliably trigger `insufficient_funds`. Build an automated test that asserts your decline-handling code surfaces the right message and offers an alternative payment method.
Detection and monitoring in production
Track `insufficient_funds` separately from other `card_declined` reasons in your monitoring. A sudden rise can indicate (a) you've started charging customers with auth holds from your own previous transactions, (b) an FX rate move is pushing customers over the edge, or (c) a checkout pricing change has crossed a typical balance threshold. Alert if `insufficient_funds` exceeds 3% of attempts over a rolling 24-hour window.
Related errors
- stripecard_declinedThe customer's card issuer refused to authorise the charge. Stripe relays the issuer's decline reason in `decline_code` (e.g., `insufficient_funds`, `lost_card`, `do_not_honor`).
- stripeauthentication_requiredThe card issuer requires the customer to complete 3D Secure (SCA / PSD2) authentication, but the PaymentIntent was confirmed without the customer completing the 3DS challenge — usually because you're using a bare Card element instead of the Payment Element or didn't handle `requires_action`.
- stripeapi_key_invalidThe API key you sent in the `Authorization: Bearer ...` header doesn't match any active Stripe key — usually because of a test/live mode mismatch, a deleted or rolled key, copy-paste corruption, or a missing environment variable falling through to `undefined`.
- stripesignature_verification_failedYour webhook handler called `stripe.webhooks.constructEvent()` and the signature in the `Stripe-Signature` header didn't match the HMAC-SHA256 of the raw request body computed with your endpoint secret.
- openairate_limit_exceededYour account has exceeded its per-minute request (RPM) or per-minute token (TPM) limit for the model you're calling. Limits are tier-based and per-model.
Frequently asked questions
Is `insufficient_funds` a hard decline or a soft decline? +
Should I auto-retry an `insufficient_funds` decline? +
Why does `insufficient_funds` happen even when the customer says they have money? +
Does `insufficient_funds` count toward my Stripe decline rate? +
Can I see how often customers retry successfully after `insufficient_funds`? +
Is `insufficient_funds` ever a fraud indicator? +
How does `insufficient_funds` interact with 3D Secure? +
What's the difference between `insufficient_funds` from Stripe and from a bank statement? +
When to escalate to Stripe support
Stripe support can't reverse an `insufficient_funds` decline — it's between the customer and their issuer. Escalate only if (a) you see a sudden cluster of `insufficient_funds` from a single BIN range, suggesting an issuer outage, or (b) Smart Retries aren't firing for subscriptions despite being enabled. For everything else, route the user to retry or pick a different payment method.