Agent skill

Logto Webhooks Skill

Receive and verify Logto webhooks. Use when setting up a Logto webhook endpoint, validating the logto-signature-sha-256 HMAC header, or handling events like PostSignIn, PostRegister, User.Created, User.Data.Updated and Grant.LimitExceeded.

Install this skill

npx skills add hookdeck/webhook-skills --skill 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.

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/, examples/nextjs/, 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).

eventDescription
PostRegisterA user completes sign-up through the Experience API
PostSignInA user signs in
PostResetPasswordA user resets their password via "Forgot password"
User.CreatedA user account is created (data is the user)
User.Data.UpdatedUser profile data changes (data is the user)
User.DeletedA user is deleted (data is null)
User.SuspensionStatus.UpdatedA user is suspended or reactivated
Grant.LimitExceededAn 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.

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

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

Local Development

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

Reference Materials


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 12, 2026

View on GitHub →