← All guides
ClerkJuly 12, 20266 min read

Test Clerk Webhooks on Localhost Without Deploying

Receive, route, and transform Clerk webhooks on your clerk webhook localhost setup with Relayers. Sync users into your DB with Svix verification intact.

You wired up Clerk for auth, and now you need your own database to know when a user signs up. The clean way to do that is a webhook: Clerk fires user.created, your backend catches it, and you upsert a row. Simple, until you try to build the handler on your laptop.

Clerk can only deliver webhooks to a public URL. Your handler lives on localhost:3000. So you deploy a half-finished endpoint to staging, click around in Clerk, tail remote logs, tweak one line, redeploy, and repeat. Every iteration costs minutes. And because Clerk signs payloads with Svix, you can't just fake a request with curl — you need the real headers to test your verification code.

Relayers closes that gap. It gives you a public ingest URL, forwards the original request untouched down a WebSocket tunnel to your machine, and lets you filter and reshape the payload before it ever hits your code. Signature verification keeps working because the bytes and headers arrive exactly as Clerk sent them.

What you'll build

Prerequisites

1Create an endpoint in Relayers

Open the Relayers desktop app and go to Endpoints → Create. Name it something like clerk-dev. Relayers generates a public ingest URL that looks like:

https://api.relayers.app/v1/webhooks/wep_abc123

Copy it. That wep_ identifier is your endpoint's public ID — it's the only address Clerk needs to know about. Use the Send test button now if you want to confirm the endpoint is live before touching Clerk.

2Add the endpoint in Clerk

In the Clerk Dashboard, go to Webhooks → Add Endpoint. Paste your Relayers URL into Endpoint URL, then subscribe to the events you care about. For user sync, start with:

You might also want session.created or organization.created depending on your app. Save the endpoint.

Under the hood, Clerk uses Svix to deliver webhooks. That means every request carries three signing headers:

svix-id: msg_2abc...
svix-timestamp: 1720000000
svix-signature: v1,g0h1...

Clerk also shows you a Signing Secret (it starts with whsec_). Copy that into your local environment — your handler uses it, together with the svix-* headers, to verify that the request genuinely came from Clerk. Relayers forwards all three headers unchanged, so this verification works identically on localhost and in production.

3Forward events to localhost

Log in and start the tunnel from your terminal:

relayers login
relayers listen --forward localhost:3000

That's it. Relayers now pipes anything hitting wep_abc123 straight to your local dev server, preserving the method, path body, and every header. Trigger a real event by signing up a test user in your app, or hit Replay in the Clerk dashboard on a past delivery.

Watch the request land in the Relayers Events view. Open the payload inspector to see the full body and headers — including the svix-* set — exactly as your handler receives them.

4Route and filter with JQ

Clerk sends every subscribed event to the same URL, with the event name in the type field. If your user.created handler only cares about new users, filter noise out at the edge instead of in your code.

In the Relayers Rules tab, add a rule with a JQ filter:

{ "type": "user.created" }

Set the destination to your tunnel (localhost:3000). Rules match top to bottom and first match wins, so you can layer more specific routes above general ones and reorder them by dragging. For example, route user.created and user.updated to your sync handler, and send session.created somewhere else entirely.

5Transform the payload

Clerk's payloads are deep — data.email_addresses is an array, IDs are nested, and there's a lot you don't need. Instead of writing mapping code in every handler, reshape the event once in the Transformations tab.

Here's a JQ transform that flattens a user.created event into exactly what your DB upsert needs:

{
  event: .type,
  userId: .data.id,
  email: .data.email_addresses[0].email_address
}

Relayers supports jq, jsonata, javascript, and go_template, and the built-in test playground lets you paste a sample Clerk payload and see the output before you save. Your local handler now receives a tidy, predictable shape.

One caveat: if you transform the body, the payload no longer matches the original svix-signature. During development that's usually fine — you're testing your DB logic, not the signature. When you need to verify signatures, forward the untouched body (skip the transform, or keep signature verification and transformation on separate endpoints).

Signature verification, unchanged

This is the part that trips people up with other tunneling tools. Relayers forwards the original request body and headers untouched, so the svix-id, svix-timestamp, and svix-signature headers arrive byte-for-byte. Your Svix verification code — the same code you'll run in production — passes locally against real, signed events. No mocking, no bypass flags, no "it works on my machine but breaks in prod" surprises.

Going to production

When you ship, you don't need Relayers in the request path at all — point Clerk directly at your deployed URL. But keep the Relayers endpoint around: it's the fastest way to reproduce a production bug locally. Use Import/Export to save your endpoint, rules, and transformations as a JSON bundle so a teammate can spin up the identical setup in seconds.

If you prefer to keep Relayers in production as a routing layer, swap the tunnel destination for a public URL in the rule and you're done — same filters, same transforms.

Troubleshooting

Wrap-up

Syncing Clerk users shouldn't mean redeploying to test one line. With a Relayers endpoint in front of your Clerk webhook, you get a public URL, a tunnel to localhost, JQ routing, payload transforms, and — critically — Svix signatures that verify locally exactly as they will in production.

Create your first endpoint at relayers.app and download the app to start catching Clerk events on localhost in minutes.