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/complete publishes identity.linked once 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/unlink publishes identity.unlinked at 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 with temporarily_unavailable.
  • application_id is the client that created the intent, so application-filtered endpoints for that client receive it. user_id is the player; it never changes. context.provider is the provider and context.country is set as for other player events.
  • event_id is 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