Gareth Wilson Gareth Wilson

Guide to Clerk Webhooks: Features and Best Practices

Published


Clerk webhooks notify your application when something changes in your user authentication system: a user signs up, a session starts, an organization adds a member. If you're building on Clerk's user management platform, webhooks are how you sync users to your database, send welcome emails, and provision resources without polling.

This guide covers how Clerk webhooks work, the Svix-powered signature scheme and its raw-body requirement, the event envelope and catalog, the delivery semantics that shape your handler, and the best practices for production.

What are Clerk webhooks?

Clerk delivers webhooks as HTTP POSTs to endpoints you register in the Clerk Dashboard. Delivery is handled by Svix, which brings automatic retries, signature verification, and comprehensive logging. Requests follow the Standard Webhooks protocol, with one naming quirk: Clerk sends the headers as svix-id, svix-timestamp, and svix-signature rather than the spec's webhook-* names (the format is otherwise identical).

Clerk webhook features

FeatureDetails
ConfigurationClerk Dashboard, Webhooks section: add an endpoint URL and select the events to receive
VerificationHMAC-SHA256 over svix-id.svix-timestamp.rawBody, base64-encoded, sent in svix-signature as v1,<signature> (possibly several, space-separated); secret format whsec_<base64>
Envelopedata, object, type, instance_id, and timestamp (milliseconds since epoch)
EventsUser, session, organization, membership, and invitation events; full catalog in the Dashboard's Event Catalog
RetriesFailed deliveries are retried with exponential backoff for up to 3 days; 2xx acknowledges, 4xx or 5xx triggers a retry
ReplayFailed webhooks can be replayed from the Clerk Dashboard
DeliveryAt-least-once, with no ordering guarantee
Replay protectionReject deliveries whose svix-timestamp is older than 5 minutes
IP allowlistingOptional, against Svix's published IP addresses
SDK verificationverifyWebhook(request) from @clerk/backend/webhooks (Next.js); the standardwebhooks npm package (Express)

Common events

Clerk event names are the type field values. The ones most integrations start with:

EventFires when
user.createdA new user signs up
user.updatedA user's profile or metadata changes
user.deletedA user account is removed
session.createdA user signs in
session.endedA user signs out
session.removedA session is revoked
organization.createdA new organization is created
organization.updatedOrganization settings change
organizationMembership.createdA user joins an organization
organizationInvitation.createdAn invite is sent to join an organization

Branch on the type field. If you sync users to your own database, subscribe to all three user events: handling only user.created leaves updates and deletions unreflected and is a common cause of stale or duplicate records. The full event reference lives in Clerk's docs and in the Dashboard's Event Catalog tab.

See Clerk webhook payloads in action. Inspect and replay sample Clerk webhook payloads in the Hookdeck Console — no account or setup required.

Setting up Clerk webhooks

Registration happens in the Clerk Dashboard:

  1. Go to the Clerk Dashboard and select your application.
  2. Navigate to Webhooks in the left sidebar and click Add Endpoint.
  3. Enter your endpoint URL, for example https://yourdomain.com/webhooks/clerk.
  4. Select the events to receive: user events (user.created, user.updated, user.deleted), session events (session.created, session.ended, session.removed), and organization events if you use them.
  5. Click Create, open the endpoint's details, and copy the Signing Secret (it starts with whsec_).

Store the secret in your environment as CLERK_WEBHOOK_SIGNING_SECRET (the name used by @clerk/nextjs and Clerk's docs; CLERK_WEBHOOK_SECRET appears as an alternative in some examples), and never commit it to source control. Two more setup details matter: the webhook route must be public, so exclude it from clerkMiddleware() or any auth middleware, and you can confirm everything works with the Dashboard's Send test event feature.

For local development, the Hookdeck CLI (hookdeck listen 3000 clerk --path /webhooks/clerk) gives you a public URL that forwards to your local server, no account needed. Use the tunnel URL when adding your endpoint in the Clerk Dashboard, then switch to your live URL and production signing secret when you deploy.

Securing Clerk webhooks

Every delivery carries three headers: svix-id (unique message identifier), svix-timestamp (Unix seconds), and svix-signature (one or more base64 HMAC-SHA256 signatures in v1,<signature> format, space-separated). The signed content is svix-id.svix-timestamp.rawBody, keyed with the base64-decoded portion of your whsec_ secret. Because the signature covers the exact bytes Clerk sent, verification needs the raw request body: a parsed and re-serialized JSON body will fail.

The skill recommends the standardwebhooks npm package for Express, mapping Clerk's svix-* headers to the webhook-* names the library expects:

const express = require("express");
const { Webhook } = require("standardwebhooks");

const app = express();

// Use express.raw() for the webhook route: verification needs the raw body
app.post(
  "/webhooks/clerk",
  express.raw({ type: "application/json" }),
  (req, res) => {
    const secret = process.env.CLERK_WEBHOOK_SIGNING_SECRET;
    if (!secret || !secret.startsWith("whsec_")) {
      return res.status(500).json({ error: "Server configuration error" });
    }

    // Clerk sends svix-* headers; standardwebhooks expects webhook-*
    const headers = {
      "webhook-id": req.headers["svix-id"],
      "webhook-timestamp": req.headers["svix-timestamp"],
      "webhook-signature": req.headers["svix-signature"]
    };

    let event;
    try {
      const wh = new Webhook(secret);
      event = wh.verify(req.body, headers);
    } catch (err) {
      return res.status(400).json({ error: "Webhook verification failed" });
    }

    switch (event.type) {
      case "user.created":
        console.log("User created:", event.data.id);
        break;
      case "session.created":
        console.log("Session created:", event.data.user_id);
        break;
      default:
        console.log("Unhandled event:", event.type);
    }

    res.status(200).json({ success: true });
  }
);

In Next.js App Router, use verifyWebhook(request) from @clerk/backend/webhooks and pass the request directly; it reads CLERK_WEBHOOK_SIGNING_SECRET. If you verify manually, use a timing-safe comparison, check every signature in the header (Svix can send several), and reject timestamps older than 5 minutes to prevent replays. Access headers in lowercase, since some frameworks capitalize them.

Make Clerk webhooks production-ready. Hookdeck Event Gateway verifies Svix signatures upstream, deduplicates, and durably queues every event.

Clerk webhook limitations and pain points

Signature verification demands the raw body

The Problem: Verification fails whenever the handler sees a parsed body, and the failure mode is an opaque "invalid signature" error that looks like a wrong secret.

Why It Happens: The HMAC covers the exact bytes of svix-id.svix-timestamp.rawBody. Framework body parsers (Express's express.json(), Next.js's default body parsing) consume the raw bytes before your code runs, and re-serializing the parsed JSON produces different bytes.

Workarounds:

  • Use express.raw({ type: "application/json" }) on the webhook route in Express, disable body parsing in Next.js Pages Router (export const config = { api: { bodyParser: false } }), or read await request.body() in FastAPI.
  • Check the other usual suspects in order: all three svix-* headers present, secret matches exactly and starts with whsec_, headers accessed in lowercase.

How Hookdeck Can Help: Hookdeck verifies the Svix signature at the edge and logs every request's exact headers and body, so a verification failure is visible and diagnosable instead of a silent 400.

Duplicate and out-of-order events

The Problem: The same event can arrive more than once, and events can arrive out of order, so a handler that blindly inserts on user.created produces duplicate rows and one that assumes lifecycle order breaks.

Why It Happens: Delivery is at-least-once with no ordering guarantee, and failed deliveries are retried with exponential backoff for up to 3 days, so a delayed retry can land long after newer events.

Workarounds:

  • Make handlers idempotent: upsert keyed on the Clerk object's id rather than inserting.
  • Handle user.updated and user.deleted alongside user.created so your database converges on the current state regardless of arrival order.

How Hookdeck Can Help: Deduplication rules drop repeats at the edge before they reach your handler. See our guide to webhook idempotency.

Slow handlers turn into timeouts and retry noise

The Problem: A handler that does heavy work inline (provisioning resources, calling third-party APIs) responds slowly, gets treated as failed, and the same event comes back again as a retry.

Why It Happens: A 2xx response acknowledges the delivery; anything else, including a timeout, triggers Svix's retry schedule. The skill's guidance is to keep webhook processing under 5 seconds and move heavy operations to background jobs.

Workarounds:

  • Verify, enqueue, and return 200 immediately; do the real work in a background job.
  • Monitor for repeated failures so a persistently failing endpoint gets fixed before retries are exhausted.

How Hookdeck Can Help: Hookdeck acknowledges Clerk in milliseconds and retries delivery to your handler on its own schedule, with a max delivery rate to keep bursts from overwhelming your consumer.

Missing events with no obvious error

The Problem: Webhooks silently stop arriving, or specific event types never show up, and your database quietly drifts out of sync.

Why It Happens: The usual causes are mundane: the event type was never selected in the endpoint configuration, the webhook route is protected by auth middleware and returns 401, or the route path doesn't match what was registered (404). Clerk sends webhooks to a public route, so clerkMiddleware() protecting the path blocks every delivery.

Workarounds:

  • Exclude the webhook path from auth middleware and use one consistent path (such as /webhooks/clerk) in both code and Dashboard.
  • Check the Clerk Dashboard's webhook logs for delivery attempts and response codes, and replay failed webhooks from the Dashboard once fixed.

How Hookdeck Can Help: Issues alert you when deliveries start failing, and the full request history shows exactly which events arrived and how your endpoint responded.

Best practices

Verify every delivery before touching it

Verify the signature first and parse second. Use the library for your stack (verifyWebhook in Next.js, standardwebhooks in Express), keep the raw body available, reject stale timestamps, and never log the signing secret.

Return 200 fast, process asynchronously

Acknowledge as soon as verification passes and queue the real work. Slow responses become retries, and retries become duplicates. See why to process webhooks asynchronously.

Upsert on the object's id

At-least-once delivery guarantees you'll eventually see a duplicate. Key records on the Clerk object's id and upsert so replays and retries are harmless. Our guide to webhook idempotency covers the patterns.

Handle all three user events

Subscribing only to user.created leaves your copy stale as soon as a profile changes or an account is deleted. Handle user.updated and user.deleted too, so your database follows the full lifecycle.

Sync only what you need

Webhooks are eventually consistent, so sync when you need other users' data, custom fields, or integrations. When you only need the current user's data and Clerk already holds everything, read it from the session token instead of maintaining a copy.

Separate environments and use test events

Register separate endpoints for staging and production, each with its own signing secret, and use the Dashboard's test events to confirm your handler before real traffic arrives. Logging received webhooks makes later debugging much easier.

Conclusion

Clerk webhooks ride on Svix: HMAC-SHA256 signatures over the raw body, svix-* headers, at-least-once unordered delivery, and retries with exponential backoff for up to 3 days. Verify with the raw body, keep the route public, return 200 quickly, and upsert on the object's id so duplicates and reordering never corrupt your data.

Hookdeck Event Gateway verifies the Svix signature at the edge, absorbs delivery bursts, deduplicates, and retries your handler independently, so Clerk's retry schedule and your processing speed stop being coupled.

Get started with Hookdeck for free and handle Clerk webhooks reliably in minutes.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.