Webhooks¶
Webhooks let your server react to account events in real time. Configure them per-client in the developer portal.
Event taxonomy¶
| Event | Fired when |
|---|---|
account.signed_in | A user successfully signed in via your client. |
account.signed_out | A user signed out via RP-initiated logout. |
account.consent_granted | A user granted consent for a new scope set. |
account.consent_revoked | A user revoked consent (from their LXL profile). |
account.linked | A provider (Google/etc.) was linked. |
account.unlinked | A provider was unlinked. |
account.deleted | The user's LXL account was deleted. |
tokens.revoked | An access or refresh token was revoked. |
Delivery format¶
```http POST /your/webhook/endpoint HTTP/1.1 Host: yoursite.org Content-Type: application/json X-LXL-Event: account.signed_in X-LXL-Delivery: 8c5e4e2a-2e1d-4f7e-a4c2-1b9c3d5e7f02 X-LXL-Signature: t=1714680000,v1=4f6e2c3a91b5... User-Agent: Little-X-Little-Webhooks/1.0
{ "id": "evt_8c5e4e2a", "event": "account.signed_in", "created_at": "2026-05-03T10:00:00Z", "client_id": "your-client-id", "data": { "account": "Bootim", "scopes": ["openid","profile","email"], "ip": "203.0.113.42", "user_agent": "Mozilla/5.0..." } } ```
Verify the signature¶
The header X-LXL-Signature follows the Stripe convention:
t=<unix-timestamp>,v1=<hex hmac_sha256(secret, t + "." + body)>
php function verify($secret, $body, $header) { if (!preg_match('/t=(\d+),v1=([a-f0-9]+)/', $header, $m)) return false; [$_, $ts, $sig] = $m; if (abs(time() - (int)$ts) > 300) return false; // reject stale $expected = hash_hmac('sha256', $ts . '.' . $body, $secret); return hash_equals($expected, $sig); }
js import crypto from 'crypto'; function verify(secret, body, header) { const m = /t=(\d+),v1=([a-f0-9]+)/.exec(header); if (!m) return false; const [, ts, sig] = m; if (Math.abs(Date.now()/1000 - +ts) > 300) return false; const expected = crypto.createHmac('sha256', secret).update(ts + '.' + body).digest('hex'); return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig)); }
python import hmac, hashlib, time, re def verify(secret, body, header): m = re.match(r't=(\d+),v1=([a-f0-9]+)', header) if not m: return False ts, sig = m.group(1), m.group(2) if abs(time.time() - int(ts)) > 300: return False expected = hmac.new(secret.encode(), f'{ts}.{body}'.encode(), hashlib.sha256).hexdigest() return hmac.compare_digest(expected, sig)
Retry policy¶
If your endpoint returns a non-2xx status (or times out after 10s), we retry on this schedule:
| Attempt | After |
|---|---|
| 2 | 1 second |
| 3 | 5 seconds |
| 4 | 30 seconds |
| 5 | 5 minutes |
| 6 | 30 minutes |
| 7 | 2 hours |
| 8 | 12 hours |
After 8 failed attempts, the delivery moves to the dead-letter queue. You can replay any delivery from the developer portal.
Best practices¶
- Respond fast. Return
2xximmediately and process asynchronously. - Be idempotent. We may deliver the same event more than once; key your processing on
id. - Verify timing. Reject events older than 5 minutes to defeat replay attacks.
- Rotate secrets by adding a second secret in the portal, switching your verifier to accept either, then removing the old one.