Test Clerk webhooks without deploying

Capture user and organisation lifecycle events from Clerk, which delivers through Svix and signs with svix- headers.

Get a URL that accepts the webhook#

A capture URL takes any method, any body, and records exactly what arrived: method, headers, query string, body and source IP. You do not need an account to make one, and an anonymous URL is deleted after 24 hours.

bash·No signup required
curl -X POST https://mocklane.com/api/v1/anon/endpoints \
  -H "Content-Type: application/json" -d '{}'

# The response carries a capture URL of the form:
#   https://mocklane.com/h/<endpoint-id>

Paste that URL into Clerk at Clerk dashboard, Webhooks, Add Endpoint, then trigger something. Events worth starting with:

Verify the svix-signature header#

Clerk signs each request so you can tell a real delivery from anyone who has guessed your URL. The digest is HMAC-SHA256 over the message id, the timestamp and the raw body, joined with full stops, base64-encoded, keyed with your signing secret (starts with whsec_).

Header
svix-signature
Timestamp header
svix-timestamp
Value format
v1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE=
Algorithm
HMAC-SHA256, base64
Signed content
the message id, the timestamp and the raw body, joined with full stops
javascript·Verify in Node
const crypto = require("crypto");

// Svix signs "{id}.{timestamp}.{rawBody}".
const signed = `${req.headers["svix-id"]}.${req.headers["svix-timestamp"]}.${rawBody}`;

// The key is the DECODED secret, not the whsec_ string itself.
const key = Buffer.from(process.env.CLERK_WEBHOOK_SECRET.replace(/^whsec_/, ""), "base64");
const expected = crypto.createHmac("sha256", key).update(signed).digest("base64");

// svix-signature can carry several space-separated "v1,<sig>" values during
// a secret rotation. Any one of them matching is a pass.
const ok = req.headers["svix-signature"]
  .split(" ")
  .some((part) => {
    const sig = part.split(",")[1];
    return sig && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig));
  });
Compare digests with a constant-time function such as crypto.timingSafeEqual. A plain === returns as soon as two bytes differ, and that timing difference is enough to recover a valid signature one byte at a time.

What catches people out#

Replay it until your handler is right#

A captured request can be re-sent to your own endpoint, with the body or headers edited first. That turns a bug that needed a real Clerk event into something you can reproduce on demand, which matters most for events that are awkward to trigger twice.

Assert on it in CI#

A scenario chains the steps into one run that passes or fails, so a change that breaks your Clerk handling fails the build rather than reaching production.

yaml·scenario.yaml
steps:
  - type: send_webhook
    body: { event: "user.created" }

  - type: wait_for_webhook
    timeout_seconds: 30
    assert:
      - status_code equals 200

Triggered from CI with an API token. See the scenarios documentation for the full step list.

Everything above is on the free plan, and the capture URL works before you sign up. Create a free account to keep a URL for longer than 24 hours.

Signature details checked against Clerk’s documentation on 22 October 2026. Schemes change: if this page and their docs disagree, believe theirs and tell us.