Stripe Invoice Paid but Credits Not Added: Causes and Safe Recovery
Usage-credit products — AI token credits, API quota top-ups, prepaid usage packs — have a specific failure mode: the invoice is paid, Stripe confirms it, but the credits are never added to the account. The customer is charged and has nothing to show for it.
This failure is invisible until the customer complains or exhausts their existing balance and notices nothing was added. By then, trust is already damaged.
Here is how to diagnose what happened and recover without risk of crediting customers twice.
How the Credit Grant Flow Usually Works
Most credit-based products handle this flow through invoice.paid:
- Customer purchases or auto-renews a credit pack
- Stripe generates an invoice and processes payment
- Stripe sends
invoice.paidto your webhook endpoint - Your handler queues a job to add credits to the customer’s balance
- The job runs and updates the
creditscolumn (or equivalent) in your database
The failure can happen at any step after step 3 — and the places most likely to break silently are steps 4 and 5.
Why Silent Job Failures Are So Dangerous Here
Unlike access provisioning failures — which the customer notices immediately when they try to use a feature — credit balance failures can go undetected for days. The customer already had credits from a previous purchase, so the app still works. The shortfall only becomes visible when that existing balance is exhausted.
This delay makes attribution harder: by the time a support ticket arrives, the payment event is days old, the job logs may have rotated, and the customer’s account history is harder to reconstruct.
Common Causes
The job enqueued but workers were down
Your handler accepted the invoice.paid event, returned 200, and enqueued the credit grant job. But your queue workers were down — due to a deploy, a crash, or an auto-scaling event — at the time the job was enqueued.
Depending on your queue configuration:
- The job may still be sitting in the queue, pending
- The job may have failed after an initial attempt and been moved to the dead letter queue
- The job may have been silently dropped if the queue itself lost in-memory state (common with Redis-backed queues that lost persistence)
Check: Your queue dashboard for pending or failed jobs around the time of the payment event. Your dead letter queue for a failed credit grant job. Your worker restart logs for downtime around the event timestamp.
The job failed and retries were exhausted silently
Your credit grant job raised an exception — a database connection failure, a uniqueness constraint violation, an unexpected null — and was retried several times before being moved to the dead letter queue. No alert was triggered.
This is common on small SaaS teams where dead letter queue alerting was never configured because it “has not been a problem yet.”
Check: Your dead letter queue. Your exception tracking tool (Sentry, Bugsnag, Honeybadger) for errors in the credit grant job class around the payment timestamp.
Deduplication logic blocked the credit grant
If your credit grant handler checks for duplicates using the invoice ID or event ID, a previous failed attempt may have written a deduplication record even though it did not complete the credit grant. When Stripe retried the event or when you attempted a replay, the deduplication check blocked it.
Check: Your processed events or job deduplication table. Look for a record matching the relevant invoice ID with a status of “processed” or similar. If the record exists but the credits were never added, your deduplication logic wrote the record before confirming success.
Fix: The deduplication record should only be written after the credit grant completes, not before. For recovery, you may need to delete the deduplication record and replay the event.
For safer handler patterns, see the Stripe webhook idempotency guide.
The invoice lookup failed to associate the right customer
Your handler looks up the customer by Stripe customer ID from the invoice payload and finds no matching record in your database. This happens if:
- The Stripe customer was created before your user record, and the link was not stored
- The customer changed their email, and your lookup is by email rather than Stripe customer ID
- A data migration moved user IDs without updating the linked Stripe customer reference
Check: The customer field in the invoice.paid payload. Verify whether a user account with that Stripe customer ID exists in your database.
Finding All Affected Customers
If this was a recurring failure (workers were down for an extended window, or a code bug silently failed for multiple invoices), you need to identify all customers who are missing credits — not just the one who complained.
Approach:
Pull all
invoice.paidevents from Stripe for the affected time window:1stripe events list --type invoice.paid --created[gte]=TIMESTAMP --created[lte]=TIMESTAMPFor each event, extract the invoice ID, customer ID, and credit amount
Cross-reference against your credits transaction log — every credit grant should have a corresponding record. Events without a matching credit transaction are missing grants.
Generate a list of customers and amounts to be corrected.
Recovering Safely Without Double-Crediting
Before you start applying credits, confirm that your credit grant logic is idempotent when given the same invoice ID. If it is not, you need to add that protection before running any recovery operation.
Recovery options:
Option A: Replay the Stripe invoice.paid event
If the root cause has been fixed and your handler is idempotent, replay the event from the Stripe dashboard. The handler will run again with the original invoice payload, grant the credits, and write the idempotency record.
Use the safe Stripe webhook replay guide if you need dashboard, CLI, or bulk replay steps.
Option B: Run a targeted recovery script
For bulk recovery, write a script that iterates over the affected invoice IDs and calls your credit grant function directly, passing the invoice ID as the idempotency key. Log every grant with invoice ID, customer ID, credit amount, and timestamp.
Option C: Manual grant with audit trail
For small numbers of affected customers, use an admin action in your application to grant the missing credits. Document each grant: which customer, which invoice, how many credits, by whom, at what time. This audit trail is important if the customer later questions their balance.
Communicating with Affected Customers
For customers who have not complained, proactive communication is better than waiting:
“We identified a processing issue that caused your recent credit purchase to not be applied to your account. We have corrected this and added [N] credits to your balance. No action is needed on your part. We are sorry for the delay and have taken steps to prevent this from happening again.”
For customers who already complained:
“You were right — the credits from your purchase were not applied correctly. We have added [N] credits to your account now. The issue has been resolved. Thank you for reporting it.”
Proactive correction before the customer notices builds trust. Correction after the complaint rebuilds some trust. Either is better than waiting for more customers to report it.
Related guides
- How to monitor Stripe webhooks in production — catch missing credit outcomes earlier
- Stripe payment reconciliation guide — find invoices with missing app-side credit records
- Post-payment failure recovery — see how SendPromptly opens and repairs missing-result incidents
SendPromptly monitors invoice.paid events for credit products the same way it monitors checkout.session.completed for access provisioning — it opens an incident when the business effect (credits added) does not follow the payment event within your expected window. See how credit recovery works →