Skip to main content

Webhooks overview

A webhook is an event-driven method of communication between applications.

Unlike typical APIs where you would need to poll for data very frequently to get it "real-time", webhooks only send data when there is an event to trigger the webhook. This makes webhooks seem "real-time", but it's important to note that they are asynchronous.

For example, if you are onboarding a new user, you can't rely on the webhook delivery as part of that flow. Typically the delivery will happen quickly, but it's not guaranteed to be delivered immediately or at all. Webhooks are best used for things like sending a notification or updating a database, but not for synchronous flows where you need to know the webhook was delivered before moving on to the next step. If you need a synchronous flow, see the onboarding guide for an example.

Clerk webhooks

Clerk webhooks allow you to receive event notifications from Clerk, such as when a user is created or updated. When an event occurs, Clerk will send an HTTP POST request to your webhook endpoint configured for the event type. The payload carries a JSON object. You can then use the information from the request's JSON payload to trigger actions in your app, such as sending a notification or updating a database.

Clerk uses Svix to send our webhooks.

You can find the Webhook signing secret when you select the endpoint you created on the Webhooks page in the Clerk Dashboard.

Supported webhook events

To find a list of all the events Clerk supports:

  1. In the Clerk Dashboard, navigate to the Webhooks page.
  2. Select the Event Catalog tab.

There is also a dedicated guide that describes the events Clerk supports for Billing.

Payload structure

The payload of a webhook is a JSON object that contains the following properties:

  • data: contains the actual payload sent by Clerk. The payload can be a different object depending on the event type. For example, for user.* events, the payload will always be the User object.
  • object: always set to event.
  • type: the type of event that triggered the webhook.
  • timestamp: timestamp in milliseconds of when the event occurred.
  • instance_id: the identifier of your Clerk instance.

The following example shows the payload of a user.created event:

{
  "data": {
    "birthday": "",
    "created_at": 1654012591514,
    "email_addresses": [
      {
        "email_address": "example@example.org",
        "id": "idn_29w83yL7CwVlJXylYLxcslromF1",
        "linked_to": [],
        "object": "email_address",
        "verification": {
          "status": "verified",
          "strategy": "ticket"
        }
      }
    ],
    "external_accounts": [],
    "external_id": "567772",
    "first_name": "Example",
    "gender": "",
    "id": "user_29w83sxmDNGwOuEthce5gg56FcC",
    "image_url": "https://img.clerk.com/xxxxxx",
    "last_name": "Example",
    "last_sign_in_at": 1654012591514,
    "object": "user",
    "password_enabled": true,
    "phone_numbers": [],
    "primary_email_address_id": "idn_29w83yL7CwVlJXylYLxcslromF1",
    "primary_phone_number_id": null,
    "primary_web3_wallet_id": null,
    "private_metadata": {},
    "profile_image_url": "https://www.gravatar.com/avatar?d=mp",
    "public_metadata": {},
    "two_factor_enabled": false,
    "unsafe_metadata": {},
    "updated_at": 1654012591835,
    "username": null,
    "web3_wallets": []
  },
  "instance_id": "ins_123",
  "object": "event",
  "timestamp": 1654012591835,
  "type": "user.created"
}

The payload should always be treated as unsafe until you validate the incoming webhook. Webhooks will originate from another server and be sent to your application as a POST request. A bad actor would fake a webhook event to try and gain access to your application or data.

Delivery guarantees

At-least-once delivery

Clerk retries each event until your endpoint returns a 2xx response or the retries run out. Clerk removes duplicate events before sending them, but retries mean an event can still reach your endpoint more than once. For example, if your endpoint processes an event but Clerk never gets the response, it retries the event.

No ordering guarantee

Events aren't guaranteed to arrive in the order they occurred. For example, a user.updated event can arrive before the user.created event for the same user, or two user.updated events can arrive in reverse order. Retries make this more likely, since a failed event is resent after events that were delivered successfully in the meantime.

Handle out-of-order and duplicate events

Your webhook handler should produce the same result no matter the order events arrive in, or how many times each one arrives. The following approaches can help:

  • Skip events you've already processed: Every delivery attempt for the same event carries the same svix-id header. If your handler has side effects, such as sending a welcome email on user.created, store the svix-id of each event you process and skip any you've already seen.
  • Compare timestamps before writing: Most objects in the payload include an updated_at timestamp. Store it alongside the data in your database, and skip an incoming event if its data.updated_at is older than the stored value. This discards stale events that arrive late. Not every change to an object's related data updates its updated_at. To break ties, also store the event's top-level timestamp, and skip an event with an equal updated_at if its timestamp is older.
  • Use the event timestamp when the object doesn't have one: Deleted objects, such as the payload of a user.deleted event, don't include updated_at. Use the top-level timestamp of the event instead.
  • Don't let late events undo deletions: If a create or update event arrives for an object you've already deleted, ignore it instead of recreating the object. A soft delete, such as a deleted_at column, lets you compare the event's timestamp against when the object was deleted.
  • Upsert instead of insert: Treat *.created and *.updated events the same way, by creating the record if it doesn't exist and updating it otherwise. This handles an update arriving before its create, and a create arriving twice.
  • Fetch the latest state when order matters: If your handler depends on the current state of an object, use the event as a signal and fetch the object from the Backend API instead of relying on the payload.

How Clerk handles delivery issues

Retry

Svix will use a set schedule and retry any webhooks that fail. To see the up-to-date schedule, see the Svix Retry Schedule.

If Svix is attempting and failing to send a webhook, and that endpoint is removed or disabled from the Webhooks page in the Clerk Dashboard, then the attempts will also be disabled.

Replay

If a webhook message or multiple webhook messages fail to send, you have the option to replay the webhook messages. This protects against your service having downtime or against a misconfigured endpoint.

To replay webhook messages:

  1. In the Clerk Dashboard, navigate to the Webhooks page.
  2. Select the affected endpoint.
  3. In the Message Attempts section, next to the message you want to replay, open the three-dot menu on the right, then select Replay.
  4. The Replay Messages menu will open. You can choose to:
  • Resend the specific message you selected.
  • Resend all failed messages since the first failed message in that date range.
  • Resend all missing messages since the first failed message in that date range.

Sync data to your database

You can find a guide on how to use webhooks to sync your data to your database.

Protect your webhooks from abuse

To ensure that the API route receiving the webhook can only be hit by your app, there are a few protections you should put in place:

Test and debug your webhooks

To test webhooks during development, your local app needs to be reachable from the internet. See Debug your webhooks for guidance on tunneling tools and troubleshooting common issues.

Feedback

What did you think of this content?

Last updated on