Skip to main content

Webhooks

GET /orgs/{orgId}/api/v1/admin/webhooks
POST /orgs/{orgId}/api/v1/admin/webhooks
GET /orgs/{orgId}/api/v1/admin/webhooks/{webhookId}
PUT /orgs/{orgId}/api/v1/admin/webhooks/{webhookId}
DELETE /orgs/{orgId}/api/v1/admin/webhooks/{webhookId}
POST /orgs/{orgId}/api/v1/admin/webhooks/{webhookId}/rotate-secret
POST /orgs/{orgId}/api/v1/admin/webhooks/{webhookId}/enable
POST /orgs/{orgId}/api/v1/admin/webhooks/{webhookId}/disable
GET /orgs/{orgId}/api/v1/admin/webhooks/{webhookId}/deliveries
GET /orgs/{orgId}/api/v1/admin/webhooks/{webhookId}/deliveries/{deliveryId}
POST /orgs/{orgId}/api/v1/admin/webhooks/{webhookId}/deliveries/{deliveryId}/replay
POST /orgs/{orgId}/api/v1/admin/webhooks/{webhookId}/test
GET /orgs/{orgId}/api/v1/admin/webhooks/events

Webhooks allow your systems to receive real-time notifications when events occur in LumoAuth. All webhook payloads are signed with a rotating HMAC secret.

Authentication​

All webhook management endpoints require an admin API key or a Bearer token with admin:webhooks:read / admin:webhooks:write, or a browser session.

Each endpoint needs the credential's scope and a permission. A session or Bearer caller must hold the permission; for an API key its creator must still hold it. A missing permission returns 403 forbidden with details.required_permissions. See Required permissions.

MethodEndpointPermissionAPI-key / token scope
GET/admin/webhookswebhooks.viewadmin:webhooks:read
GET/admin/webhooks/eventswebhooks.viewadmin:webhooks:read
GET/admin/webhooks/{webhookId}webhooks.viewadmin:webhooks:read
POST/admin/webhookswebhooks.createadmin:webhooks:write
PUT PATCH/admin/webhooks/{webhookId}webhooks.editadmin:webhooks:write
POST/admin/webhooks/{webhookId}/enablewebhooks.editadmin:webhooks:write
POST/admin/webhooks/{webhookId}/disablewebhooks.editadmin:webhooks:write
DELETE/admin/webhooks/{webhookId}webhooks.deleteadmin:webhooks:write
POST/admin/webhooks/{webhookId}/testwebhooks.editadmin:webhooks:write
POST/admin/webhooks/{webhookId}/rotate-secretwebhooks.editadmin:webhooks:write
GET/admin/webhooks/{webhookId}/deliverieswebhooks.viewadmin:webhooks:read
GET/admin/webhooks/{webhookId}/deliveries/{deliveryId}webhooks.viewadmin:webhooks:read
POST/admin/webhooks/{webhookId}/deliveries/{deliveryId}/replaywebhooks.editadmin:webhooks:write
POST/admin/webhooks/tunnel/startwebhooks.editadmin:webhooks:write
POST/admin/webhooks/tunnel/{webhookId}/stopwebhooks.editadmin:webhooks:write
GET/admin/webhooks/tunnel/{webhookId}/streamwebhooks.viewadmin:webhooks:read
POST/ssf/streamwebhooks.createadmin:webhooks:write
GET/ssf/streamwebhooks.viewadmin:webhooks:read
DELETE/ssf/streamwebhooks.deleteadmin:webhooks:write
POST/ssf/verifywebhooks.editadmin:webhooks:write

Webhook Object​

Webhook Object
{
"id": "01JF3KWBK...",
"url": "https://api.example.com/hooks/lumoauth",
"description": "User lifecycle events",
"events": ["user.created", "user.updated", "user.deleted"],
"enabled": true,
"signingSecret": "whsec_...",
"createdAt": "2026-01-15T10:30:00Z",
"updatedAt": "2026-03-01T08:00:00Z"
}

Supported Events​

CategoryEvent Types
Useruser.created, user.updated, user.deleted, user.login, user.logout, user.blocked, user.unblocked, user.password_changed, user.mfa_enabled, user.mfa_disabled
MFAmfa.enroll.started, mfa.enroll.completed, mfa.enroll.failed, mfa.enroll.deferred, mfa.challenge.issued, mfa.challenge.succeeded, mfa.challenge.failed, mfa.push.denied, mfa.push.reported_fraud, mfa.factor.removed, mfa.factor.default_changed, mfa.recovery_codes.generated, mfa.recovery_code.used, mfa.recovery.started, mfa.recovery.completed, mfa.tap.issued, mfa.locked, mfa.policy.changed, mfa.trusted_device.added, mfa.trusted_device.revoked — see MFA events
Identityidentity.linked, identity.unlinked
Roles & Groupsrole.assigned, role.unassigned, group.member_added, group.member_removed
Clientsclient.created, client.deleted
Tokens & Consenttoken.issued, token.revoked, consent.granted, consent.revoked
AI agentsagent.created, agent.updated, agent.revoked (also sent when an agent is deleted), agent.attestation.accepted, agent.attestation.rejected
JIT permissionsjit.approval_required, jit.request.approved, jit.request.denied — decisions fire from every review channel (portal inbox, API, mobile app, approval channels)
Wildcard* — subscribe to all events

Create a Webhook​

Create Webhook
curl -X POST https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/webhooks \
-H "Authorization: ApiKey lmk_abc123" \
-H "Content-Type: application/json" \
-d '{
"url": "https://api.example.com/hooks/lumoauth",
"description": "User lifecycle events",
"events": ["user.created", "user.updated", "user.deleted", "user.login"]
}'
201 Created
{
"id": "01JF3KWBK...",
"url": "https://api.example.com/hooks/lumoauth",
"description": "User lifecycle events",
"events": ["user.created", "user.updated", "user.deleted", "user.login"],
"enabled": true,
"signingSecret": "whsec_AbCdEf123..."
}
warning

The signingSecret is only shown once at creation. Store it securely — use rotate-secret to generate a new one if lost.

Rotate Signing Secret​

Rotate Signing Secret
curl -X POST https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/webhooks/01JF3KWBK.../rotate-secret \
-H "Authorization: ApiKey lmk_abc123"
New Secret
{
"signingSecret": "whsec_NewSecret..."
}

Delivery History​

Get Delivery History
curl "https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/webhooks/01JF3KWBK.../deliveries" \
-H "Authorization: ApiKey lmk_abc123"
Delivery History
{
"data": [
{
"id": 4821,
"webhook_id": "01JF3KWBK...",
"delivery_id": "del_01JF3K...",
"event_name": "user.login",
"status": "success",
"attempt_count": 1,
"last_status_code": 200,
"last_response_body": "ok",
"last_error": null,
"next_attempt_at": null,
"succeeded_at": "2026-03-01T08:00:00Z",
"dead_lettered_at": null,
"created_at": "2026-03-01T08:00:00Z",
"updated_at": "2026-03-01T08:00:00Z"
}
],
"meta": {
"count": 1,
"dead_lettered_count": 0
}
}

Filter with ?status=pending|success|failed|dead_lettered and cap the result set with ?limit= (max 200, default 50).

Inspect or Replay a Single Delivery​

Fetch one delivery (including its per-attempt history) by its delivery_id, or re-enqueue it for redelivery:

Get / Replay a Delivery
# Inspect a single delivery
curl "https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/webhooks/01JF3KWBK.../deliveries/del_01JF3K..." \
-H "Authorization: ApiKey lmk_abc123"

# Replay (re-enqueue) it — returns the new delivery id
curl -X POST "https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/webhooks/01JF3KWBK.../deliveries/del_01JF3K.../replay" \
-H "Authorization: ApiKey lmk_abc123"

Send a Test Delivery​

Send a synthetic event to the webhook URL to confirm connectivity. The endpoint refuses non-HTTPS and private/internal URLs.

Test a Webhook
curl -X POST "https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/webhooks/01JF3KWBK.../test" \
-H "Authorization: ApiKey lmk_abc123"
Test Response
{
"success": true,
"statusCode": 200,
"message": "Test webhook delivered successfully"
}

List Subscribable Event Types​

Returns a map of every event type you can subscribe a webhook to, keyed by event name with a human-readable description (the same set shown in Supported Events).

List Event Types
curl "https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/webhooks/events" \
-H "Authorization: ApiKey lmk_abc123"
Event Types Response
{
"data": {
"user.created": "Triggered when a new user is created",
"user.updated": "Triggered when a user is updated",
"user.login": "Triggered when a user logs in successfully"
}
}

Verifying Webhook Signatures​

LumoAuth signs every webhook delivery with HMAC-SHA256. The signature is sent in the X-LumoAuth-Signature header as a comma-separated list of fields:

X-LumoAuth-Signature
X-LumoAuth-Signature: kid=v1,t=1709280000,id=del_01JF3K...,v1=9f86d081884c7d659a2feaa0...
FieldMeaning
kidSigning-key version (v1, or v2 after a secret rotation) — pick the matching secret during a rotate-with-grace-period cycle.
tUnix timestamp the delivery was signed (also sent as X-LumoAuth-Timestamp).
idDelivery ID (also sent as X-LumoAuth-Delivery).
v1The HMAC-SHA256 hex digest.

The HMAC is computed over the signing base "{t}.{id}.{rawBody}" — i.e. the timestamp, the delivery ID, and the raw request body joined by dots — not over the body alone. Reconstruct the same string and compare against v1:

Verify Signature (Node.js)
const crypto = require("crypto");

function parseSignature(header) {
return Object.fromEntries(
header.split(",").map((kv) => {
const i = kv.indexOf("=");
return [kv.slice(0, i), kv.slice(i + 1)];
})
);
}

// `rawBody` MUST be the exact bytes received (do not re-serialize the JSON).
function verifyWebhook(rawBody, sigHeader, secret) {
const { t, id, v1 } = parseSignature(sigHeader);
const signingBase = `${t}.${id}.${rawBody}`;
const expected = crypto
.createHmac("sha256", secret)
.update(signingBase)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(expected, "hex"),
Buffer.from(v1, "hex")
);
}
Verify Signature (Python)
import hmac, hashlib

def parse_signature(header: str) -> dict:
return dict(field.split("=", 1) for field in header.split(","))

# `raw_body` MUST be the exact bytes received (do not re-serialize the JSON).
def verify_webhook(raw_body: bytes, sig_header: str, secret: str) -> bool:
fields = parse_signature(sig_header)
signing_base = f"{fields['t']}.{fields['id']}.".encode() + raw_body
expected = hmac.new(secret.encode(), signing_base, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, fields["v1"])
tip

Always use a constant-time comparison (timingSafeEqual / compare_digest) to prevent timing attacks. Reject deliveries whose t is too far from your server clock to defend against replay.

Payload Format​

Example Webhook Payload
{
"id": "evt_01JF3K...",
"type": "user.login",
"orgId": "acme-corp",
"createdAt": "2026-03-01T08:00:00Z",
"data": {
"userId": "01JF3KUSER...",
"email": "alice@example.com",
"ip": "203.0.113.1",
"userAgent": "Mozilla/5.0 ..."
}
}

Webhook requests include the following headers:

HeaderValue
X-LumoAuth-Signaturekid=<v1|v2>,t=<ts>,id=<deliveryId>,v1=<hex-hmac> (see Verifying Webhook Signatures)
X-LumoAuth-EventEvent type (e.g. user.login)
X-LumoAuth-TimestampUnix timestamp the delivery was signed
X-LumoAuth-DeliveryUnique delivery ID
User-AgentLumoAuth-Webhook/1.0
Content-Typeapplication/json