# Guide to Auth0 Webhooks: Features and Best Practices

Auth0 webhooks tell your systems what is happening in your tenant: users signing up, logging in, failing to log in, being updated or deleted, and organization membership changing. If you're syncing users into your own database or feeding authentication activity to a security pipeline, webhooks let those systems hear about changes without polling the Management API.

This guide covers the two ways Auth0 delivers events over HTTP (Custom Log Streams and Event Streams), how each authenticates its requests, the retry and auto-disable rules you need to plan around, and the best practices for production.

## What are Auth0 webhooks?

Auth0 does not have a single "webhooks" product. Two features send HTTP POST requests to an endpoint you control, and they behave differently:

* Custom Log Streams export your tenant logs. Each request carries one or more log records, each with a `log_id` and a `data` object whose `data.type` is a short log event type code such as `s` (successful login). Auth0 positions log streams for monitoring and analytics, not for your application's critical path.
* Event Streams (labeled Event Streams (Early) in the dashboard) deliver lifecycle events such as `user.created` or `organization.member.added`. Each request is a single CloudEvents-format event with `id`, `type`, `time` and `data` fields, and the changed entity sits under `data.object`. Event Streams can also target AWS EventBridge or an Auth0 Action.

Neither one signs its requests. Both authenticate with a static value you configure, which Auth0 sends in the `Authorization` header.

## Auth0 webhook features

| Feature | Details |
| --- | --- |
| Delivery mechanisms | Custom Log Streams (tenant logs) and Event Streams (lifecycle events) |
| Configuration | Log Streams: Dashboard > Monitoring > Streams > Create Stream > Custom Webhook. Event Streams: Dashboard > Event Streams (Early), the Management API (`POST /api/v2/event-streams`), the Auth0 CLI, or Terraform |
| Authentication | No signature. Log Streams send the optional Authorization Token verbatim in `Authorization`. Event Streams support bearer, basic, or custom-header authorization |
| Log Stream format | Content Format of JSON lines, arrays, or objects; records carry `log_id` and `data.type` |
| Event Stream format | CloudEvents object with `id`, `type`, `time`, and `data.object` |
| Log Stream retries | Up to three attempts per log; errored logs are retried until the problem is resolved; stream pauses after 7 consecutive days of failures |
| Event Stream retries | Four total attempts (initial plus three retries at 1s, 2s, 4s); 4xx responses fail immediately with no retry |
| Auto-disable | Event Streams disable after 500 consecutive failures or 5000 total failed deliveries |
| Filtering | Log Streams: Filter by Event Category. Event Streams: subscribe to specific event types |
| Recovery | Log Streams: recreate with a Starting Cursor within your log retention period. Event Streams: Deliveries and Redelivery APIs |

## Common events

Event Streams use dotted event names in the `type` field:

| Event | Fires when |
| --- | --- |
| `user.created` | A user is created in the tenant |
| `user.updated` | A user's profile is updated |
| `user.deleted` | A user is deleted |
| `organization.created` | An organization is created |
| `organization.updated` | An organization is updated |
| `organization.deleted` | An organization is deleted |
| `organization.member.added` | A user is added to an organization |
| `organization.member.deleted` | A user is removed from an organization |
| `organization.member.role.assigned` | A role is assigned to an organization member |
| `group.created` | A group is created |

For Event Streams, branch on the top-level `type` and read the entity from `data.object`. Log Streams use log event type codes at `data.type` on each record instead, such as `s` (successful login), `f` (failed login), `ss` (successful signup) and `slo` (successful logout), out of a much longer list in Auth0's log event type codes reference.

> Inspect Auth0 webhooks as they arrive. Point Auth0 at a [Hookdeck Console](https://console.hookdeck.com) URL to inspect and replay real deliveries — no account or setup required.

## Setting up Auth0 webhooks

Both mechanisms rely on a secret you generate yourself, since Auth0 does not issue a signing secret. Create a long random value (`openssl rand -hex 32` works) and store it in your app's environment.

Custom Log Stream. In the dashboard, go to Monitoring > Streams, click Create Stream, select Custom Webhook, and name the stream. Then fill in:

* Payload URL: your HTTPS endpoint. Self-signed certificates are not supported.
* Authorization Token: your secret. Auth0 sends exactly what you type as the `Authorization` header, so if you enter `Bearer abc123`, that full string is what arrives.
* Content Type: `application/json`.
* Content Format: JSON lines, arrays, or objects. Pick JSON Array if your handler parses the body as a single JSON document; JSON lines sends newline-delimited objects that a standard JSON parser will reject.
* Filter by Event Category: limit the stream to the log categories you need.

Save, then confirm the stream status is Active in the Health view.

Event Stream via the API. Create an M2M application with the `create:event_streams` scope, get a Management API token, and POST the stream definition:

```bash
curl -X POST https://YOUR_TENANT.auth0.com/api/v2/event-streams \
  -H "Authorization: Bearer YOUR_MANAGEMENT_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "user-sync",
    "subscriptions": [
      { "event_type": "user.created" },
      { "event_type": "user.updated" },
      { "event_type": "user.deleted" }
    ],
    "destination": {
      "type": "webhook",
      "configuration": {
        "webhook_endpoint": "https://your-app.com/webhooks/auth0",
        "webhook_authorization": { "method": "bearer", "token": "YOUR_SECRET" }
      }
    }
  }'

```

The endpoint must use `https://`, and new event streams are enabled by default. With bearer authorization, requests arrive with `Authorization: Bearer YOUR_SECRET`. The dashboard equivalent is Event Streams (Early) > Create Event Stream > Webhook.

For local development, use the [Hookdeck CLI](/docs/cli): `hookdeck listen 3000 auth0 --path /webhooks/auth0` gives you a public HTTPS URL that forwards to your local server, plus a web UI for inspecting and replaying deliveries, with no account required. Use the generated URL as the Payload URL or webhook endpoint, then trigger a login or create a test user.

## Securing Auth0 webhooks

With no signature to check, authenticating an Auth0 request means comparing the incoming `Authorization` header against the value you configured. Compare the full header byte for byte, including any `Bearer ` prefix, using a timing-safe comparison with a length check first (`crypto.timingSafeEqual` throws on buffers of different lengths). Serve the endpoint over HTTPS only: the static token is the sole credential, and anyone who captures it can forge requests.

Because the credential lives in a header rather than in a signature over the body, you can parse JSON normally. This Express handler receives a Custom Log Stream configured with the JSON Array content format:

```javascript
const crypto = require("crypto");
const express = require("express");

const app = express();

function verifyAuth0Token(headerValue, expectedToken) {
  if (!headerValue || !expectedToken) return false;
  const a = Buffer.from(headerValue);
  const b = Buffer.from(expectedToken);
  // timingSafeEqual throws on unequal lengths, so guard first
  if (a.length !== b.length) return false;
  try {
    return crypto.timingSafeEqual(a, b);
  } catch {
    return false;
  }
}

app.post("/webhooks/auth0", express.json(), (req, res) => {
  const authHeader = req.headers["authorization"];

  if (!authHeader) {
    return res.status(401).send("Missing Authorization header");
  }

  if (!verifyAuth0Token(authHeader, process.env.AUTH0_LOG_STREAM_TOKEN)) {
    return res.status(401).send("Invalid Authorization token");
  }

  // Log Streams batch records into an array
  const events = Array.isArray(req.body) ? req.body : [req.body];

  // Acknowledge first; Auth0 retries non-2xx responses
  res.status(200).json({ received: events.length });

  for (const event of events) {
    const data = event.data || event;
    switch (data.type) {
      case "s":
        // Successful login: update last-login, notify
        break;
      case "f":
        // Failed login: brute-force detection, alerting
        break;
      case "ss":
        // Successful signup: provision the user
        break;
    }
  }
});

```

For an Event Stream with bearer authorization, store `Bearer <your-secret>` as the expected value, treat the body as one object rather than an array, and switch on `req.body.type`.

Note the exact match: a stored token of `abc123` will never equal an incoming header of `Bearer abc123`, and a mismatched prefix or stray whitespace is the usual cause of every request returning 401.

> Make Auth0 webhooks production-ready. [Hookdeck Event Gateway](/event-gateway) checks the Authorization header at the edge, deduplicates, and durably queues every event with replay for anything that fails.

## Auth0 webhook limitations and pain points

### A static token instead of a signature

The Problem: Auth0 requests carry no HMAC signature and no timestamp. The only proof a request came from Auth0 is a fixed string in the `Authorization` header, which cannot detect a modified body or a replayed request.

Why It Happens: Both Log Streams and Event Streams use a shared-secret model: you configure a value, Auth0 sends it with every request, and there is nothing to compute.

Workarounds:

* Generate a long random token, keep it out of source control and logs, and rotate it if it may have been exposed.
* Treat request contents as untrusted until validated, and make handlers idempotent so a replayed request does no harm.

How Hookdeck Can Help: Hookdeck's Auth0 source compares the incoming `Authorization` header against the exact value you enter (a Log Stream's Authorization Token as typed in Auth0, or `Bearer <token>` for an Event Stream using bearer auth), so requests without the right header are rejected before they reach your handler. Every delivery is logged with full headers and body.

### Event Stream retries are over in seconds

The Problem: An Event Stream makes four attempts in total, with retries after 1, 2 and 4 seconds. A deploy or a brief outage can outlast that window, and the event is then marked as failed. A 4xx response fails the event immediately with no retry at all.

Why It Happens: Auth0's retry policy for Event Streams is short and fixed. Failed events stay failed until you call the Redelivery API, and the stream is disabled automatically after 500 consecutive failures or 5000 total failed deliveries.

Workarounds:

* Poll the `GET /api/v2/event-streams/{id}/deliveries` endpoint (Auth0 suggests every 5 minutes) and alert on failures.
* Use `POST /api/v2/event-streams/{id}/redeliver` to retry all failed events, or `/redeliver/{event_id}` for one.
* Avoid returning 4xx for transient problems in your own code, since Auth0 will not retry them.

How Hookdeck Can Help: Hookdeck acknowledges Auth0 immediately and durably queues each event, then delivers it to your endpoint with [automatic retries](/docs/retries) on a schedule you control. [Issues](/docs/issues) alert you when deliveries fail and you can replay any event, so an outage on your side no longer burns through Auth0's retry budget or edges the stream toward auto-disable.

### Log Streams pause, and backfill means recreating the stream

The Problem: When a Custom Log Stream cannot reach your server, Auth0 retries each log up to three times, records an error in the Health view, and keeps restarting the process for errored logs. After 7 consecutive days of failures the stream pauses and has to be resumed manually. If logs never arrive, the documented recovery is to delete the stream and recreate it with a Starting Cursor.

Why It Happens: Log Streams are built for monitoring and analytics, with recovery tied to your tenant's log retention period. The Health view only shows the last ten errors from the last 5 days.

Workarounds:

* Check stream status regularly and alert if it leaves Active.
* Know your plan's log retention period, since a Starting Cursor can only reach back that far.

How Hookdeck Can Help: Hookdeck sits between Auth0 and your endpoint and accepts every batch as it arrives, so failures in your service do not put the stream at risk. Each request is stored with its headers and body, and you can replay it once your handler is fixed.

### Duplicates and out-of-order delivery

The Problem: Both mechanisms can deliver the same data more than once, and neither guarantees order. An Event Stream can deliver `user.updated` before the `user.created` it follows, and a retried Log Stream batch arrives again in full.

Why It Happens: Log Streams guarantee at-least-once delivery and may not arrive in the order logs occurred. Auth0 says Event Stream endpoints may occasionally receive an event more than once, with no ordering guarantee either.

Workarounds:

* Record processed identifiers (`id` for Event Streams, `log_id` for Log Stream records) and skip repeats.
* Compare the event `time`, or `updated_at` in `data.object`, with what you have stored before overwriting.

How Hookdeck Can Help: Hookdeck [deduplication](/docs/deduplication) can drop repeated Event Stream deliveries based on the event `id` before they reach your handler, and [filters](/docs/filters) let you route Event Stream types to different destinations by `type`.

## Best practices

### Use the right stream for the job

Use Event Streams to act on user, organization or group changes: one typed event per request, limited to the types you subscribe to. Use Custom Log Streams for authentication activity such as logins and failed logins, which Event Streams do not cover, and treat that feed as monitoring data.

### Compare the Authorization header exactly and safely

Store the full expected header value, including any `Bearer ` prefix, and compare it with a length check followed by `crypto.timingSafeEqual`. Use a separate token per stream so you can rotate one without touching the others.

### Acknowledge fast, process asynchronously

Event Streams give up after retries spaced 1, 2 and 4 seconds apart, and Auth0 recommends processing events through an asynchronous queue. Return a 2xx as soon as the header checks out and hand the work to a queue or background job. See [why to process webhooks asynchronously](/webhooks/guides/why-implement-asynchronous-processing-webhooks).

### Make handlers idempotent and order-aware

Deduplicate on the Event Stream `id` or the Log Stream `log_id`, and check `time` or `updated_at` before writing so an older event never overwrites newer data. See our [guide to webhook idempotency](/webhooks/guides/implement-webhook-idempotency).

### Parse the format you configured

A Log Stream body depends on its Content Format, so make your parser match it. Iterate every record in a batch and read fields defensively, since failed events can omit `user_id` and non-interactive events can omit `user_agent`.

## Conclusion

Auth0 sends webhooks through two separate features: Custom Log Streams for batched tenant logs keyed by codes like `s` and `f`, and Event Streams for CloudEvents-format lifecycle events like `user.created` and `organization.member.added`. With no signature on either, an exact, timing-safe comparison of the `Authorization` header over HTTPS is the whole of your verification.

Event Streams retry for only a few seconds and disable themselves after repeated failures, and Log Streams pause after a week of errors, so a fast-acknowledging, idempotent handler matters. [Hookdeck Event Gateway](/event-gateway) checks the Authorization header at the edge and puts deduplication and a durable queue with replay in front of your endpoint, so your handlers only process trustworthy events.

[Get started with Hookdeck](https://dashboard.hookdeck.com/signup) for free and handle Auth0 webhooks reliably in minutes.