Billing webhook not firing on plan changes? Confirm delivery, trace the failure mode, reconcile charges, and communicate to customers clearly. Get the checklist.
If your billing webhook doesn’t fire during a subscription plan change—and customers see “unexpected” charges—treat it as two parallel problems: (1) customer reassurance + clear charge breakdown, and (2) event delivery + processing verification. First, confirm what actually happened in your billing system (invoices/charges/proration), then trace the event from “generated” → “delivered” → “processed” in your app, and finally add reconciliation checks so a single missed webhook can’t silently drift billing vs. access.
Photo by Albert Stoynov on Unsplash
What this post covers
A step-by-step debugging flow for “plan changed but webhook didn’t fire”
A simple charge reconciliation checklist (so you can explain charges confidently)
A customer communication checklist + ready-to-send email template
Preventative hardening (retries, idempotency, and event backfills)
1) Triage: confirm the customer impact in 10 minutes
1.1 Capture the facts (do this before debugging)
Collect these details in one place:
Customer / account ID (internal)
Old plan → new plan (and whether it was switched back)
Time window of the plan change
All invoices and charges created in the window (including $0 invoices)
Any access changes your app made (upgrades/downgrades/entitlements)
1.2 Decide: is this billing confusion or a real billing error?
In many “unexpected charge” situations, the billing system behaved correctly (proration, mid-cycle adjustments, pending authorizations), but the explanation wasn’t clear.
Quick check:
Do the invoice line items match the plan change rules (proration/credits)?
Did the customer get multiple invoices because the plan toggled more than once?
Are there charges that are authorizations (not captured) or $0 invoices?
If the invoice/charge trail is internally consistent, you can often avoid refunds and focus on transparent explanation + ensuring the plan state is correct going forward.
2) Debugging flow: “webhook not firing” during plan changes
2.1 Confirm the webhook endpoint is live and reachable
Verify your endpoint is deployed and responding quickly.
Check recent deploys, DNS changes, firewall/WAF rules, and TLS certificate status.
Look for 4xx/5xx spikes and timeouts.
Rule: A webhook “didn’t fire” is frequently “it fired, but delivery failed” or “it delivered, but your handler errored.”
2.2 Separate 3 failure modes: generated vs delivered vs processed
Mode A — Event not generated (rare):
The billing system never created the event you expected (wrong event type, wrong subscription object, plan change done via a different path).
Delivery to the wrong environment (test vs live) or wrong endpoint version.
Mode C — Event delivered but not processed correctly:
Handler threw an exception.
Signature verification failed.
You processed the wrong object (e.g., subscription updated but you listen for invoice paid).
Ordering/race conditions (plan change event arrives before your DB transaction commits).
Dedup/idempotency issues cause you to drop the “real” event.
2.3 Check the provider’s delivery log first (before your app logs)
Most billing providers offer a webhook delivery log/event history:
Find the relevant event by customer ID, subscription ID, or timestamp.
Confirm:
the event exists
the destination URL matches your current endpoint
the latest attempt status (HTTP code + response body)
retry history (how many attempts, over what time)
If you see repeated failures, fix the HTTP failure first. If you see success but your app didn’t update state, jump to handler processing.
2.4 Validate you’re listening to the right event(s) for plan changes
Plan changes may emit different events depending on provider and settings:
subscription updated / plan updated
invoice created / invoice paid
charge succeeded / payment succeeded
entitlement updated
Avoid relying on only one event. For plan changes, it’s common to require:
one event to update “plan state”
another event to confirm “payment outcome”
2.5 Look for configuration toggles that “fix it” temporarily
A common clue: you flip a webhook setting and suddenly messages start arriving.
That points to:
misconfigured endpoint selection (multiple endpoints; only one active)
stale configuration cached by your system or provider
wrong URL saved (e.g., old ngrok/dev URL)
disabled endpoint after failures
Treat this as a configuration drift problem—then harden it (see section 5).
3) Charge reconciliation checklist (so you can explain “unexpected charges”)
Use this when you need to confidently tell a customer what happened:
List every invoice created in the incident window (including $0 invoices)
For each invoice, list line items and what they represent (plan, proration, credits)
For each invoice, list payment attempts/charges and their final status
Sum the total impact for the customer (e.g., “4 invoices totaling $X”)
Confirm the current plan state (what the customer is on now)
Confirm your app’s entitlement/access state matches the current plan
If these don’t match, you have a billing vs access drift problem. Fix it immediately (either by replaying events or by running a backfill reconciliation job).
4) Customer communication checklist + template
4.1 What to include (keep it simple)
A clear charge breakdown (count of invoices/charges + total)
Confirmation of the customer’s current plan (and any manual correction)
Whether a refund is needed (often: “No refunds needed; charges are valid”)
A short commitment: “We’re fixing the underlying issue and will confirm when complete”
A timeline (days, not weeks) and a point of contact
4.2 Ready-to-send email template
Subject: Update on your plan change + billing charges
Hi —,
Thanks for flagging this. Here’s what we found:
Total charges: $[TOTAL] across [N] invoices during the plan change window.
What those charges were for:
Invoice 1: [brief explanation]
Invoice 2: [brief explanation]
(include any $0 invoice as “$0 invoice generated during the plan change”)
Your account is currently on [CURRENT PLAN] (we confirmed this on our side).
At this time, no refunds are needed—the invoices align with the plan change and prorations. That said, we’ve identified a bug in our webhook-based plan-change handling and we’re fixing it more thoroughly now.
We’ll follow up within [X days] to confirm the fix is complete. In the meantime, you can continue using the product normally.
Thanks,
[Name]
5) Prevent this from happening again (hardening checklist)
5.1 Make webhook processing resilient
Idempotency: store event IDs and ignore duplicates safely
Retries: queue processing and retry transient failures
Timeouts: keep webhook response fast; do heavy work async
Alerting: alert on repeated delivery failures or handler errors
5.2 Add reconciliation so you’re not “webhook-only”
Webhooks should update state quickly, but your system should be able to heal if one is delayed or missed:
Daily/Hourly job: compare subscription state vs. your entitlements
Backfill job: re-fetch events/invoices for a given customer and re-apply
“Drift detector”: alert when access doesn’t match paid plan
5.3 Incident hygiene
Write a brief internal postmortem: what happened, impact, fix, prevention
Add a runbook: this exact checklist + links to logs/dashboards
Get help debugging this
If your billing webhook system is creating support tickets or causing revenue leakage, Connex Digital can help you audit your event flow, add reconciliation jobs, and harden your subscription lifecycle.
Book a ZoomFlow session — one of our consultants will trace the event chain with you live and help you ship the fix in the same call.
Build an InvestNext GoHighLevel integration in Zapier: catch webhooks (or email fallback) to create/update contacts, dedupe by email, and link fund opportunities.
JobPro Sage integration without a public API is doable. Use outbound webhooks and a middleware layer to sync customers, jobs, invoices, and job costs reliably.
Zapier's Procore triggers lock you to one project. Here's how to build a custom Zapier trigger against the Procore API so Slack alerts work across all projects.