Consuming webhook events

At its core, Inngest is centered around functions that are triggered by events. Webhooks are one of the most ubiquitous sources for events for developers. Inngest was designed to support webhook events.

This guide will show you how to use Inngest to consume your webhook events and trigger functions.


When talking about webhooks, it's useful to define some terminology:

  • Producer - The service sending the webhook events as HTTP post requests.
  • Consumer - The URL endpoint which receives the HTTP post requests.

Inngest enables you to create any number of unique URLs which act as webhook consumers. You can create a webhook for each third party service that you use (e.g. Stripe, Github, Clerk) along with custom rules on how to handle that webhook.

Creating a webhook

First, you'll need to head to the Manage tab in the Inngest dashboard and then click Webhooks. From there, click Create Webhook.

Now you'll have a uniquely generated URL that you can provide to any Producer service to start sending events. These URLs are configured in different ways for different producer services. For example, with Stripe, you need to enter "developer mode" and configure your webhook URLs.

Give your webhook a name and save it. Next we'll explore how to turn the request payload into Inngest events.

Inngest dashboard showing a newly created webhook

Defining a transform function

Most webhooks send event data as JSON within the POST request body. These raw events must be transformed slightly to be compatible with the Inngest event payload format. Mainly, we must have name and data set in the Inngest event.

Fortunately, Inngest includes transform functions for every webhook. You can define a short JavaScript function used to transform the shape of the payload. This transform runs on Inngest's servers so there is no added load or cost to your infra.

Here is an example of a raw webhook payload from Clerk on the left and our transformed event:

Example Clerk webhook payload

{
  "type": "user.created",
  "object": "event",
  "data": {
    "created_at": 1654012591514,
    "external_id": "567772",
    "first_name": "Example",
    "id": "user_29w83sxmDNGwOuEthce5gg56FcC",
    "last_name": "Example",
    "last_sign_in_at": 1654012591514,
    "object": "user",
    "primary_email_address_id": "idn_29w83yL7CwVlJXylYLxcslromF1",
    // ... simplified for example
  },
}

Example Inngest event format

{
  "name": "clerk/user.created",
  "data": {
    "created_at": 1654012591514,
    "external_id": "567772",
    "first_name": "Example",
    "id": "user_29w83sxmDNGwOuEthce5gg56FcC",
    "last_name": "Example",
    "last_sign_in_at": 1654012591514,
    "object": "user",
    "primary_email_address_id": "idn_29w83yL7CwVlJXylYLxcslromF1",
    // ... simplified for example
  }
}

Transforms are defined as simple JavaScript functions that accept three arguments and expect the Inngest event payload object in the returned value. The arguments are:

  • Name
    evt
    Type
    object
    Description

    The raw JSON payload from the POST request body

  • Name
    headers
    Type
    object
    Description

    A map of HTTP headers sent along with the request as key-value pairs. Header names are case-insensitive and are canonicalized by making the first character and any characters following a hyphen uppercase and the rest lowercase. For more details, check out the underlying implementation reference.

  • Name
    queryParams
    Type
    object
    Description

    A map of parsed query string parameters sent to the webhook URL. Values are all arrays to support multiple params for a single key.

Here's a simple transform function for the Clerk example shown above:

function transform(evt, headers = {}, queryParams = {}) {
  return {
    name: `clerk/${evt.type}`,
    data: evt.data,
    // You can optionally set ts using data from the raw json payload
    // to explicitly set the timestamp of the incoming event.
    // If ts is not set, it will be automatically set to the time the request is received.
  }
}

👉 We also recommend prefixing each event name with the name of the producer service, e.g. clerk/user.created, stripe/charge.failed.

Example transforms

💡 Header names are case-insensitive and are canonicalized by making the first character and any characters following a hyphen uppercase and the rest lowercase.

Remember to check your transforms for header usage and make sure to use the correct case.

Github - Using headers

Github uses a X-Github-Event header to specify the event type:

function transform(evt, headers = {}, queryParams = {}) {
  const name = headers["X-Github-Event"];
  return {
    // Use the event as the data without modification
    data: evt,
    // Add an event name, prefixed with "github." based off of the X-Github-Event data
    name: "github." + name.trim().replace("Event", "").toLowerCase(),
  };
}
Stripe - Using an id for deduplication

Stripe sends an id with every event to deduplicate events. We can use this as the id for the Inngest event for the same reason:

function transform(evt, headers = {}, queryParams = {}) {
  return {
    id: evt.id,
    name: `stripe/${evt.type}`,
    data: evt,
  };
}
Linear - Creating useful event names
function transform(evt, headers = {}, queryParams = {}) {
  return {
    // type (e.g. Issue) + action (e.g. create)
    name: `linear/${evt.type.toLowerCase()}.${evt.action}`,
    data: evt,
  };
}
Intercom - Setting the ts field
function transform(evt, headers = {}, queryParams = {}) {
  return {
    name: `intercom/${evt.topic}`,
    // the top level obj only contains webhook data, so we omit that
    data: evt.data,
    ts: evt.created_at * 1000,
   };
};
Resend
function transform(evt, headers = {}, queryParams = {}) {
  return {
    name: `resend/${evt.type}`,
    data: evt.data,
   };
};

Testing transforms

The Inngest dashboard includes a tool to quickly test your transform function. You can paste the incoming payload from the webhook producer in the "Incoming Event JSON" editor and immediately preview what the transformed event will look like.

Inngest dashboard transform testing

Some webhook producers do not provide example payloads in their documentation. If that's the case, you can use a tool that we built, TypedWebhooks.tools to test webhooks and browse payloads.

Advanced configuration

Additionally, you can configure allow/deny lists for event names and IP addresses. This can be useful if you want a bit more control over what events are ingested.

Inngest currently does not yet support webhook signature verification. While this means that you cannot use the signature to verify the authenticity of the payload, each webhook URL is unique and can only be used by one producer. This means that you can be confident that the events are coming from the producer that you expect.

Managing webhooks via REST API

Webhooks can be created, updated and deleted all via the Inngest REST API. This is very useful if you want to manage all transforms within your codebase and sync them to the Inngest platform. Check out the documentation below to learn more:

Local development

To test your webhook locally, you can forward events to the Dev Server from the Inngest dashboard using the "Send to Dev Server" button. This button is found anywhere that an event payload is visible on the Inngest dashboard. This will send a copy of the event to your local machine where you can test your functions.

Send to dev server button in the Inngest cloud dashboard

Writing functions

Now that you have events flowing into Inngest, you can write functions that that handle the events that you care about. You can also explore the list of events that have been received at any time by heading to the Events tab of the Inngest dashboard.

Example: Send a welcome email when the clerk/user.created event is received

inngest.createFunction(
  { name: "Send welcome email", id: "send-welcome-email" },
  { event: "clerk/user.created" },
  async ({ event, step }) => {
    const emailAddress = event.data.email_addresses[0].email_address;
    await step.run('send-email', async () => {
      return await resend.emails.send({
        to: emailAddress,
        from: "noreply@inngest.com",
        subject: "Welcome to Inngest!",
        react: WelcomeEmail(),
      })
    });
  }
)

💡 Tip: To test functions locally, copy an event from a webhook from the Inngest dashboard and use it with the Inngest dev server's Send test button.

Verifying request signatures

Many webhook producers sign their requests with a secret key to ensure that the request is coming from them. This establishes trust with the webhook producer and ensures that the event data has not been tampered with.

To verify a webhook signature, you'll need to return the signature and raw request body string in your transform. For example, the following transform function could be used for Stripe webhooks:

function transform(evt, headers, queryParams, raw) {
  return {
    name: `stripe/${evt.type}`,
    data: {
      raw,
      sig: headers["Stripe-Signature"],
    }
  };
};

Then you can use that data to verify the signature in your Inngest functions:

inngest.createFunction(
  { id: "stripe/charge.updated" },
  { event: "stripe/charge.updated" },
  async ({ attempt, event, step }) => {
    if (!verifySig(event.data.raw, event.data.sig, stripeSecret)) {
      throw new NonRetriableError("failed signature verification");
    }

    // Now it's safe to use the event data.
    const data = JSON.parse(event.data.raw);
  }
);

Branch environments

All branch environments share the same webhooks. They are centrally-managed in a single page.

Additionally, the target branch environment must be specified using either an x-inngest-env query param or header. For example, the following command will send an webhook to the branch-1 branch environment:

curl 'https://inn.gs/e/REDACTED?x-inngest-env=branch-1' -d '{"msg": "hi"}'

If the branch environment is not specified with the header or query param, the webhook will be sent to this page and will not trigger any functions. Events will also go here if the branch environment does not exist.

The value for x-inngest-env is the name of the branch environment, not the ID in the URL.