How the webhook URL works. It is a public address that records whatever is sent to it and
answers 200. An inbox keeps the 50 most recent requests, accepts bodies up to 256 KB, and is
deleted 24 hours after it was created — or sooner if you press Delete URL or our server
restarts, because requests are held in memory and never written to disk.
Anyone who has the URL can read what it received. Use sandbox and test-mode accounts, do not route production traffic or customer data through it, and delete it when you are done. Privacy details.
What is a Clerk webhook?
A Clerk webhook is an HTTP POST sent to an endpoint you registered for an application instance when something changes in it: a user signs up, updates their profile or is deleted, a session starts or ends, an organization or membership changes, an email or SMS is sent. Its usual job is keeping your own database in step with Clerk's user records.
The body is a compact envelope. type names the event, object is always event, and data is the resource in the same shape the Backend API returns. Delivery itself is handled by Svix, which is why the identifying and signing headers are prefixed svix- rather than clerk-.
What a Clerk webhook request looks like
The request line and headers as they reach your server, followed by the body. Ids, tokens and the signature are placeholders.
POST /your/webhook/path HTTP/1.1
Host: your-server.example
svix-id: msg_2abcEXAMPLE0000000000000001
svix-timestamp: 1767225600
svix-signature: v1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE=
Content-Type: application/json
User-Agent: Svix-Webhooks/1.0
{
"type": "user.created",
"object": "event",
"timestamp": 1767225600000,
"data": {
"id": "user_2abcEXAMPLE0000000000000001",
"object": "user",
"first_name": "Ada",
"last_name": "Lovelace",
"username": null,
"primary_email_address_id": "idn_2abcEXAMPLE0000000000000002",
"email_addresses": [
{
"id": "idn_2abcEXAMPLE0000000000000002",
"object": "email_address",
"email_address": "ada@example.com",
"verification": {
"status": "verified",
"strategy": "email_code"
}
}
],
"external_id": null,
"public_metadata": {},
"created_at": 1767225599000,
"updated_at": 1767225599000
}
}
A user has a list of email addresses, not a single field; the one to use is the entry whose id equals primary_email_address_id. Timestamps inside the body are in milliseconds, while the svix-timestamp header is in seconds. A user.deleted event carries only the id and a deleted flag, since the rest of the record no longer exists.
Headers Clerk sends
| Header | Example | What it is |
|---|---|---|
svix-id | msg_2abcEXAMPLE0000000000000001 | Unique id of the message. The same on every retry, so use it for de-duplication. |
svix-timestamp | 1767225600 | Unix time of this attempt, in seconds. Part of the signed string. |
svix-signature | v1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ… | One or more space-separated signatures, each v1, plus a base64 HMAC-SHA256. |
Content-Type | application/json | The body is JSON. |
User-Agent | Svix-Webhooks/1.0 | Identifies the delivery service. |
Header names are case-insensitive. Your framework may show them lower-cased, and HTTP/2 always does.
How a Clerk webhook is signed
Each endpoint has a signing secret that starts with whsec_. The part after that prefix is base64, and the HMAC key is the decoded bytes, not the text you see. The signed content is the message id, the timestamp and the raw body joined by full stops: {svix-id}.{svix-timestamp}.{body}. The HMAC-SHA256 of that, base64-encoded and prefixed v1,, is what appears in svix-signature.
The header can hold several signatures separated by spaces, which happens while a secret is being rotated; accept the request if any one matches. Reject timestamps more than about five minutes from your clock. Clerk's SDKs and the Svix libraries do all of this for you, and the function below shows what they do. Compute it over the raw request body exactly as received — before any JSON parsing — and compare with a constant-time function.
const crypto = require('crypto');
// rawBody: string of the body exactly as received
// headers: lower-cased request headers
function verifyClerk(rawBody, headers, secret, toleranceSec = 300) {
const id = headers['svix-id'];
const ts = headers['svix-timestamp'];
const given = String(headers['svix-signature'] || '').split(' ');
if (!id || !ts) return false;
if (Math.abs(Date.now() / 1000 - Number(ts)) > toleranceSec) return false;
const key = Buffer.from(secret.replace(/^whsec_/, ''), 'base64');
const expected = 'v1,' + crypto.createHmac('sha256', key)
.update(id + '.' + ts + '.' + rawBody).digest('base64');
const b = Buffer.from(expected);
return given.some((s) => {
const a = Buffer.from(s);
return a.length === b.length && crypto.timingSafeEqual(a, b);
});
}
Retries, ordering and the delivery log
Any 2xx within the timeout counts as delivered. Otherwise the message is retried on a fixed schedule that starts within seconds and stretches to gaps of several hours, ending roughly a day after the first attempt, per Svix's documentation at the time of writing. Every attempt keeps the same svix-id and gets a fresh timestamp and signature. An endpoint that fails continuously for days can be disabled.
Events are not guaranteed to arrive in order: a user.updated can overtake the user.created it follows. Write handlers as upserts keyed on the resource id so that either order ends in the same row. The dashboard keeps a log of each message and attempt, and a failed one can be replayed from there.
Catch a real Clerk webhook
- Press Create webhook URL at the top of this page and copy the address.
- In the Clerk Dashboard, select a development instance and open Webhooks.
- Add an endpoint, paste the address as the URL, and subscribe to events such as
user.created. - Open the endpoint's testing tab and send an example event, or sign up a test user in your development app.
- Read the three
svix-headers and the event body in the inbox above.
The endpoint's signing secret is shown on its page in the dashboard. Keep the webhook on a development instance while it points at a public inbox: the payloads contain names and email addresses.
To run a caught request against your own code, select it and press Open in API Tester: the method, headers and body are loaded into the request console, where you enter your server's public URL and send it. A body you edit will no longer match its signature — that is the signature doing its job.
Things that catch people out
- Decode the secret before using it. Keying the HMAC with the literal
whsec_…string is the classic reason a hand-written check never matches. - A webhook is not a sign-up hook. It arrives after the fact and possibly late; do not make the user's first page load depend on it having been processed.
- The same scheme, other senders. Several services deliver through Svix with identical headers, sometimes renamed to
webhook-id,webhook-timestampandwebhook-signature.
api-tester.com is independent and is not affiliated with Clerk. Providers change header names, limits and retry schedules; treat the official Clerk documentation as the source of truth.
Related examples and tools
Paddle Webhook Example
Paddle Billing notifications look a lot like Stripe's until you parse the signature header, which uses different separators and joins the timestamp to the body differently.
Razorpay Webhook Example
Razorpay wraps each event's entities inside payload under their own names, and signs the body with a secret you choose when creating the webhook.
Twitch Webhook Example
Twitch EventSub sends three kinds of message to the same URL, told apart by a header: a challenge to prove you own the endpoint, the notifications themselves, and a revocation when a subscription ends.
Zoom Webhook Example
Zoom will not save a webhook endpoint until it has answered a validation challenge with a hash made from the app's Secret Token, and it repeats that check from time to time.
WhatsApp Webhook Example
WhatsApp Cloud API webhooks come from Meta's shared webhook system: a GET challenge when you register the URL, then deeply nested JSON for every incoming message and every delivery status.
Base64 Encoder
Decode the part of a whsec_ secret after the prefix to see the raw key bytes.
Curl POST JSON
Send a JSON body like this one from a terminal.
Webhook Tester
The inbox on its own page, plus a guide to signatures, retries and idempotent handlers.
All Webhook Examples
30 providers side by side: content type, signature scheme and retry behaviour.
Other webhooks with a timestamp in the signature
References
- Official Clerk documentation — The provider's own guide to the requests described on this page.
- Clerk Docs: sync Clerk data with webhooks — A worked endpoint, from setup to verification.
- Svix Docs: verifying webhooks manually — The signature scheme, for when you are not using the library.
- Standard Webhooks — The open specification this header and signature format follows.
- RFC 2104: HMAC — The keyed-hash construction this signature is built on.
- Node.js: crypto.timingSafeEqual() — The constant-time comparison used in the code on this page.
These links leave api-tester.com, which is independent of the companies and projects they point to.
Clerk webhook FAQ
What does a Clerk webhook payload look like?
A JSON object with type (for example user.created), object set to event, a timestamp, and data containing the resource in the same shape as the Backend API returns it.
How do I verify a Clerk webhook?
Join the svix-id header, the svix-timestamp header and the raw body with full stops, compute an HMAC-SHA256 keyed with the base64-decoded part of the whsec_ secret, base64-encode the result and compare it with the v1 values in svix-signature.
What are the svix headers?
svix-id is the unique message id, svix-timestamp is the time of the attempt in seconds and svix-signature holds one or more v1 signatures. Clerk sends webhooks through Svix, which sets them.
Does Clerk retry failed webhooks?
Yes. A failed message is retried on a schedule of growing delays that ends about a day after the first attempt, according to Svix's documentation at the time of writing. Each retry has the same svix-id.
Which email address should I use from a Clerk user.created event?
Find the entry in data.email_addresses whose id equals data.primary_email_address_id and read its email_address field.