Webhooks
Get notified when a user updates their profile, revokes your app, or closes their account. Every event is HMAC-signed; verify before you trust.
Setup
Set your webhook URL on the developer dashboard. Must be HTTPS in production; HTTP localhost is allowed for dev. We don't validate reachability — the delivery worker marks failed events and retries with backoff.
Event envelope
Every event is delivered as a POST with this body:
{
"id": "evt_01H...",
"type": "user.profile.updated",
"created_at": "2026-06-03T18:30:21Z",
"client_id": "your-client-id",
"user_sub": "the-pairwise-sub-for-your-app",
"data": {
"fields": ["given_name", "preferred_username", "pronouns"]
}
} Headers:
- X-Whisp3r-Signature
- HMAC-SHA256 of the raw body, hex-encoded, prefixed with
sha256=. Verify against your client secret. - X-Whisp3r-Event
- The event type. Use for fast routing without parsing JSON.
- X-Whisp3r-Timestamp
- Unix seconds. Reject events older than 5 minutes to defend against replay.
Signature verification
import crypto from 'node:crypto';
function verify(rawBody, signatureHeader, clientSecret) {
const expected = 'sha256=' +
crypto.createHmac('sha256', clientSecret)
.update(rawBody)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signatureHeader)
);
} Event catalog
A webhook fires whenever ANY profile field changes that you have a
scope for. The event type narrows what changed; the data.fields array (where applicable) names the specific
OIDC claim keys whose values are now stale. In every case the right
response is to re-fetch /oauth2/userinfo for the current
values — the webhook tells you "something moved," not what the new
value is.
user.profile.updated
General-purpose profile change. Fires for any combination of these
fields the user updates at /dashboard/profile: name (first,
middle, last, suffix), username, pronouns, birthday, age tier
derivations. The data.fields array names which OIDC
claim keys are stale (given_name, preferred_username, pronouns, birthdate, etc.).
user.photo.changed
Specifically the avatar. If you cache the picture URL,
invalidate. Fires as its own event because most apps cache photos
separately from the rest of the profile.
user.email.updated
The user changed their primary email address. You don't see the address itself (the email-relay model means you never have it), but you should re-validate any internal flag that tracked "email-verified-since-X" — the underlying verification state may have reset.
user.cookies.changed
The user updated their analytics or marketing cookie preferences.
Re-fetch cookie_preferences from userinfo and apply the
new opts — same UX they get on Whisp3r Auth itself.
user.language.changed
The user changed their preferred interface language. Re-fetch locale (BCP-47 tag) and switch the UI on next page load.
user.age_gate.passed
The user just crossed a tier boundary (turned 13, 16, 18, or 21).
Fires once per tier per user. Apps that gate access by age can flip
their feature on without polling. The data.tier field
indicates which threshold was crossed.
user.consent.revoked
The user disconnected your app. All outstanding tokens are revoked;
subsequent calls fail with invalid_grant. Sign the user
out of your app and discard your cached profile.
user.deleted
The user closed their Whisp3r Auth account entirely. Delete their row
from your database, including any data you've stored against the sub. This is one of the few times you must delete; the
pairwise sub is the only identifier you have for them,
and once their WA account is gone, you have no way to sign them in
again.
Delivery semantics
- At-least-once. A flaky retry can deliver the same
idtwice. Idempotency: maintain a seen-event-ID set. - Ordering not guaranteed. If you need strict order, treat the body as a "you should re-fetch" trigger and call userinfo.
- Retry policy. 5xx, network failure, and timeouts are retried with exponential backoff up to 24 hours. 4xx responses are treated as terminal — fix your handler.
- Response. Return
200within 5 seconds. Anything else is treated as failure and retried.