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.
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 devStart 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-appThe terminal prints HORIZON: Tunnel connected. Your public URL is https://my-app.hrzn.run.
Add the endpoint in Clerk
- Open the Webhooks page in the Clerk Dashboard.
- Select Add Endpoint.
- Enter
https://my-app.hrzn.run/api/webhooks/clerkas the URL. - Under Subscribe to events, select
user.created. - 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:
CLERK_WEBHOOK_SIGNING_SECRET=whsec_123Replace 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:
- Open your endpoint in the Clerk Dashboard and select the Testing tab.
- Select
user.createdin the dropdown. - Select Send Example.
- Check that Status shows "Succeeded" under Message Attempts.
Horizon prints one line for the request:
POST | [200] | /api/webhooks/clerkYour 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
- Read Getting started for the full CLI setup.
- Read Clerk's guide on syncing data with webhooks to store users in your database.