Most webhook reliability writing treats a lost event as a data problem. You are missing a row, you notice eventually, you backfill. Annoying, recoverable, mostly invisible to anyone outside engineering.
Identity events break that framing, because the two directions are not symmetric.
An access.granted event that never arrives produces a user who cannot get in, and users who cannot get in tell you within minutes. It is self-reporting. The system has a human alerting layer built into it for free.
An access.revoked event that never arrives produces a user who can still get in. Nobody reports that. The person it benefits has no reason to mention it, and everyone else believes the offboarding completed because your identity provider says it did. The gap persists until an audit finds it, which could be a year.
TL;DR
- Missing a grant is a support ticket. Missing a revocation is an open door with no alarm on it
- Your identity provider's dashboard shows the revocation succeeded, because on their side it did
- Clerk, and anything else built on Svix, retries 8 times over roughly 18 hours, then disables the endpoint after 5 days of total failure
- The endpoint being disabled is itself a silent state, and it is the one that matters most
- Treat webhook receiver availability as an access control, not as infrastructure hygiene
The failure the dashboard cannot show you
An admin removes someone from the org. The identity provider updates its own records, invalidates its own sessions, and fires organizationMembership.deleted or user.deleted or session.revoked at your endpoint.
Your endpoint is mid-deploy and returns 502.
From the identity provider's console, the membership is gone. From the admin's view, offboarding is done. Your application still holds a row saying this person is a member of that org, and whatever cached session or API token you issued off the back of the original grant is still valid.
There is no error surface anywhere that a human looks at. The provider's webhook log has a failed delivery in it, but nobody reads a webhook log unless they already suspect something.
Retry buys you time, and then stops
Clerk sends its webhooks through Svix, and a good number of other identity and infrastructure products do the same. The schedule is public and worth knowing precisely, because it defines exactly how long your endpoint can be broken before a revocation is lost for good.
Eight attempts: "Immediately, 5 seconds, 5 minutes, 30 minutes, 2 hours, 5 hours, 10 hours, 10 hours (in addition to the previous)."
That is roughly eighteen hours of coverage, which is genuinely generous. A deploy window, an incident, a bad Friday: all comfortably absorbed. After the eighth attempt, Svix sends an operational webhook of type message.attempt.exhausted, which is useful only if you have somewhere for operational webhooks to go.
Then there is the second threshold: "If all attempts to a specific endpoint fail for a period of 5 days, the endpoint will be disabled and an operational webhook (EndpointDisabledEvent) will be sent to your account." The clock has a qualifier: "The clock only starts after multiple deliveries failed within a 24 hour span, with at least 12 hours difference between the first and the last failure."
Read the disabled state carefully. Once an endpoint is disabled, new events are not queued for it. They are not delivered later when you fix the endpoint. Every revocation during that window simply never reaches you, and the only notice is an operational webhook sent to an account nobody has configured a destination for.
The asymmetry is a design input
Once you accept that the two directions carry different costs, some ordinary decisions change.
Fail closed on the events that remove access. If your handler cannot process session.revoked, the correct behaviour is not to return 200 and log a warning. Return a 5xx so the retry schedule engages, and make sure your own alerting fires. For a grant you can afford optimism. For a revocation you cannot.
Do not rely on the webhook alone. Push events are a latency optimisation, not a source of truth. A periodic reconciliation against the identity provider's API, asking "who should currently have access," converts a permanently missed revocation into one that self-heals on the next sweep. That is the same hybrid argument made in Webhooks vs Polling, and identity is the case where it stops being optional. Run it hourly and the worst case is an hour of stale access instead of forever.
Make your own sessions expire. If your application issues its own session or token off the back of an identity event, a short TTL with a refresh that re-checks membership means a missed revocation ages out on its own. Long-lived tokens turn one dropped webhook into indefinite access.
Order matters here too. A member.added retried after a member.removed will re-add someone you just removed, and every event was processed exactly once. The version guard pattern, applying an update only when its timestamp is newer than the row's, is in You Received the Same Webhook Twice. For access control, prefer rejecting an illegal state transition outright over relying on timestamp comparison.
Know what your endpoint being disabled looks like. This is the single most valuable alert in the whole category. If your identity webhook goes quiet for longer than its normal gap, something is wrong, and "quiet" is the exact symptom of the disabled state. A "no identity events in 6 hours during business hours" check would catch it. Nothing else will.
What to do about the audit question
If you handle SOC 2, ISO 27001, or any customer security questionnaire, there is a question in there about timely deprovisioning. The honest answer, if your revocation path is a single webhook with no reconciliation, is that deprovisioning happens promptly except when it silently does not, and you cannot tell the difference after the fact.
That is a hard thing to write down. It is also fixable with two pieces: a durable log of every identity event that arrived, and a reconciliation job whose output you can point at. The log answers "what were we told, and when"; the sweep answers "and did we act on it." We wrote about why a mutable database row is a weak answer to the first question in A Tamper-Evident Webhook Log.
Where AnyHook fits
The structural issue is that your receiver's availability has become part of your access control, and receivers built on application infrastructure inherit application uptime.
Pointing the identity provider at in.anyhook.net/you/identity separates the two. AnyHook acknowledges and persists at the edge in under 50ms, so a deploy, a cold start, or a database outage on your side does not produce a failed delivery at all. From there:
- Retries against your own endpoint run for far longer than the provider's schedule, and against a system that was never the thing that went down
- Every revocation is stored before delivery is attempted, encrypted at rest, so "did we ever receive it" has an answer independent of your application logs
- Replay over a time range turns a broken deploy window into a re-delivery rather than a manual audit of who should have lost access
- Failure alerts at 1, 5, and 20 consecutive failures arrive long before any provider's disable threshold
None of this removes the reconciliation sweep, and you should still run one. A relay guarantees you received what was sent; only the sweep catches what was never sent.
Takeaway
Grants are self-correcting because the affected person complains. Revocations are not, because the affected person does not. That single asymmetry is why identity webhooks deserve a stricter reliability standard than the rest of your integrations, even though the payloads look the same.
Fail closed on removals, expire your own sessions, run a reconciliation sweep, and put an alert on unexpected quiet. The alternative is a security control whose failure mode is silence.