Why 'Delivered' Doesn't Mean Your Webhook Actually Worked
HTTP 200 from your webhook handler doesn't mean your app actually did the thing. Here's the gap that silently breaks payment flows — and how to fix it.
Every webhook tutorial ends the same way: return a 200 status code as quickly as possible, process the event asynchronously.
Good advice. But it hides a problem that costs teams real money. A 200 response proves your handler received the event — not that your application committed the side effect. For the business impact of this gap, see webhook revenue leakage. For the technical debugging workflow, see the webhook debugging checklist.
The gap between delivery and application
When Stripe sends a payment_intent.succeeded event to your webhook handler, two things need to happen. Most teams track receipt. Almost no teams independently track application. See Stripe webhook documentation for Stripe's delivery model and Stripe webhook best practices for reliability recommendations. For how outcome receipts fit into a complete reliability architecture, see webhook-driven architecture.
- Your server receives the HTTP request and returns 200
- Your application commits the side-effect — the database write, the subscription upgrade, the order created
Most teams track #1. Almost no teams independently track #2.
The result: every webhook tool in existence tells you "delivered." None of them tell you "applied."
Why they're different
Your webhook handler might return 200 and then:
- Throw an unhandled exception in the async processing code
- Fail a database write due to connection pool exhaustion
- Drop a queue message because Redis was temporarily unreachable
- Commit a partial transaction that rolled back silently
Each of these results in the same observable state from Stripe's perspective: a successful 200 delivery. But from your customer's perspective: they paid and didn't get access.
The silent failure problem
What makes this particularly dangerous is the silence. Your monitoring shows green. Your webhook logs show "delivered." Your error tracking shows nothing. The customer emails support two days later. Silent failures are the ones that accumulate into webhook revenue leakage — customers who paid, didn't receive, and left without filing a ticket. For how to run a structured incident when this is discovered, see the webhook incident runbook.
By then, you have no idea:
- Which events were affected
- Whether Stripe already retried (and whether the retry succeeded)
- How many customers are in this broken state
- Whether replaying is safe or would create duplicates
What "Applied Confirmed" actually means
The only way to know a webhook was truly applied is for your application to say so — explicitly, after the side-effect is committed to durable storage.
This is the receipt pattern:
// After your DB transaction commits
await db.transaction(async (trx) => {
await trx('subscriptions').update({
tier: 'pro',
updated_at: new Date()
}).where({ user_id: customerId })
// Transaction commits here
})
// Only now send the receipt
await fetch('https://hooks.hooktunnel.com/api/v1/receipts', {
method: 'POST',
headers: {
'Authorization': `Bearer ${RECEIPT_SIGNING_SECRET}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
event_id: stripeEventId,
receipt_id: crypto.randomUUID(),
status: 'processed',
processed_at: new Date().toISOString(),
side_effect_refs: { subscription_id: subscriptionId }
})
})
The receipt is sent after the commit, not before. That's the guarantee.
What this unlocks
Once your application sends receipts, you know — with cryptographic certainty — which events were applied and which weren't.
- Paid but no receipt after 60 seconds? SLA breach alert.
- Delivered but Applied Unknown? Something happened between receive and commit.
- Applied Failed? Your handler reported the failure; here's the reason code.
This turns reactive debugging ("a customer complained") into proactive detection ("I see a gap, let me fix it before anyone complains").
The three states that matter
Most webhook tools show you one state: Delivered (or not).
The states that actually matter are:
| State | Evidence | What it means | |---|---|---| | Paid | Stripe event captured + signature verified | Money moved | | Delivered | Your handler returned 2xx | Message received | | Applied | Verified receipt from your app | Business outcome committed |
You need all three to answer the real question: "Did the payment result in the thing it was supposed to result in?"
Start proving, not guessing
HookTunnel shows all three states for every event. Set up receipts once, and "did customer X get what they paid for?" becomes a provable yes/no with a link — not a debugging session.
Explore webhook inspection features to see the three-state model in action. See the flat $19/month Pro plan for outcome receipts, 30-day history, and reconciliation dashboards.
Start for free → No signup required. Receipt setup takes 10 minutes.
Stop guessing. Start proving.
Generate a webhook URL in one click. No signup required.
Get started free →