AUTH4D-319 identity.linked and identity.unlinked webhook events
This fragment extends AUTH4D-158 (repository file docs/api-spec/auth4d-158.md), AUTH4D-216 (repository file docs/api-spec/auth4d-216.md) and
AUTH4D-308 (repository file docs/api-spec/auth4d-308.md). It adds two Flows event types, identity.linked and
identity.unlinked, which webhook endpoints can subscribe to. No endpoint, signature header or
migration changes: webhook_endpoints.event_types is a JSON array and fan-out matches types
generically.
Events
FlowEventTypeSchema and FlowEventSchema in packages/contracts accept both types. The envelope
is the same as for every other event. data is strict and the same for both:
| Field | Type | Meaning |
|---|---|---|
data.provider |
"steam" | "discord" | "epic" |
The linked or unlinked provider. |
data.identity_id |
idn_… |
The identity row that was linked or removed. |
The provider’s subject (a SteamID, a Discord user ID, an Epic account ID) is never published, the
same rule as for the identity.linked and identity.unlinked audit events.
{
"event_id": "evt_m9Ck2Yt4Rz0aLq7VbN3xW8eHs5uJdG1pFiKoTyXc6E2",
"version": 1,
"type": "identity.linked",
"tenant_id": "ten_0123456789abcdef",
"application_id": "cli_0123456789abcdef",
"user_id": "usr_0123456789abcdef",
"occurred_at": "2026-10-22T09:41:17.208Z",
"context": { "provider": "steam" },
"data": { "provider": "steam", "identity_id": "idn_0123456789abcdef" }
}
When it is published
POST /t/{tenantId}/account/link/intent/completepublishesidentity.linkedonce the identity move and its audit event have committed and the session that created the intent has been logged out.POST /t/{tenantId}/account/link/intent/unlinkpublishesidentity.unlinkedat the same point.- Nothing is published for a refused intent (
identity_in_use,only_sign_in_method, a foreign or replayed proof, another browser), a denied or expired intent, or when the logout fails withtemporarily_unavailable. application_idis the client that created the intent, so application-filtered endpoints for that client receive it.user_idis the player; it never changes.context.provideris the provider andcontext.countryis set as for other player events.event_idis derived from the tenant, the type and the intent handle hash. An intent completes at most once, so every link or unlink has its own id and a retried publish reuses it.
Delivery
Delivery is best effort, as for every other event: a queue failure is logged as
flows.publish_failed with only type and tenant_id, and never fails the request.
Console
The webhook form lists identity.linked as Account linked and identity.unlinked as Account
unlinked, and selects both by default for new endpoints. Existing endpoints keep their stored
event types.
Source: docs/api-spec/auth4d-319.md