Skip to content
TryWebhook
New URL

Authentication · Free · No signup

Test and debug Clerk webhooks

Point a Clerk endpoint at a throwaway URL, create a test user, and read the delivery as it arrived — including the three Svix headers whose values are all folded into the signed payload.

No signup · No email · Ready in about a second

At a glance

Clerk webhooks in brief

Category
Authentication
Where to configure
Clerk Dashboard → Configure → Webhooks → Add Endpoint
Signature header
svix-signature
Secret
Signing Secret (starts with `whsec_`)
Retries
Svix's schedule — immediately, then after 5 seconds, 5 minutes, 30 minutes, 2 hours, 5 hours, and twice more at 10-hour intervals. Roughly a day in total, and every attempt is visible and replayable in the Clerk dashboard.
Response deadline
15 seconds.
Treated as success
Any 2xx. Anything else, including a 3xx, is a failed attempt.

Cross-checked against Clerk's own webhook documentation. Providers change these; if something here is stale, tell us.

The payload

What Clerk actually sends

Shape-accurate, with obviously fake identifiers. These are the same samples the inspector's Send test webhook panel fires, so you can read one here and then send the identical bytes to your own URL.

POST/w/{your-bin-id}255 B

The id and timestamp headers are part of the signed payload.

3 headers
webhook-id
msg_2fakeMessageIdExample
webhook-timestamp
1750000000
user-agent
Svix-Webhooks/1.0
Body · application/json · 1 line
{"type":"user.created","timestamp":"2026-06-15T10:04:00Z","data":{"id":"user_2fakeUserId","email_addresses":[{"email_address":"buyer@example.com","verification":{"status":"verified"}}],"first_name":"Ada","last_name":"Lovelace","created_at":1750000000000}}

No sample carries a signature header. A valid one can only be produced with your own signing secret, and a fabricated one would make the Signature tab report a mismatch that isn't real. Send a genuine delivery when you want to test verification.

Verification

Verifying a Clerk signature

Signed payload

A space-separated list of versioned signatures, each `v1,<base64>`. The signed string is `{svix-id}.{svix-timestamp}.{raw body}`, and the HMAC key is the base64-decoded portion of the secret after the `whsec_` prefix.

Implemented as: HMAC-SHA256 over `{id}.{timestamp}.{raw body}`, base64, key = base64-decoded secret.

Built in. Open any captured request, switch to the Signature tab, pick Svix / Standard Webhooks, and paste your signing secret (starts with `whsec_`). The HMAC is computed in your browser with the Web Crypto API over the raw bytes that arrived — the secret is never sent to us, and never written to disk.

Events

Events worth subscribing to first

Not the full catalogue — the handful that carry most integrations. Subscribing to everything is the fastest way to a handler that times out.

EventFires when
user.createdA user finished signing up. The event to create your own user row from — but see the note below about it racing your sign-up redirect.
user.updatedProfile, email, or metadata changed. Also fires when a user verifies an email address, which is often the state change you actually care about.
user.deletedA user was deleted. The payload is a stub — `id` and `deleted: true` — so anything you need about them has to already be in your database.
session.createdA user signed in. Useful for audit logs and last-seen tracking; too noisy for anything else.
organizationMembership.createdSomeone joined an organisation. The event B2B apps use to grant workspace access.

Adding a test endpoint

Create a URL above, then open Clerk Dashboard → Configure → Webhooks → Add Endpoint. Paste the URL, choose the events you want, and save. Clerk shows the Signing Secret for that endpoint — a whsec_ string — on the endpoint’s page.

Nothing is delivered on creation. To produce an event, either sign up a user in your development instance, or use the endpoint’s Testing tab, which sends a synthetic payload of the type you pick with a genuine signature. That second option is what you want while you are getting verification working: it is repeatable and it does not litter your user list.

The three headers

svix-id: msg_2fakeMessageIdExample
svix-timestamp: 1750000000
svix-signature: v1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE=

All three are load-bearing. svix-id identifies the message and is stable across retries — it is your idempotency key. svix-timestamp is a Unix timestamp. And both of them, not just the body, go into the string that gets signed.

The sample above shows the same headers without the svix- prefix — webhook-id, webhook-timestamp, webhook-signature — because that is what the Standard Webhooks specification calls them, and what newer implementations such as Supabase send. Clerk uses the prefixed names. The verifier here accepts either spelling, and so should yours.

Verifying the signature, step by step

This scheme has more moving parts than most, and each step has a common failure mode.

1. Build the signed content. Three values joined by literal dots:

msg_2fakeMessageIdExample.1750000000.{"type":"user.created","timestamp":"2026-06-15T10:04:00Z",...}

2. Derive the key. Take the secret, drop the whsec_ prefix, and base64-decode the remainder into bytes:

const key = base64Decode(secret.replace(/^whsec_/, ''));

This is the step that catches everyone. The secret is a base64 encoding of the key, not the key itself. Signing with the ASCII string produces a valid-looking digest that simply never matches.

3. HMAC-SHA256 and base64-encode. Compare against each v1, entry in the header, splitting on spaces. Multiple entries appear during a rotation; accept if any matches, in constant time.

4. Check the timestamp. Standard Webhooks recommends a five-minute tolerance in either direction. The signature does not expire on its own.

In the Signature tab above, choose Svix / Standard Webhooks and paste the whsec_ secret — the decoding and the dot-joining are done for you, so a pass here and a failure in your code narrows the problem to those two steps.

The race nobody expects

The usual plan is: user signs up, user.created fires, your handler writes a row, the user lands on the dashboard. In practice those last three happen at the same time, and the browser often wins. The user’s first authenticated request arrives before the row exists, and your app 500s on a new signup — intermittently, and never in local testing where the webhook cannot reach you at all.

The robust shape is to make row creation idempotent and reachable from both directions:

async function ensureUser(clerkId: string) {
  return db.users.upsert({ where: { clerkId }, create: { clerkId }, update: {} });
}

Call it from the webhook and from your session middleware. The webhook then becomes what it should be — a way to receive profile updates and deletions — rather than a prerequisite for the app working at all.

Reading the payload

{
  "type": "user.created",
  "data": {
    "id": "user_2fakeUserId",
    "email_addresses": [{ "email_address": "buyer@example.com", "verification": { "status": "verified" } }],
    "first_name": "Ada"
  }
}

email_addresses is an array, and the primary one is identified by primary_email_address_id on the user rather than by position. Taking email_addresses[0] works until a user adds a second address and it does not.

public_metadata, private_metadata, and unsafe_metadata all come through. Only public_metadata is readable from the frontend; unsafe_metadata is writable by the user, so never trust it for anything that grants access.

Failure modes

Where Clerk integrations usually break

The secret must be base64-decoded first

A Clerk signing secret looks like `whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw`. The HMAC key is not that string — it is the base64 decoding of everything after `whsec_`. Signing with the literal string is the single most common Svix verification failure, and it produces a mismatch that looks exactly like a wrong secret.

All three headers are part of the signature

`svix-id`, `svix-timestamp`, and the body are joined with literal dots: `{id}.{timestamp}.{body}`. Verifying the body alone will never match, and dropping the header on the way through a proxy breaks it just as thoroughly.

The signature is base64, not hex

Each entry is `v1,` followed by a base64 digest. There can be several, space-separated, during a secret rotation — accept the request if any of them matches.

user.created races your redirect

The webhook and the browser redirect after sign-up happen concurrently. Your app can receive the user's first authenticated request before the webhook that was supposed to create their row. Handle a missing row by creating it on demand, and treat the webhook as a backstop rather than the source of truth.

Each instance has its own secret

Development, staging, and production Clerk instances are separate, with separate endpoints and separate signing secrets. So is every endpoint within one instance — two endpoints on the same instance do not share a key.

Answers

Clerk webhook questions

Does Clerk use Svix?+

Yes. Clerk's webhooks are delivered by Svix and follow the Standard Webhooks specification, which is why the headers are `svix-*` and why Svix's own libraries verify them unchanged. Supabase Auth Hooks and several other products use the same scheme.

Can I verify a Clerk signature here?+

Yes. Choose Svix / Standard Webhooks in the Signature tab and paste the `whsec_` secret. The base64 decoding of the key and the `{id}.{timestamp}.{body}` construction are both handled, which are the two steps hand-rolled verifiers get wrong.

How do I test a Clerk webhook without signing up a user?+

The Clerk dashboard has a Testing tab on each endpoint that sends a synthetic event of any type, with a real signature. It is the fastest way to check your verification code. Past deliveries can also be replayed from the Messages list.

Why is my user.created handler running twice?+

Either Svix retried after your 200 was lost, or you have two endpoints subscribed to the same event. `svix-id` is stable across retries of one message — store it, and make the handler idempotent on the Clerk user id.

What arrives in a user.deleted payload?+

Very little: the user id, the object type, and `deleted: true`. Clerk has already removed the record, so if you need their email to send a farewell or to clean up a third-party service, you must have stored it earlier.

Other providers

Testing something else?

One click

Point Clerk at a URL and watch the payload land.

Clerk Dashboard → Configure → Webhooks → Add Endpoint — paste the URL, trigger an event, and read exactly what arrived.

No signup · No email · Ready in about a second