Webhook Integration
logi notifies your RP of the following events.
Event catalog
| event_type | Delivery path | When |
|---|---|---|
user.deleted | legacy | A user account is deleted |
user.unlinked | legacy | A partner app is disconnected |
consent.revoked | legacy | A scope consent is withdrawn |
token.revoked | legacy | An access/refresh token is force-invalidated |
user.merged | PLAN-L outbox | Two logi accounts were merged — for the full payload, see Account Merge + Merge Idempotency |
user.grants_revoked | PLAN-L outbox | The user revoked every grant they had given the RP, in one go |
webhook_key.compromised | PLAN-L outbox | An operator force-retired a webhook signing key (the RP must rotate immediately) |
Events whose Delivery path is PLAN-L arrive only with the new signature format (t=,kid=,v1=), while legacy events arrive with the old format (sha256=) for the time being. For the detailed format branching, see below.
Configuration
Specify webhook_url when registering the app. Change it with PATCH /api/v1/applications/:id.
🛡️ URL policy (P1 SSRF defense)
- Only
https://is allowed (withhttp://localhostandhttp://127.0.0.1as exceptions in development). - The host is DNS-resolved and validated both at registration and at dispatch time:
- Private IP ranges are blocked (10/8, 172.16/12, 192.168/16, 127/8, 169.254/16, 100.64/10)
- Link-local / multicast are blocked
- IPv6 private/loopback/link-local are blocked the same way
- DNS rebinding defense: connect directly to the validated IP, while keeping the original domain for the SNI/Host header.
- On a violation, that event is treated as a delivery failure with the reason
ssrf_blocked.
Secret rotation
You must rotate the webhook signing secret. Until you do, signing falls back to BCrypt, and in that case the response includes the X-Logi-Secret-Deprecated: true and Deprecation headers — we recommend rotating from the developer portal.
curl -X POST -H "Authorization: Bearer $PAK" \
https://api.1pass.dev/api/v1/applications/:id/rotate_webhook_secretThe rotation response exposes the plaintext only once. Use it for verification afterward.
Expiry policy
- Default TTL: 1 year
- Admins/developers are notified starting 30 days before expiry (audit log + email in Phase 2)
- Attempting to issue a token with an expired client_secret is rejected with
invalid_client
Request format — two signature formats coexist
logi currently uses two signature formats at the same time. Your RP's verifier must accept both.
① PLAN-L outbox (canonical; new events use this path)
Dispatched by Logi::Webhooks::DeliveryJob. All events from PLAN-L onward — user.merged, user.grants_revoked, webhook_key.compromised, and so on — take this path.
POST https://your.app/hooks/logi
Content-Type: application/json
X-Logi-Event: user.merged
X-Logi-Event-Id: 01HV... # idempotency key (`event_id`)
X-Logi-Delivery-Id: 12345 # identical across retries
X-Logi-Signature: t=1735000000,kid=whk_2025q4_a1,v1=a3d9f0...
{"event_id":"01HV...","event_type":"user.merged","data":{...},"created_at":"..."}The signature is HMAC-SHA256(secret_for_kid, raw_body) — look up the key by kid, then verify.
② Legacy WebhookDispatchJob (deprecated, back-compat)
The existing four events (user.deleted, user.unlinked, consent.revoked, token.revoked) are still dispatched via this path.
POST https://your.app/hooks/logi
Content-Type: application/json
X-Logi-Event: user.deleted
X-Logi-Delivery-Id: 12345
X-Logi-Timestamp: 1735000000
X-Logi-Signature: sha256=a3d9f0...
{"id":12345,"event_type":"user.deleted","payload":{"user_id":42},"created_at":"..."}The signature is HMAC-SHA256(webhook_secret, raw_body) — a single secret.
Verifier-writing guide: the HMAC signature verification page has verifier examples that handle both formats — branch to PLAN-L if the header value contains a ,, and to legacy if it starts with sha256=.
Retry policy
Legacy path
- Up to 10 attempts (within 24h)
- Exponential backoff: 1m → 2m → 4m → 8m → 16m → 32m → 60m → 120m → 240m → 480m
- A 2xx response means delivery is complete. 3xx/4xx/5xx and timeouts are retried.
- If all 10 fail → mark
failed_atand surface it in the developer portal
PLAN-L outbox path
- Up to 5 attempts — Linear-style backoff: 1m → 5m → 30m → 2h → 6h
- 2xx → delivery complete
- 408 / 429 / 5xx → retried
- Any other 4xx → straight to DLQ (interpreted as the RP explicitly rejecting it)
- If all 5 fail → DLQ → developer portal + mark
webhook_outbox_entries.dlq_at