Webhook events
Rakomi dispatches webhooks from the same transaction that commits the state change, so a webhook is queued if and only if the state change itself succeeded. Delivery is eventual-consistent (queued, then retried with exponential backoff); consumers must be idempotent.
Publisher apps: the events below are the tenant webhook catalog. Third-party publishers consuming install-lifecycle events use the separate publisher-webhook receiver contract.
Account linking
Section titled “Account linking”user.account_linked
Section titled “user.account_linked”Fires on BOTH explicit and automatic account linking (the method field
discriminates). The provider email is HMAC-SHA256 hashed with a
tenant-scoped secret — raw emails never leave the event payload (GDPR
Art. 25 data minimisation).
{ "id": "01J9...", "event": "user.account_linked", "severity": "info", "timestamp": "2026-04-24T12:34:56.789Z", "tenant_id": "c7f4...", "data": { "userId": "7a2e...", "provider": "google", "providerEmailHash": "4e3c9b...", "linkedAt": "2026-04-24T12:34:56.789Z", "method": "explicit_link", "notificationSent": true }}method—'explicit_link'when the user clicked “Add account” in the dashboard,'automatic_link'when the backend merged a verified-email provider identity onto an existing account during sign-in.notificationSent—trueiff the account-linked notification email was enqueued in the same transaction as the link commit.falseonly in the defensive-fallback case where the enqueue INSERT failed but the link tx already committed (effectively impossible given our same-tx enqueue; documented as theoretical).
Note that notificationSent: true is a “notification was queued”
semantic, NOT a “delivered” semantic. Delivery SLO is 15 minutes (OWASP
ASVS V2.7.3). If you need delivery confirmation, read the delivery history
for your webhook endpoint rather than inferring it from this event.
user.account_unlinked
Section titled “user.account_unlinked”{ "id": "01J9...", "event": "user.account_unlinked", "severity": "info", "timestamp": "2026-04-24T12:34:56.789Z", "tenant_id": "c7f4...", "data": { "userId": "7a2e...", "provider": "google", "unlinkedAt": "2026-04-24T12:34:56.789Z" }}Unlink is the de-escalation path — a user who mis-linked can always
reverse it, even during the post-link cooldown window. If the provider
is Apple and a refresh token was persisted, Rakomi calls the Apple
revocation endpoint best-effort in the background; the result lands in
the audit log (apple_revocation_status: 'success' | 'failed' | 'no_token')
but NOT in the webhook payload.
OAuth clients
Section titled “OAuth clients”oauth_client.scopes_updated
Section titled “oauth_client.scopes_updated”Fires when a Tenant Owner changes an OAuth client’s registered scopes via the
dashboard (PATCH .../oauth-clients/{clientId}). Emitted only on a real
transition — an identical-scope PATCH (order/duplicate-insensitive,
case-sensitive) is a no-op and dispatches nothing.
{ "id": "01J9...", "event": "oauth_client.scopes_updated", "severity": "info", "timestamp": "2026-07-01T12:34:56.789Z", "tenant_id": "c7f4...", "data": { "scope_change_id": "3f1a...", "client_id": "client_abc123", "previous_scopes": ["openid", "profile", "email"], "new_scopes": ["openid", "email", "sso:read"], "added_scopes": ["sso:read"], "removed_scopes": ["profile"], "change_reason": "We now read your SSO configuration.", "updated_at": "2026-07-01T12:34:56.789Z", "updated_by": "{\"user_id\":\"7a2e...\"}" }}Delta-shape semantics — read these carefully to stay redelivery-safe:
new_scopesis the authoritative full scope set. Prefer it over replayingadded_scopes/removed_scopes; at-least-once redelivery or reordering from the outbox is then self-correcting.previous_scopes/new_scopesare canonical;added_scopes(new − previous) andremoved_scopes(previous − new) are convenience-derived.removed_scopesis informational — a requested-scope change, NOT a revocation. Tokens already issued on the old scopes keep working until natural expiry (RFC 7662 reports token-time scope). To revoke immediately, use the token/session revocation path.scope_change_idis a stable, per-change UUID — use it as the dedupe key (updated_atalone is insufficient).updated_byis a JSON-encoded object ({"user_id":"..."}) — id-only on the wire for PII minimization (the actor email stays in the tenant’s internal audit log). The object shape is forward-compatible: a future non-human actor gains atypediscriminator, never a scalar replacement. (This mirrors thechangesfield onrole.updated, which is likewise JSON-encoded.)
Governing specs: RFC 6749 §3.3 (scope tokens, case-sensitivity, set semantics),
§4.1.2.1 (state echo on the consent decline); RFC 9700 (incremental auth,
exact redirect matching); OIDC Core 1.0 §3.1.2.6 (consent_required under
prompt=none); RFC 8414 §2 (scopes_supported); RFC 7662 / RFC 7009
(introspection/revocation report token-time scope — removal ≠ revocation).