Skip to Content
Clerk logo

Clerk Docs

Ctrl + K
Go to clerkstage.dev

Sync Clerk data to your backend with webhooks

A common set up for applications involves a frontend for customers to interact with a backend that includes a database. Since authentication and user management happens on Clerk's side, data eventually needs to reach the application's backend.

The recommended way to sync data between Clerk and your application's backend is via webhooks. In this guide, you'll learn how to enable webhooks and how to set up your backend so that it is updated every time an event happens on your Clerk instance.

Given the asynchronous nature of webhooks, they might not fit in every use case out there but they are a great fit for most applications.

Enable webhooks

To enable webhooks, go to the Clerk Dashboard and navigate to the Webhooks page. Select the Add Endpoint button.

Add endpoint

You'll be presented with a form where you can specify the URL of your backend endpoint. This is the URL where Clerk will send the webhook events. You can also specify the events you want to receive. For example, if you only want to receive events related to users, you can select the user option.

If you are developing on your localhost, you will need to expose your endpoint to the internet to work with webhooks. See the Testing the webhook section for information.

Add endpoint form

Once you click the Create button, you'll be presented with your webhook endpoint dashboard. Here you can see the URL of your endpoint and the events you selected and you can also test your endpoint.

Add your Signing Secret to your .env.local file

To retrieve your Webhook Signing Secret, click on the Webhooks page in the side nav of the Clerk Dashboard.

The Webhooks page in the Clerk Dashboard. There is a red arrow pointing to where the Signing Secret is located.

You will need to set this value as an environment variable in your project. This guide uses WEBHOOK_SECRET as the key. However, you can set the key to whatever you like; just be sure to update the code examples.

.env.local
WEBHOOK_SECRET=your_signing_secret

Understanding the webhook payload

The Clerk webhook events are sent as HTTP POST requests with a JSON body. All messages contain:

  • data - an object that holds information for the event's payload.
  • object - this is always event
  • type - the type of webhook event. See Supported webhook events for a full list.

Below is an example of a webhook object with no payload:

{ "data": { // The event type specific payload will be here. }, "object": "event", "type": "<event>" }

Additionally messages contain an id string in the headers. To learn more about the payload structure, check out the webhooks reference.

Install the svix package

To get started setting up your endpoint, you will need to install the svix package. Svix provides a package for verifying the webhook signature, making it easy to verify the authenticity of the webhook events.

terminal
npm install svix
terminal
yarn add svix
terminal
pnpm add svix

Create the endpoint in your application

Create a webhook endpoint in the /api directory.

app/api/webhook/route.ts
import { Webhook } from 'svix' import { headers } from 'next/headers' import { WebhookEvent } from '@clerk/nextjs/server' export async function POST(req: Request) { // You can find this in the Clerk Dashboard -> Webhooks -> choose the webhook const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET if (!WEBHOOK_SECRET) { throw new Error('Please add WEBHOOK_SECRET from Clerk Dashboard to .env or .env.local') } // Get the headers const headerPayload = headers(); const svix_id = headerPayload.get("svix-id"); const svix_timestamp = headerPayload.get("svix-timestamp"); const svix_signature = headerPayload.get("svix-signature"); // If there are no headers, error out if (!svix_id || !svix_timestamp || !svix_signature) { return new Response('Error occured -- no svix headers', { status: 400 }) } // Get the body const payload = await req.json() const body = JSON.stringify(payload); // Create a new Svix instance with your secret. const wh = new Webhook(WEBHOOK_SECRET); let evt: WebhookEvent // Verify the payload with the headers try { evt = wh.verify(body, { "svix-id": svix_id, "svix-timestamp": svix_timestamp, "svix-signature": svix_signature, }) as WebhookEvent } catch (err) { console.error('Error verifying webhook:', err); return new Response('Error occured', { status: 400 }) } // Get the ID and type const { id } = evt.data; const eventType = evt.type; console.log(`Webhook with and ID of ${id} and type of ${eventType}`) console.log('Webhook body:', body) return new Response('', { status: 200 }) }
pages/api/webhook.ts
import { Webhook } from 'svix' import { WebhookEvent } from '@clerk/nextjs/server' import { NextApiRequest, NextApiResponse } from 'next' import { buffer } from 'micro' export const config = { api: { bodyParser: false, } } export default async function handler(req: NextApiRequest, res: NextApiResponse) { if (req.method !== 'POST') { return res.status(405) } // You can find this in the Clerk Dashboard -> Webhooks -> choose the webhook const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET if (!WEBHOOK_SECRET) { throw new Error('Please add WEBHOOK_SECRET from Clerk Dashboard to .env or .env.local') } // Get the headers const svix_id = req.headers["svix-id"] as string; const svix_timestamp = req.headers["svix-timestamp"] as string; const svix_signature = req.headers["svix-signature"] as string; // If there are no headers, error out if (!svix_id || !svix_timestamp || !svix_signature) { return res.status(400).json({ error: 'Error occured -- no svix headers' }) } console.log('headers', req.headers, svix_id, svix_signature, svix_timestamp) // Get the body const body = (await buffer(req)).toString() // Create a new Svix instance with your secret. const wh = new Webhook(WEBHOOK_SECRET); let evt: WebhookEvent // Verify the payload with the headers try { evt = wh.verify(body, { "svix-id": svix_id, "svix-timestamp": svix_timestamp, "svix-signature": svix_signature, }) as WebhookEvent } catch (err) { console.error('Error verifying webhook:', err); return res.status(400).json({ 'Error': err }) } // Get the ID and type const { id } = evt.data; const eventType = evt.type; console.log(`Webhook with and ID of ${id} and type of ${eventType}`) console.log('Webhook body:', body) return res.status(200).json({ response: 'Success' }) }

Add your endpoint to Middleware

Your Route Handler must be made public or ignored by Middleware to allow the request to succeed. The following example will make any webhooks created under app/api/webhooks/ public. See authMiddleware for more information.

middleware.tsx
import { authMiddleware } from "@clerk/nextjs"; export default authMiddleware({ publicRoutes: ["/api/webhooks(.*)"] }); export const config = { matcher: ['/((?!.+\\.[\\w]+$|_next).*)', '/', '/(api|trpc)(.*)'], };

Test the webhook

To test a webhook locally, you can use a tool like ngrok or localtunnel to expose your local server to the internet. You can then use the ngrok URL as the webhook URL in the Clerk Dashboard.

If you are testing webhooks outside of your development, you can use our webhook test requests to test your webhook. In the Clerk Dashboard, go to the Webhooks page, select the webhook endpoint you created, and then select the Testing tab. You can select an event from the Send Event dropdown to see the example payload and to send a test message to your application.

The Testing section of the Webhooks page in the Clerk Dashboard. The red arrows indicate how to navigiate to the section and select the event to test.

What did you think of this content?

Clerk © 2023