← 목록

Stripe webhooks can work and your app access can still be wrong

devto 2026-07-02 원문 보기 ↗


Most Stripe billing bugs get described as webhook bugs.

Did the event arrive? Did the signature verify? Did the handler return 200? Is the handler idempotent? Can we replay failed events?

Those are good questions. But they miss another one:

Does the access state in your app match the billing state in Stripe right now?

That check is final-state reconciliation.

The failure mode

A common SaaS setup looks like this:

That works until it doesn't.

A few normal ways it breaks:

Now Stripe says one thing and your app says another.

There are two different problems here:

  1. Unpaid but active

    Stripe says canceled, unpaid, or past due, but the app still grants access. This is usually silent. Nobody opens a support ticket to say they are still getting free compute.

  2. Paid but blocked

    Stripe says active or paid, but the app blocks or downgrades the customer. This is urgent because the customer will probably notice before your cron does.

Those two cases should not be handled the same way.

Webhook reliability is not the same check

A reliable webhook pipeline asks:

Final-state reconciliation asks:

You probably need both.

Webhook infrastructure prevents a lot of failures. Reconciliation catches the ones that still escape. It also catches problems that never went through the webhook path: admin overrides, migrations, backfills, legacy status fields, and access logic that drifted over time.

What a small reconciliation check needs

You do not need a huge system to start.

From Stripe, export or query:

From your app, export:

That last part matters.

If your middleware checks access_enabled, your rate limiter checks plan_tier, and your billing page checks subscription_status, your first drift problem might be inside your own database.

Start with the fields that actually grant or deny access.

Do not auto-fix on day one

It is tempting to auto-suspend every unpaid-but-active account.

I would not start there.

A first reconciliation job should usually flag, not fix. There are too many legitimate edge cases: trials, grace periods, dunning windows, enterprise comps, test accounts, manual support exceptions, and custom contracts.

A safer first workflow:

  1. Run the comparison nightly or weekly.
  2. Split findings by direction.
  3. Treat paid-but-blocked as urgent.
  4. Put unpaid-but-active and ambiguous cases into review.
  5. Add notes for known exceptions.
  6. Only automate actions after you trust the classification.

The first version should help you see drift, not create a new production incident.

I built a small local-first prototype

I built EntitleGuard to test this workflow as a free local audit.

It compares:

The comparison runs in the browser.

No Stripe API key. No database credentials. No account. No upload.

It flags:

The source is public, so the local-only claim is easy to inspect.

Live audit:

https://entitleguard.amertech.online/audit

Source:

https://github.com/impara/EntitleGuard

The product question I am testing now is whether this should stay as a one-time diagnostic or become recurring monitoring: nightly diff, alerting, review history, and an evidence trail for each mismatch.

My guess is that most teams only care about this after they have seen drift once.

If you run a Stripe SaaS

A practical first check:

This is not a replacement for correct webhook handling.

It is a backstop for the final state your users actually experience.

If Stripe and your app disagree, the user does not care that the webhook pipeline looked healthy.