Stripe Payment Reconciliation: Compare Stripe State to Your App
Stripe is the source of truth for payment state. Your application is the source of truth for customer experience. Reconciliation compares the two so you can find customers whose payment state and app state no longer match.
This matters because webhook delivery does not prove fulfillment. Stripe may show a paid invoice or active subscription while your app still shows missing credits, a free plan, or stale access.
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
SendPromptly is designed to catch many of these gaps closer to real time by tracking the expected outcome after a payment event. Reconciliation remains useful as a backstop and audit practice.