Our Stripe webhook had never delivered once. Here is how we found out.
The apex domain 308-redirected to www, Stripe does not follow redirects, and every webhook since launch had died on a redirect nobody ever saw — one hostname before the code we kept testing.
Our Stripe webhook endpoint was well built. Signature verification before parsing, idempotent event handling, correct status codes, unit tests on every branch. It had also never processed a single event, because no event had ever reached it.
The endpoint was registered in Stripe as:
https://vigil.wtf/api/stripe/webhookAnd vigil.wtf — the apex — answers every request with a 308 Permanent Redirect to www.vigil.wtf. That is standard, sensible hosting behaviour: one canonical hostname, the other permanently redirecting to it. Browsers follow it invisibly. SDKs follow it. curl -L follows it.
Stripe does not follow redirects on webhook deliveries. Deliberately — a redirect on a webhook is a way to trick a sender into re-POSTing a signed payload at an attacker's choice of URL, so Stripe treats 3xx as delivery failure, full stop. Every event since the endpoint was registered had arrived at the apex, received a 308, and died. The signature verification, the idempotency handling, the tests — all of it sat one hostname behind where the traffic actually went, unreachable since launch.
Why nothing caught it
Every layer had a reasonable excuse, which is what makes the failure worth writing down.
The app looked healthy because the webhook handler never errored — it was never invoked. Zero errors from zero traffic is indistinguishable, on an error dashboard, from zero errors from healthy traffic. Absence-of-failure metrics cannot tell silence from success.
Manual testing passed because humans test with tools that follow redirects. Paste the URL into a browser: works. curl -L: works. The one behaviour that mattered — a client that refuses redirects — is a flag nobody adds by hand, because almost no client behaves that way. Almost.
The business logic worked anyway, at first glance, because checkout returns the customer to a success page and the app also verifies state there. Subscriptions appeared to activate. What was actually missing was everything that arrives only by webhook — renewal events, payment failures, cancellation sync. The redirect had converted "billing is event-driven" into "billing is whatever the success page happened to see", and that distinction only detonates weeks later, on the first renewal.
The one place the truth was displayed all along: Stripe's own webhook dashboard, listing every delivery attempt as failed with a 308. Nobody had looked, because nothing had prompted anyone to look. The monitoring gap was not on our side of the wire, and our side of the wire was the only side we monitored.
There is also a quieter reason redirects specifically evade review: a permanent redirect is supposed to be the safe kind. A 308 preserves the method and the body — a POST stays a POST — so an engineer who knows the status codes reads "308 to www" as a transparent rewrite, not as a wall. It is transparent to any client that follows it. The entire failure lived in the difference between "the redirect is lossless" and "the recipient is willing to walk through it", and webhook senders as a class are unwilling, for good security reasons that have nothing to do with your site's hostname hygiene.
The fix, and the rule behind it
The mechanical fix took one minute: register the endpoint on the canonical host — https://www.vigil.wtf/api/stripe/webhook, the exact string the server answers 200 on — replay the missed events from Stripe's dashboard, and add a check that the configured webhook URL matches the canonical hostname exactly — the same class of derived-not-typed guard we use for provider counts and rate tables, because a URL that can silently disagree with the hosting config is a constant that can silently go stale.
The rule worth exporting is bigger than Stripe: a webhook that has never been observed succeeding has never succeeded. Delivery infrastructure fails silently by design — the sender retries, gives up, and records the failure on its side. Until you have seen one real event traverse the full path — sender log says delivered, your log says processed, with matching ids — the integration is a hypothesis. Ours was a hypothesis with 100% test coverage.
The general habit: verify the boring parts, in production, from the outside. The interesting parts — signature math, event handling — get reviewed and tested precisely because they are interesting. The boring parts — DNS, redirects, hostname config, TLS — are where this class of failure lives, exactly because nobody considers them worth checking. It is the same lesson as the edge timeout that killed long calls and the auth flag that failed open: the failure was never in the code we were staring at; it was in the plumbing around it that "obviously" worked. There is a reason the what-Vigil-watches list is built around observed traffic rather than declared configuration — declared configuration is what lied here.
What to do
Open your payment provider's webhook delivery log right now and look at the actual delivery outcomes — not your application logs, the sender's log. If you have never looked, assume nothing until you have. Then check every registered webhook URL against redirect behaviour: curl -sI -X POST <url> — a curl without -L — and anything other than your handler's own response (a 2xx, or a 400/401 signature rejection) means deliveries are dying in transit. If your apex redirects to www or vice versa, audit every externally registered URL — webhooks, OAuth callbacks, verification endpoints — for the wrong variant, because every one of them is this same bug waiting on a sender that refuses to follow.