Horizon
GuidesWebhooks

Test Clerk webhooks locally

Receive Clerk webhook events on localhost with a Horizon tunnel, and verify them with verifyWebhook in a Next.js route.

Receive Clerk events on your laptop while you build, with a URL Clerk can reach.

Before you begin

  • Node.js 18 or later
  • A Horizon account and the CLI (see Getting started)
  • A Clerk application with a Next.js App Router app using @clerk/nextjs

Start your app

Create the route handler. Clerk's verifyWebhook helper checks the signature and returns the event. It throws when the check fails.

app/api/webhooks/clerk/route.ts
import { verifyWebhook } from '@clerk/nextjs/webhooks'
import { NextRequest } from 'next/server'

export async function POST(req: NextRequest) {
  try {
    const evt = await verifyWebhook(req)

    if (evt.type === 'user.created') {
      console.log('userId:', evt.data.id)
    }

    return new Response('Webhook received', { status: 200 })
  } catch (err) {
    console.error('Error verifying webhook:', err)
    return new Response('Error verifying webhook', { status: 400 })
  }
}

Clerk sends events without a signed-in user, so this route must be public. If your Clerk middleware protects routes, exclude /api/webhooks(.*).

Start the app:

npm run dev

Start a tunnel

Pass -s to pick the subdomain. Without it, the URL is random and changes on every run.

hrzn tunnel http://localhost:3000 -s my-app

The terminal prints HORIZON: Tunnel connected. Your public URL is https://my-app.hrzn.run.

Add the endpoint in Clerk

  1. Open the Webhooks page in the Clerk Dashboard.
  2. Select Add Endpoint.
  3. Enter https://my-app.hrzn.run/api/webhooks/clerk as the URL.
  4. Under Subscribe to events, select user.created.
  5. Select Create.

Add the signing secret

Open the endpoint you created on the Webhooks page and copy its signing secret. Add it to your .env file:

.env
CLERK_WEBHOOK_SIGNING_SECRET=whsec_123

Replace whsec_123 with your secret. verifyWebhook reads this variable by default. Restart your dev server so it picks up the change.

Check it works

Send a test event from Clerk:

  1. Open your endpoint in the Clerk Dashboard and select the Testing tab.
  2. Select user.created in the dropdown.
  3. Select Send Example.
  4. Check that Status shows "Succeeded" under Message Attempts.

Horizon prints one line for the request:

  POST | [200] | /api/webhooks/clerk

Your dev server logs userId: followed by the ID from the example event.

Troubleshooting

Horizon shows a non-200 status, or Clerk reports a failure

Your Clerk middleware may block the route. Clerk sends events without a signed-in user, so the route must be public. Exclude /api/webhooks(.*) from any auth check in your middleware.

The handler logs "Error verifying webhook" and returns 400

Check the signing secret first. It must match the one on your endpoint in the Clerk Dashboard, and your dev server must have restarted after you set CLERK_WEBHOOK_SIGNING_SECRET.

Events stop arriving after you restart the tunnel

You started it without -s, so the subdomain changed. Restart with -s and the subdomain from your endpoint URL. To keep a subdomain yours across restarts, reserve it (see Pricing).

Clerk reports a 404

The path in the endpoint URL must match your route: /api/webhooks/clerk.

Next steps

On this page