Handling webhooks with Inngest

Webhooks allow you to synchronize data from Clerk to your application backend. You can either handle them directly in your backend with an endpoint or use a tool like Inngest which receives the webhook events for you and reliably executes functions in your codebase. When handling webhooks, Inngest receives the webhook events for you and uses a built-in queue to reliably execute longer running functions with additional functionality including:

In this guide, you'll learn how to set up Inngest to receive Clerk webhook events and how to define Inngest functions in your application using Clerk events including synchronizing data and sending welcome emails.

To follow this guide, you need an Inngest account (free tier is enough) and have Inngest set up in your codebase.

Setting up the Inngest webhook

To create an Inngest webhook endpoint and add it to your Clerk account, open the Webhooks page at the Clerk dashboard. Next, select the Add Endpoint button.

The Webhooks page in the Clerk Dashboard. A red arrow points to the button for Add Endpoint.

On the next page, select the Transformation template tab and the Inngest template, then click on the Connect to Inngest button.

The Webhooks page in the Clerk Dashboard showing the Inngest transformation template. Red arrows point to the Transformation Template tab, the Inngest template, and the Connect to Inngest button.

A popup window will appear to complete the setup. Select Approve to create the webhook.

The Inngest permissions popup window showing the Approve button.

After the popup window disappears, the Webhooks page will now display Connected with the webhook URL underneath. There is one more step to complete setup.

The Webhooks page in the Clerk Dashboard showing a connected Inngest account. A red arrow points to the Connected button.

To complete the setup, scroll down and select Create.

The Webhooks page in the Clerk Dashboard showing the end of the page to create a new endpoint. A red arrow points to the Create button.

You'll be redirected to the new endpoint. In your Inngest dashboard, you will see a new webhook created in your account's production environment.

Viewing webhook events within Inngest

After setup, as webhook events are sent from Clerk to Inngest, new clerk events will appear in your Inngest dashboard. Event names will be the prefixed clerk/ followed by the event name. See webhook events for a full list.

The Events page in the Inngest Dashboard showing a list of Clerk events.

Creating a function to sync a new user to a database

With Inngest already set up in your codebase, you can now use these events as triggers for your functions.

Suppose you need to write a function which will insert a new user into the database which will be triggered whenever clerk/user.created event occurs. You would use the inngest.createFunction method, like in the example below:

const syncUser = inngest.createFunction(
 { id: 'sync-user-from-clerk' },  // ←The 'id' is an arbitrary string used to identify the function in the dashboard
 { event: 'clerk/user.created' }, // ← This is the function's triggering event
 async ({ event }) => {
   const user =; // The event payload's data will be the Clerk User json object
   const { id, first_name, last_name } = user;
   const email = user.email_addresses.find(e => === user.primary_email_address_id
   await database.users.insert({ id, email, first_name, last_name })

The event object contains all of the relevant data for the event. The will match the data object from the standard Clerk webhook payload structure. With this clerk/user.created event, the will be a Clerk User json object.

As you can see, you can choose which events you want to handle with each function. You might write a separate function for clerk/user.updated and clerk/user.deleted handling the entire lifecycle end to end.

Note that multiple functions can also listen to the same event. This pattern is called “fan-out.”

Creating a function to send a welcome email

Often, applications need to perform additional tasks when a new user is created, like send a welcome email with tips and useful information.

While it is possible to add this logic at the end of your sync function as seen in the previous section, it’s better to decouple unrelated tasks into different functions so issues with one task do not affect the other ones. For example, if your email fails to send, it should not affect starting a trial for that user in Stripe.

With Inngest, each function has automatic retries, so only the code that has issues is re-run.

The code below creates another function using the same clerk/user.created event and adds the logic to send the welcome email:

const sendWelcomeEmail = inngest.createFunction(
 { id: 'send-welcome-email' },
 { event: 'clerk/user.created' },
 async ({ event }) => {
   const user =
   const { first_name } = user
   const email = user.email_addresses.find(e => === user.primary_email_address_id
   // `emails` is a placeholder for the function in your codebase to send email
   await emails.sendWelcomeEmail({ email, first_name })

Now, you have a function that utilizes the same Clerk webhook event for another purpose. Clerk webhook events can be used for all sorts of application lifecycle use cases. For example, adding users to a marketing email list, starting a Stripe trial, or provisioning new account resources.

Sending a delayed follow-up email

Every Inngest function handler has an additional step object which provides tools to create more complex functions. Using allows you to encapsulate specific code that will be automatically retried ensuring that issues with one part of your function don't force the entire function to re-run. Additionally, other tools like step.sleep, are available to extend functionality.

The code below sends a welcome email, then uses step.sleep to wait for three days before sending another email offering a free trial:

const sendOnboardingEmails = inngest.createFunction(
 { id: 'onboarding-emails' },
 { event: 'clerk/user.created' },
 async ({ event, step }) => { // ← step is available in the handler's arguments
   const user =
   const { first_name } = user
   const email = user.email_addresses.find(e => === user.primary_email_address_id

   await'welcome-email', async () => {
     // `emails` is a placeholder for the function in your codebase to send email
     await emails.sendWelcomeEmail({ email, first_name })

   // wait 3 days before second email
   await step.sleep('wait-3-days', '3 days')

   await'trial-offer-email', async () => {
     await emails.sendTrialOfferEmail({ email, first_name })

Now, you've extended the usefulness of Clerk webhook events even further to build an onboarding drip email campaign in just a few lines of code.

Testing webhook events using the Inngest Dev Server

During local development with Inngest, you can use the Inngest Dev Server to run and test your functions on your own machine. To start the server, in your project directory run the following command:

npx inngest-cli@latest dev

In your browser open http://localhost:8288 to see the Inngest Dev Server.

To quickly get events to test within Dev Server, you can select any individual event from the Events tab then select the Send to Dev Server.

The Inngest Dashboard showing an individual event payload. Red arrows point to the Events tab and the Send to Dev Server button.

You'll now see the event in the Inngest Dev Server's Stream tab alongside any functions that it triggered.

The Inngest Dev Server showing the Stream tab. The forwarded event is visible in the stream.

From here you can select the event, replay it to re-run any functions or edit and replay to edit the event payload to test different types of events.

The Inngest Dev Server showing the forwarded event payload. A red arrow points to the Replay button.


Congratulations! You've now learned how to use Inngest to create functions that use Clerk Webhook events.


What did you think of this content?