Skip to Content
Platform APIWebhooks

Webhooks

platform-api receives identity events from Clerk (delivered via svix) and syncs them locally. This is a server-to-server entry point — you don’t call it directly.

EndpointPOST /api/webhooks/clerk
Auth@Public (no Clerk guard) — verified by svix signature instead
BodyClerk raw event JSON (raw body preserved for verification)
Response200 · { "ok": true }

Signature verification

The controller verifies the svix headers (svix-id / svix-timestamp / svix-signature) with CLERK_WEBHOOK_SECRET. A missing secret or missing headers → 400; a bad signature → 401.

main.ts preserves the raw request body for /api/webhooks/* so signatures can be verified — don’t move webhook routes off that prefix (see Architecture & conventions).

Idempotency

Each delivery’s svix-id is the idempotency key, written to the WebhookEvent ledger:

  1. Check the ledger first — a previously seen id is skipped (no repeated side effects).
  2. Handle the event.
  3. Persist the id only after success — a failed delivery stays eligible for retry.

A concurrent duplicate collides on the primary key (P2002), which is exactly the “already processed” outcome and is swallowed safely.

Events handled

EventAction
session.createdRecord a sign-in (SignInEvent, upsert by clerkSessionId)
user.created · user.updatedSync that user from Clerk into the local mirror
user.deletedDelete the local user (ignored if it doesn’t exist locally)
anything elseIgnored
Last updated on