Stripe Payment Reconciliation: Compare Stripe State to Your App
You just found one customer whose Stripe subscription is active but whose app account is still on the free plan. You fixed that one. Now the uncomfortable question: how many more are there right now, silently paying for nothing, that nobody has complained about yet?
This is the moment most teams start reconciliation — not proactively, but reactively, after a support ticket or a chargeback surfaces the first mismatch. Stripe is the source of truth for payment state. Your application is the source of truth for what the customer actually experiences. Webhook delivery does not guarantee the two stay in sync: Stripe can show a paid invoice or an active subscription while your app still shows missing credits, a free plan, or stale access, and neither system will flag that gap on its own.
A one-off script catches today’s mismatches. It does not catch tomorrow’s, and it does not run itself. Here is how to build a reconciliation check you can actually trust and keep running.
Decide what should match
Start with one payment path and define the expected app-side effect.
Examples:
- Active Stripe subscription means the app account has the matching paid plan.
- Paid
invoice.paidevent means a credit grant record exists. - Cancelled subscription means app access is downgraded or scheduled to downgrade.
- Refunded payment means the related credit deduction or access revocation happened.
Keep the first reconciliation job narrow. A small check that runs weekly is more useful than a broad script nobody trusts.
Pull Stripe state
For a subscription app, list active subscriptions:
| |
For invoice-credit products, list paid invoices for a window:
| |
Use a bounded time window when you are starting. It keeps the result set small and makes discrepancies easier to audit.
Compare against app state
The comparison should use stable identifiers, not only email addresses. Prefer Stripe Customer ID, subscription ID, invoice ID, or payment intent ID stored in your own database.
Here is a runnable Python skeleton for a subscription check:
| |
Adapt the SQL to your schema. The important part is that every discrepancy becomes a concrete record your team can inspect.
Classify the mismatch
Not every mismatch has the same cause.
- Stripe active, app free: access provisioning failed or linked the payment to the wrong user.
- Stripe paid invoice, no credit grant:
invoice.paidhandling failed, dedupe blocked the grant, or workers were down. - Stripe cancelled, app active: revocation handling failed or your app intentionally waits until period end.
- App paid, Stripe inactive: manual app update, failed cancellation handling, or stale Stripe customer link.
Add enough context to each finding: customer ID, user ID, subscription or invoice ID, expected app state, observed app state, and timestamp.
Repair safely
Reconciliation tells you what is wrong. It should not blindly mutate production data without guardrails.
For each mismatch:
- Confirm the Stripe record and app record refer to the same customer.
- Confirm whether the expected effect already happened under another identifier.
- Use an idempotent handler, replay, or admin repair action.
- Record the repair with the Stripe identifier and operator.
- Verify the app state after repair.
For replay-based repair, your handler must dedupe by event ID or business effect. For direct admin repair, use a path that records an audit trail.
Automate after the first run
Once the script produces useful findings, schedule it. Start with a weekly run, then move high-risk paths to daily or hourly checks.
Alert only on actionable findings. A reconciliation job that produces noisy, untriaged output becomes background noise quickly.
Related guides
- How to monitor Stripe webhooks in production — combine reconciliation with queue and outcome monitoring
- Stripe invoice paid but credits not added — recover invoice-credit mismatches
- Customer paid on Stripe but has no access — repair access mismatches
- Post-payment failure recovery — detect missing outcomes before reconciliation catches them
Reconciliation is what you run after you already suspect something is wrong. SendPromptly is designed to catch these gaps closer to real time, by tracking the expected outcome after each payment event and opening an incident the moment one goes missing — so reconciliation becomes a backstop and audit practice instead of your only safety net. See how it works →