Reference · Detectors
10 checks on the boundary between Stripe and your app DB.
Every detector below is deterministic — we never invent numbers, never train models on your data. Some checks run on Stripe alone (delivery failures, expired coupons, expiring cards); the rest compare Stripe against the subscription CSV you upload. Either way, every finding in your PDF traces back to the exact Stripe row — and, where a CSV is involved, the cell that disagrees.
Stuck-event silent failure
What it catches
Stripe fired a state-changing event your application database never reflected. Either your webhook handler returned 200 and then crashed silently, or it never received the event and you missed it.
How it works
With a CSV attached we run full-confidence mode: for every state-changing event, we derive what your CSV should now look like (sub created means a row exists; payment succeeded means the row is marked paid) and emit a finding when reality disagrees. Without a CSV we run partial-confidence: any event still showing pending_webhooks > 0 after 24h is flagged as likely silent.
Webhook delivery failure
What it catches
The 503s, the timeouts, the silent 5xx’s. We compute a workspace-wide success ratio across every endpoint and every event type Stripe tried to deliver.
How it works
From Stripe’s events API: count drained events (delivered + acknowledged) versus total. Below 95% delivery success across the window we emit a workspace-wide finding. Per-endpoint findings emit at higher confidence when an individual endpoint has elevated failure.
Subscription state mismatch
What it catches
Two failure modes. Leaked service: your DB says canceled but Stripe says active — they keep paying for something your app already shut off. Phantom paying: your DB says active but Stripe says canceled — they have access but you’re no longer billing them.
How it works
Join Stripe subscriptions to CSV rows by external customer id (or lowercased email). Compare normalized status. We currently flag active ↔ canceled in both directions; active ↔ past_due is left to D6 to avoid double-counting.
Plan / price drift
What it catches
Your DB’s plan_code and Stripe’s active price ID don’t agree. A migration left one side behind, or a customer was manually moved on one side and not the other. Both numbers look plausible in isolation — the problem is they aren’t the same row.
How it works
We auto-derive a plan_code-to-price_id map per audit (modal price id per plan_code, sample ≥ 3 and modal share ≥ 0.70). For each subscription we check whether the CSV’s plan_code maps to the price Stripe is actually billing. Mismatches emit a finding.
Ghost customer
What it catches
Stripe has them paying — recurring or one-time, at least $0.01 in the last 90 days — but your CSV has no row matching their email. Almost always a signup webhook that returned 200 and then silently failed to insert.
How it works
Email-only match. We lowercase every Stripe customer email and look for it in the CSV email column. Customers with paid invoices in the last 90 days and no match emit a finding. Active subscriptions are critical; one-time-only paid customers are high.
Dunning / payment-fail drift
What it catches
Stripe has the subscription in past_due or unpaid — the card declined, Smart Retries fired, the customer didn’t fix it. Your DB still marks them active and they still have access to the product.
How it works
Per subscription, compare Stripe status against CSV status. Stripe past_due or unpaid + CSV active produces a finding. We don’t model retry counts or dunning age — the state of the sub in Stripe is the source of truth.
Trial / period-end drift
What it catches
Either the trial ended in Stripe and your DB still has them as trialing, or a renewal date drifted across the boundary. Churn signals, dunning logic, and renewal reminders all consume these fields — when they disagree, downstream code makes wrong decisions.
How it works
For each subscription: if status is trialing in either source, compare trial_end; otherwise compare current_period_end. A delta of more than 24 hours emits a finding.
Expired coupon still applied
What it catches
A promo code's redemption window closed, but the discount it created keeps applying to existing subscribers every cycle. The Black Friday code you never turned off is still taking 20% off in March.
How it works
For each active or trialing subscription, inspect its discounts. If the coupon's redeem_by has passed (or Stripe already marked it invalid) yet the discount is still attached, emit a finding. Estimated give-away = amount_off, or percent_off × the item's unit price.
Card expiring soon
What it catches
An active subscription's card expires this month or next, with no updated card on file. The next renewal fails silently and you lose a paying customer to involuntary churn.
How it works
For each active or trialing subscription, read the default payment method's card expiry. If it falls in the current or next calendar month (or has already passed), emit a finding. At-risk revenue = the item's unit price × an involuntary-churn factor.
Uncollected subscription
What it catches
A subscription Stripe still counts toward your MRR but is no longer collecting — status past_due (a payment failed) or unpaid (retries exhausted, service usually still on). It's revenue you're billing on paper while taking in nothing.
How it works
Across every non-canceled subscription, flag any whose status is past_due or unpaid — Stripe's own billing status is the truth, so no app DB is needed. past_due is High (still retrying); unpaid is Critical (retries done). At-risk revenue = the item's unit price × a recovery factor. This is the Stripe-only sibling of D6.
Driftcheck