# Guide to Logto Webhooks: Features and Best Practices

Logto is an open-source identity platform: sign-in, sign-up, social and enterprise SSO, roles and organisations for SaaS and AI apps, run on Logto Cloud or self-hosted. Its webhooks tell your backend when users register, sign in or reset a password, and when users, roles, permissions or organisations change, so you can send a welcome email, sync a CRM or react to a locked account.

Below: how to create a Logto webhook, the three kinds of event and their payloads, verifying `logto-signature-sha-256`, and the limits of Logto's short retry policy.

## What are Logto webhooks?

A Logto webhook subscribes to one or more events and posts their payloads to an HTTPS endpoint. Delivery is asynchronous and happens outside the authentication request, so a webhook can't allow, block or modify a sign-in. Logto points you to Actions for code that must run before sign-in continues.

Events fall into three groups. User flow hooks fire when a user finishes registration, sign-in or a password reset in the sign-in experience. Data mutation events fire when users, roles, scopes, organisations and related records change, whether through the Experience API or the Management API. Exception events report security incidents such as a locked account.

## Logto webhook features

| Feature | Details |
| --- | --- |
| Signature | hex HMAC-SHA256 |
| Signature header | `logto-signature-sha-256` |
| Body | JSON |
| Event routing | Body field `event` |

From Logto's [webhook configuration guide](https://docs.logto.io/developers/webhooks/configure-webhooks):

* Endpoint URLs must be public HTTPS URLs, not localhost.
* Logto asks you to respond with `200` immediately and not to return any other status.
* Responses of `500` or above are retried up to three times. Timeout length is not stated.
* **Recent Requests** keeps 24 hours of request logs, and the webhook list shows a 24-hour success rate.
* **Send test payload** posts sample data for each selected event. These requests aren't logged.

## Events and payloads

| Event routing | Body field `event` |

Every payload has `hookId`, `event` and `createdAt`, and usually `userAgent`.

User flow events (`PostRegister`, `PostSignIn`, `PostResetPassword`) add `interactionEvent`, `sessionId`, `userIp`, `userId`, `user`, `applicationId` and `application`. Data mutation events add `ip` and a `data` object holding the affected entity, which is `null` for deletions and membership changes. They also include the context that caused them: Experience API fields, or Management API fields such as `path`, `method`, `status` and `matchedRoute`. `PostRegister` covers sign-up through the user flow, while `User.Created` covers users created through the Management API.

This is the `Grant.LimitExceeded` sample from the [webhook samples library](https://github.com/hookdeck/webhook-samples/blob/main/providers/logto/latest/Grant.LimitExceeded.json):

```json
{
  "hookId": "hook_abc",
  "event": "Grant.LimitExceeded",
  "createdAt": "2024-01-01T00:00:00.000Z",
  "ip": "192.168.0.1",
  "userAgent": "Mozilla/5.0",
  "userId": "u_001",
  "applicationId": "app_xyz",
  "application": {
    "id": "app_xyz",
    "type": "SPA",
    "name": "My app",
    "description": "My app description"
  },
  "maxAllowedGrants": 2,
  "preRevocationActiveGrantCount": 3,
  "revokedGrantIds": [
    "grant_001"
  ]
}

```

Logto sends this event when a successful authorisation pushes a user past an application's `maxAllowedGrants`, and it revokes the oldest grants. `revokedGrantIds` lists what was removed.

## Setting up Logto webhooks

1. In Logto Console, go to **Webhooks** and click **Create webhook**.
2. Give it a **Name** for your own reference.
3. Enter the **Endpoint URL**, for example `https://example.com/webhooks/logto`.
4. Select the events. Logto recommends choosing only what you need, and you can change the selection later.
5. Create the webhook. It's active straight away, and the three-dots menu disables or reactivates it.
6. On the webhook's details page, copy the **Signing key** into your configuration as `LOGTO_WEBHOOK_SIGNING_KEY`. Optionally add custom headers.

For Hookdeck, create a source with the Logto type, give it the signing key as its verification secret, and use the source URL as the endpoint. Logto rejects localhost URLs, so local testing goes through a tunnel:

```bash
npx hookdeck-cli listen 3000 logto --path /webhooks/logto

```

Then click **Send test payload**.

## Securing Logto webhooks

`logto-signature-sha-256` contains a bare hex HMAC-SHA256 of the request body, keyed with the webhook's signing key. There's no prefix and no timestamp. Compute the HMAC over the raw bytes and compare in constant time. Logto's [secure webhooks page](https://docs.logto.io/developers/webhooks/secure-webhooks) compares with `===`, which you should avoid.

### Node.js verification

```javascript
const crypto = require('node:crypto');

function verifySignature(rawBody, headers, secret) {
  if (!Buffer.isBuffer(rawBody) || typeof secret !== 'string' || !secret) return false;
  const signature = headers['logto-signature-sha-256'];
  if (typeof signature !== 'string') return false;
  const key = Buffer.from(secret, 'utf8');
  const message = rawBody;
  const candidates = [signature];
  const expected = crypto.createHmac('sha256', key).update(message).digest();
  return candidates.some((candidate) => {
    if (!/^[0-9a-fA-F]{64}$/.test(candidate)) return false;
    const received = Buffer.from(candidate, 'hex');
    return received.length === expected.length && crypto.timingSafeEqual(received, expected);
  });
}

```

If several webhooks point at the same URL, each has its own signing key, so try each key or give each webhook its own path.

The handler queues the event and returns `200`, as Logto asks:

```javascript
const express = require('express');

const app = express();

app.post('/webhooks/logto', express.raw({ type: '*/*' }), async (req, res) => {
  const signingKey = process.env.LOGTO_WEBHOOK_SIGNING_KEY;
  if (!signingKey) return res.sendStatus(500);
  if (!verifySignature(req.body, req.headers, signingKey)) return res.sendStatus(401);

  let payload;
  try {
    payload = JSON.parse(req.body.toString('utf8'));
  } catch {
    return res.sendStatus(400);
  }
  if (!payload || typeof payload !== 'object' || Array.isArray(payload)) return res.sendStatus(400);
  try {
    await enqueueLogtoEvent({
      hookId: payload.hookId,
      event: payload.event,
      createdAt: payload.createdAt,
      userId: payload.userId ?? payload.data?.id ?? null,
      payload,
    });
  } catch {
    return res.sendStatus(503);
  }

  res.sendStatus(200);
});

```

Logto's payloads have no event ID. `enqueueLogtoEvent` can derive a deduplication key from `hookId`, `event`, `createdAt` and the user or entity ID. Returning `401` for a bad signature goes against Logto's "200 only" advice, but Logto only retries `5xx`, so it doesn't cause retries.

Hookdeck's Logto source checks the same HMAC, answers with `200`, and records signature mismatches as failed verification. Logto signs no timestamp and Hookdeck applies no time window.

> **Make Logto webhooks production-ready.** [Hookdeck Event Gateway](/event-gateway) gives Logto the immediate `200` it wants, keeps every sign-in and user event, and retries your service for as long as it needs.

## Logto webhook limitations and pain points

### Three retries, and only after a `5xx`

**The Problem:** Events are lost when your endpoint is down for more than a moment, or answers with a `4xx`.

**Why It Happens:** Logto retries a delivery up to three times, and only when the response is `500` or above. Logto doesn't say how long it waits between attempts or what happens on a timeout.

**Workarounds:**

* Keep the receiver highly available and return `200` before doing any work.
* Use the 24-hour **Recent Requests** log to find failures, then resync affected users through the Management API.

**How Hookdeck Can Help:** Hookdeck takes the delivery on Logto's first attempt and applies the retry schedule you choose, over hours or days.

### No event ID

**The Problem:** You can't reliably tell a retry from a new event.

**Why It Happens:** The payload has `hookId`, `event` and `createdAt` but no unique event or delivery identifier.

**Workarounds:**

* Build a key from `hookId`, `event`, `createdAt` and the affected entity ID.
* Make handlers safe to run twice, for example by upserting user records.

**How Hookdeck Can Help:** Hookdeck deduplication can compare those fields and drop repeated deliveries before they reach you.

### Webhooks can't change a sign-in

**The Problem:** You want to block a sign-in or enrich a token based on your own data, but the webhook arrives too late.

**Why It Happens:** Webhooks run outside the authentication request and their response is ignored.

**Workarounds:**

* Use Logto Actions for synchronous logic.
* Use webhooks for follow-up work such as emails, analytics and CRM updates.

**How Hookdeck Can Help:** Hookdeck can fan a single `PostSignIn` event out to several destinations, so each downstream system gets its own copy without slowing anything else.

### Request logs last 24 hours

**The Problem:** A failure you notice on Monday may no longer be visible in Logto.

**Why It Happens:** Logto keeps **Recent Requests** for 24 hours, and test payloads aren't logged at all.

**Workarounds:**

* Log deliveries on your side, without the personal data in them.
* Check webhook health in the Console regularly.

**How Hookdeck Can Help:** Hookdeck keeps requests and events for your plan's retention period, with search by event name or user ID.

## Best practices

### Respond with `200` and queue

Logto's own guidance is to put the event on your queue and return immediately, then let a worker handle errors. Our [asynchronous processing guide](/webhooks/guides/why-implement-asynchronous-processing-webhooks) explains why that model holds up.

### Derive an idempotency key

Without an event ID, use a composite key and store it with the work it caused, as covered in [webhook idempotency patterns](/webhooks/guides/implement-webhook-idempotency).

### Pick events deliberately

Data mutation events fire for Management API calls as well as user actions, including your own admin scripts. Check the `path` and `method` fields if you need to ignore changes your own services made.

### Treat IPs and user agents as personal data

User flow payloads include the user's IP address and user agent. Store them only where you have a reason to.

## Conclusion

Logto webhooks report user flows, data changes and security exceptions, each signed with a hex HMAC-SHA256 of the body in `logto-signature-sha-256`. Verify the raw bytes, answer `200` immediately, and derive your own deduplication key. Logto retries only three times and only after a `5xx`, so anything that matters needs a durable queue behind it.

[Hookdeck Event Gateway](https://hookdeck.com) is that queue, with verification and replay built in.

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