---
title: "Test Clerk Webhooks Locally | WebhookRelay"
meta:
  "og:description": "Test Clerk webhooks locally and receive them on localhost without deploying. Inspect the real payload, forward to your handler, and verify the Svix signature."
  "og:title": "Test Clerk Webhooks Locally"
  description: "Test Clerk webhooks locally and receive them on localhost without deploying. Inspect the real payload, forward to your handler, and verify the Svix signature."
---

![Stripes](https://webhookrelay.com/blog/receive-clerk-webhooks-locally/images/stripes.svg)

# **Test Clerk Webhooks Locally**

Test Clerk webhooks locally and receive them on localhost without deploying. Inspect the real payload, forward to your handler, and verify the Svix signature.

![Test Clerk Webhooks Locally (Receive Clerk Webhooks on localhost)](https://webhookrelay.com/blog/receive-clerk-webhooks-locally/images/blog/heroes/localhost.jpg)

You are building a Clerk integration — provisioning a user record when someone signs up, syncing profile changes, cleaning up when an account is deleted — and you need to see your handler react to a real `user.created` or `session.created` event. The problem is immediate: Clerk will only POST to a **public URL**, and your handler is running on `localhost:3000`. Clerk has no way to reach it.

The usual workarounds are painful. Deploying to a staging server for every code change is slow. Copying a sample payload from the docs into curl gives you a _guess_ at the real request, not the real headers and body Clerk actually sends — and you cannot test signature verification at all without the real Svix headers. What you want is to **test Clerk webhooks locally** — real events, hitting your local handler, with a URL that does not change every time you restart.

This guide shows how to do exactly that.

## [Why receiving Clerk webhooks locally is tricky](#why-receiving-clerk-webhooks-locally-is-tricky)

A webhook is just an HTTP request that Clerk sends to a URL when something happens — a user is created, updated, or deleted; a session starts; an organization changes. Clerk sits on the public internet; your dev machine usually does not. It is behind a router, a corporate firewall, or both, with no public IP and no inbound ports open.

So you need something in the middle: a public endpoint Clerk can hit, that relays each request down to your laptop without you opening a single firewall port. That is what [Webhook Relay](https://my.webhookrelay.com/register) does — and unlike a random tunnel URL, the endpoint is **stable**, so you configure Clerk once and never touch it again.

## [Step 1: Inspect the real payload with Webhook Bin](#step-1-inspect-the-real-payload-with-webhook-bin)

Before you write any handler code, find out what Clerk actually sends. Open the free [Webhook Bin](https://webhookrelay.com/blog/receive-clerk-webhooks-locally/webhook-bin/) — no signup — and you get an instant public URL.

1. Copy the Webhook Bin URL.
2. In the [Clerk Dashboard](https://clerk.com), go to **Webhooks → Add Endpoint**.
3. Paste the URL into **Endpoint URL**, subscribe to the events you care about (for example `user.created`, `user.updated`, `session.created`), and create the endpoint.
4. Use the dashboard's **Send example** / **Testing** tab to fire a test event, or trigger a real one by signing up a user.

The request lands in Webhook Bin right away. Inspect the captured request: the full JSON body (for `user.*` events the payload is the User object), and every header, including the Svix trio — `svix-id`, `svix-timestamp`, and `svix-signature`.

Now you know the exact shape of the data before writing a line of code. For more on this approach, see [How to test webhooks](https://webhookrelay.com/blog/receive-clerk-webhooks-locally/blog/how-to-test-webhooks/) and [What is a webhook](https://webhookrelay.com/blog/receive-clerk-webhooks-locally/blog/what-is-webhook/).

## [Step 2: Forward the events to localhost with the relay agent](#step-2-forward-the-events-to-localhost-with-the-relay-agent)

Once you know the payload, route those same events into your local handler. [Sign up for Webhook Relay](https://my.webhookrelay.com/register), install the `relay` agent (CLI or Docker), and create a bucket — say `clerk`. The bucket gives you a stable public input endpoint.

Start forwarding to your local server:

```
relay forward --bucket clerk http://localhost:3000/api/webhooks/clerk
```

The agent opens an **outbound** connection to Webhook Relay and streams every incoming request down to your local route. Because the connection is outbound, there are **no firewall ports to open and no public IP needed** — this works from your laptop, behind a corporate proxy, or inside a Kubernetes cluster. Running in Docker? The same command works in the official `webhookrelay/webhookrelayd` image. Full details are in the [localhost forwarding docs](https://webhookrelay.com/blog/receive-clerk-webhooks-locally/docs/webhooks/internal/localhost/).

Now set the Clerk endpoint's URL to your Webhook Relay endpoint (or just create it there from the start). Sign up a test user and watch it arrive on `localhost`.

## [Clerk-specific configuration and quirks](#clerk-specific-configuration-and-quirks)

A few Clerk details worth knowing:

- **Where to add it:** **Clerk Dashboard → Webhooks → Add Endpoint**. Each endpoint has its own URL and its own signing secret.
- **Subscribe to events explicitly:** Clerk does not send everything by default. Choose the events you want — `user.created`, `user.updated`, `user.deleted`, `session.created`, `organization.created`, and so on. Branch on the `type` field in the body.
- **The payload wraps the object:** the body has a `type` (the event name), a `data` object (the resource — for `user.*` events that is the full User object), and an `object` field. Read `data` for the entity, `type` to decide what to do.
- **Signing secret:** each endpoint shows a **Signing Secret** (it starts with `whsec_`). You will need it for verification — store it as `CLERK_WEBHOOK_SIGNING_SECRET`.
- **It is Svix under the hood:** Clerk uses Svix to deliver webhooks, which is why you see `svix-*` headers rather than a single Clerk-specific signature header. Svix also handles retries, so build your handler to be idempotent (the `svix-id` is a stable delivery ID you can dedupe on).

## [Step 3: Verify the Clerk webhook signature](#step-3-verify-the-clerk-webhook-signature)

Clerk's webhooks are signed by Svix. Each request carries `svix-id`, `svix-timestamp`, and `svix-signature`. The signature is a **base64-encoded HMAC-SHA256** computed over the concatenation of the `svix-id`, the `svix-timestamp`, and the **raw** request body, using your endpoint's signing secret. The `svix-signature` header can contain multiple space-separated signatures, so compare against each.

The strongly recommended approach is to **not** hand-roll this. Use the `svix` library's `Webhook(secret).verify(payload, headers)`, or Clerk's framework helper `verifyWebhook(req)` (which reads `CLERK_WEBHOOK_SIGNING_SECRET` automatically and throws on a bad signature). These check the timestamp window and the HMAC for you. Skipping verification — even for a notification-only handler — leaves the endpoint open to spoofed events.

To understand what is happening under the hood, paste a captured body, your signing secret, and the received signature into the free [HMAC signature verifier](https://webhookrelay.com/blog/receive-clerk-webhooks-locally/hmac-verification/). For language-specific code and the common pitfalls (reading the body after a JSON parser has consumed it, the signed payload being `id.timestamp.body`, timing-safe comparison), read [Verify a webhook signature](https://webhookrelay.com/blog/receive-clerk-webhooks-locally/blog/verify-webhook-signature/).

## [Replay and iterate](#replay-and-iterate)

This is where local development gets fast:

- **Replay from Clerk** — the Webhooks dashboard keeps a delivery log and lets you **resend** a past message, which re-runs it against your handler.
- **Replay from Webhook Relay** — past requests are stored on your bucket, so you can resend a captured event without touching Clerk at all. The `svix-*` headers are preserved, so signature verification still works on replay.
- **Iterate on your handler** by editing code and replaying the same delivery until it behaves correctly. No commits, no pushes, no deploys just to test a code path.

Because the Webhook Relay endpoint is stable, you can stop and restart the agent, reboot your machine, or come back next week — the Clerk configuration never needs to change.

## [Get started](#get-started)

1. Inspect the real payload in the free [Webhook Bin](https://webhookrelay.com/blog/receive-clerk-webhooks-locally/webhook-bin/) — no signup needed.
2. [Create a Webhook Relay account](https://my.webhookrelay.com/register), install the agent, and run `relay forward --bucket clerk http://localhost:3000/api/webhooks/clerk`.
3. Point your Clerk webhook endpoint at the stable URL, sign up a test user, and watch it hit `localhost`.

You will be testing real Clerk events against your local handler in a few minutes — no deploys, no open firewall ports, and a URL you configure exactly once.