Skip to content

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 2xx immediately 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.