# Logto Webhooks

Logto (open-source identity and auth, cloud or self-hosted) sends an HTTP `POST` with a JSON body to your endpoint when a user flow completes, when users, roles, permissions or organizations change, or when a security exception occurs. Webhooks are asynchronous notifications: your response cannot allow, block or modify the sign-in in progress.

## When to Use This Skill

* How do I receive Logto webhooks?
* How do I verify the `logto-signature-sha-256` signature?
* How do I handle `PostSignIn`, `PostRegister` or `User.Created` events?
* Why is my Logto webhook signature verification failing?
* How do I tell which Logto event I received?

## Verification (core)

Logto sends `logto-signature-sha-256`: the bare lowercase **hex** HMAC-SHA256 of the **raw request body**, keyed with the webhook's signing key (UTF-8 bytes, used as-is). No prefix, no timestamp; nothing else is signed. Check the header is exactly 64 hex characters, then compare in constant time.

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

function verifyLogtoSignature(rawBody, signatureHeader, secret) {
  if (!Buffer.isBuffer(rawBody) || !signatureHeader || !secret) return false;
  if (!/^[0-9a-fA-F]{64}$/.test(signatureHeader)) return false; // validate before decoding
  const expected = crypto.createHmac('sha256', secret).update(rawBody).digest();
  const received = Buffer.from(signatureHeader.toLowerCase(), 'hex');
  return crypto.timingSafeEqual(received, expected);
}

// Express: read bytes for any Content-Type so the exact body is verified
app.post('/webhooks/logto', express.raw({ type: '*/*' }), (req, res) => {
  if (!verifyLogtoSignature(req.body, req.get('logto-signature-sha-256'), process.env.LOGTO_WEBHOOK_SECRET)) {
    return res.status(401).send('Invalid signature');
  }
  const payload = JSON.parse(req.body.toString('utf8')); // dispatch on payload.event
});

```

Because no timestamp is signed there is no replay window to check; make handlers idempotent instead (see below). Logto's own docs sample compares with `===`; use a constant-time comparison.

> **For complete handlers with tests**, see [examples/express/](https://github.com/hookdeck/webhook-skills/tree/main/skills/logto-webhooks/examples/express/), [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/logto-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/logto-webhooks/examples/fastapi/).

## Common Event Types

The event name is the top-level `event` string (not `interactionEvent`, which is a separate field on user-flow bodies and on data changes triggered from a user flow).

| `event` | Description |
| --- | --- |
| `PostRegister` | A user completes sign-up through the Experience API |
| `PostSignIn` | A user signs in |
| `PostResetPassword` | A user resets their password via "Forgot password" |
| `User.Created` | A user account is created (`data` is the user) |
| `User.Data.Updated` | User profile data changes (`data` is the user) |
| `User.Deleted` | A user is deleted (`data` is `null`) |
| `User.SuspensionStatus.Updated` | A user is suspended or reactivated |
| `Grant.LimitExceeded` | An application exceeded its grant limit (exception event) |

Logto documents more families: `Role.*`, `Scope.*`, `Organization.*`, `OrganizationRole.*`, `OrganizationScope.*`, `TrustedDevice.*`, `Identifier.Lockout` and `Message.RateLimited`. The examples log events they don't handle and still return `200`.

> **For the full list**, see [Logto webhook events](https://docs.logto.io/developers/webhooks/webhooks-events).

## Payload Shape

Every body has `hookId`, `event`, `createdAt` (ISO 8601) and optionally `userAgent`. The client IP is `userIp` on user-flow events and `ip` on data-mutation and exception events. Treat every field as untrusted after verification: check types before reading nested objects such as `user`, `application` or `data`.

## Environment Variables

```bash
# Signing key from the webhook details page in Logto Console > Webhooks
LOGTO_WEBHOOK_SECRET=your_signing_key_here

```

## Local Development

```bash
# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 logto --path /webhooks/logto

```

## Reference Materials

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/logto-webhooks/references/overview.md) - Logto webhook families, events, payloads, delivery notes
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/logto-webhooks/references/setup.md) - Create a webhook in Logto Console and get the signing key
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/logto-webhooks/references/verification.md) - Signature verification details and gotchas