Skip to main content

Webhooks

Webhooks let LumoAuth notify your applications in real time when events occur — authentication events, user changes, security alerts, and more. LumoAuth POSTs a JSON payload to an HTTPS endpoint you provide and signs each request with HMAC-SHA256 (a keyed hash computed from a shared secret) so your server can verify it.


How Webhooks Work​

LumoAuth sends a JSON payload to your configured endpoint for each subscribed event.


Setting Up Webhooks​

Via Portal​

  1. In the portal sidebar, open Integrations (/orgs/{orgId}/portal/integrations) and go to the Webhooks section
  2. Click Add webhook — this opens the form at /orgs/{orgId}/portal/integrations/webhooks/create
  3. Configure:
FieldDescription
ChannelCustom webhook (HMAC-signed JSON to any HTTPS endpoint), or Slack, Discord or Microsoft Teams, which receive a formatted message instead of raw JSON
Endpoint URLYour HTTPS endpoint for receiving events (see Allowed destinations)
Signing secretOptional shared secret for signature verification (custom webhooks)
EnabledEnable/disable the webhook
EventsWhich events to subscribe to. Some plans limit the number of event types per webhook; the form shows the limit
  1. Click Create webhook

The Create webhook form with the channel, endpoint URL, signing secret and Enabled switch

Each webhook in the Webhooks section has an actions menu with Edit, Deliveries, Send test event and Delete. To enable or disable a webhook, or to set a new signing secret, open Edit. Deliveries (/orgs/{orgId}/portal/integrations/webhooks/{id}/deliveries) lists recent attempts with their status and lets you replay failed ones.

The Webhooks section of the Integrations page listing a webhook

The Webhook deliveries page

Via API​

The webhook admin API is mounted under /orgs/{orgId}/api/v1/admin/webhooks:

curl -X POST https://your-domain.com/orgs/{orgId}/api/v1/admin/webhooks \
-H "Authorization: Bearer {admin_token}" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-app.com/webhooks/lumoauth",
"events": ["user.login", "user.login_failed", "user.created"],
"secret": "your-webhook-secret",
"active": true
}'

To fetch the full, authoritative list of subscribable event names, call:

curl https://your-domain.com/orgs/{orgId}/api/v1/admin/webhooks/events \
-H "Authorization: Bearer {admin_token}"

Allowed destinations​

A webhook URL is checked when it is created, updated and tested, and the hostname is resolved and pinned again before every delivery, so a DNS record cannot be swapped to an internal address between the check and the request. The rules:

RequirementAcceptedRejected
Schemehttps://http://, ftp://, file://, anything else
Credentials in the URLnonehttps://user:pass@host/
HostnameA public name that resolves to at least one address, or a public IP literallocalhost, *.localhost, names that do not resolve, names where any A or AAAA record is in a blocked range

Blocked address ranges (a destination is refused if any resolved address falls in one):

RangeWhat it is
10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16Private network address ranges
127.0.0.0/8, ::1, ::Loopback and unspecified
169.254.0.0/16, fe80::/10Link-local; cloud instance metadata lives here
100.64.0.0/10Shared address space / carrier-grade NAT; also cloud metadata on some providers
0.0.0.0/8, 192.0.0.0/24, 198.18.0.0/15, 240.0.0.0/4"This" network, IETF protocol assignments, benchmarking, reserved and broadcast
192.0.2.0/24, 198.51.100.0/24, 203.0.113.0/24, 2001:db8::/32Documentation ranges (TEST-NET-1/2/3)
192.88.99.0/24, 2002::/166to4 relay and 6to4 addresses (the embedded IPv4 is checked too)
64:ff9b::/96, 64:ff9b:1::/48, ::ffff:0:0/96, Teredo 2001::/32NAT64, IPv4-mapped and Teredo addresses: the embedded IPv4 is extracted and checked against this table
fc00::/7, fec0::/10IPv6 unique-local and site-local
224.0.0.0/4, ff00::/8Multicast

The portal reports The webhook URL must be a public HTTPS URL.; the Admin API returns 400 with Webhook URL must not point to a private or internal address. To receive webhooks on a machine inside your network, expose it through a tunnel or reverse proxy with a public hostname.


Available Events​

The canonical list is returned by GET /orgs/{orgId}/api/v1/admin/webhooks/events. In the portal, the Events section of the webhook form groups the same events by category, with Select all / Clear all shortcuts. The current catalog:

The Events section of the webhook form with event checkboxes grouped by category

User Events​

  • user.created - New user created
  • user.updated - User updated
  • user.deleted - User deleted
  • user.blocked - User blocked
  • user.unblocked - User unblocked
  • user.email_verified - User verified their email
  • user.login - User logged in successfully
  • user.login_failed - A login attempt failed
  • user.logout - User logged out
  • user.password_changed - User changed their password
  • user.mfa_enabled - A user enrolled their first MFA (multi-factor authentication) factor
  • user.mfa_disabled - A user no longer has any active MFA factor (the user or an admin removed the last one)

MFA Events​

Each MFA audit event is also a webhook event with the same name. The data object carries status, actor_id, actor_name, target_type, target_id (the user id), target_name (the user's email), ip_address, user_agent, severity and metadata (for example authenticator_id, factor_type, purpose, reason).

  • mfa.enroll.started - A user started enrolling an MFA factor
  • mfa.enroll.completed - A user finished enrolling an MFA factor
  • mfa.enroll.failed - An MFA enrollment verification failed
  • mfa.enroll.deferred - A user chose "Remind me later" on the MFA setup screen
  • mfa.challenge.issued - An MFA challenge was issued
  • mfa.challenge.succeeded - An MFA challenge was passed
  • mfa.challenge.failed - An MFA challenge failed
  • mfa.push.denied - A push sign-in request was denied in the app
  • mfa.push.reported_fraud - A user reported a push sign-in request as "This wasn't me" — route this to your security team
  • mfa.factor.removed - An MFA factor was removed (by the user or an admin)
  • mfa.factor.default_changed - A user changed their default MFA factor
  • mfa.recovery_codes.generated - Recovery codes were generated or regenerated
  • mfa.recovery_code.used - A recovery code was used to sign in
  • mfa.recovery.started - Account recovery started
  • mfa.recovery.completed - A recovered user finished re-enrolling (new factor and recovery codes)
  • mfa.tap.issued - An admin issued a temporary access code
  • mfa.locked - MFA was locked after too many failures
  • mfa.policy.changed - The organization MFA policy changed
  • mfa.trusted_device.added - A user trusted a device to skip MFA
  • mfa.trusted_device.revoked - A trusted device was revoked
{
"event": "mfa.push.reported_fraud",
"timestamp": "2026-09-24T10:30:00Z",
"tenant": "acme-corp",
"data": {
"status": "failure",
"target_type": "user",
"target_id": "0192a3f4-…",
"target_name": "alice@acme.com",
"severity": "warning",
"metadata": {
"factor_type": "push",
"authenticator_id": "push:5",
"initiating_ip": "198.51.100.23",
"location": "Lagos, NG",
"user_agent": "Mozilla/5.0 …",
"reason": "user_reported_not_me"
}
}
}
  • identity.linked - A social identity was linked
  • identity.unlinked - A social identity was unlinked
  • consent.granted - A user granted consent to an OAuth client
  • consent.revoked - A user revoked consent
  • session.revoked - A session was revoked

Client & Token Events​

  • client.created - OAuth client created
  • client.updated - OAuth client updated
  • client.deleted - OAuth client deleted
  • client.secret_rotated - OAuth client secret rotated
  • token.issued - An access token was issued
  • token.revoked - A token was revoked

Access-Control Events​

  • role.assigned - A role was assigned to a user
  • role.unassigned - A role was removed from a user
  • group.member_added - A user was added to a group
  • group.member_removed - A user was removed from a group

Organization Events​

  • organization.created / organization.updated / organization.deleted
  • organization.member.added / organization.member.removed / organization.member.updated
  • organization.invitation.accepted - An organization invitation was accepted

Agent & JIT Events​

  • agent.created - A new agent was registered
  • agent.updated - An agent was updated
  • agent.revoked - An agent was revoked
  • agent.attestation.accepted / agent.attestation.rejected - Agent attestation result
  • jit.approval_required - A JIT permission request needs human approval
  • jit.request.approved / jit.request.denied - A JIT permission request was resolved

Diagnostics​

  • webhook.test - Fired manually from the portal/API to verify endpoint connectivity

Webhook Payload​

Each delivery POSTs a JSON body with this envelope:

{
"event": "user.login",
"timestamp": "2025-02-01T14:30:00Z",
"tenant": "acme-corp",
"data": {
"user_id": "user-uuid",
"email": "alice@acme.com",
"ip_address": "192.168.1.100"
}
}

The event type is in the top-level event field, the organization slug is in tenant, and the event-specific fields are in data. The unique delivery identifier is not in the body — it is sent in the X-LumoAuth-Delivery header (see below).

Every request also carries these headers:

HeaderDescription
X-LumoAuth-EventThe event name (matches the body's event)
X-LumoAuth-TimestampUnix timestamp (seconds) when the request was signed
X-LumoAuth-DeliveryUnique delivery ID — use it for idempotency / replay detection
X-LumoAuth-SignatureHMAC signature (see below)

Signature Verification​

When a webhook has a secret configured, each request is signed. The X-LumoAuth-Signature header is a comma-delimited set of fields:

X-LumoAuth-Signature: kid=v1,t=1706400000,id=<deliveryId>,v1=<hmac>
FieldMeaning
kidSigning-key version (v1, or v2 after the secret has been rotated)
tUnix timestamp (matches X-LumoAuth-Timestamp)
idDelivery ID (matches X-LumoAuth-Delivery)
v1HMAC-SHA256(secret, "{t}.{id}.{rawBody}"), hex-encoded

The HMAC is computed over the signing base {timestamp}.{deliveryId}.{rawRequestBody} — not the raw body alone. Verify it like this:

const crypto = require('crypto');

function verifyWebhook(rawBody, signatureHeader, secret) {
// Parse "kid=..,t=..,id=..,v1=.." into a map
const parts = Object.fromEntries(
signatureHeader.split(',').map((kv) => {
const i = kv.indexOf('=');
return [kv.slice(0, i), kv.slice(i + 1)];
})
);

const signingBase = `${parts.t}.${parts.id}.${rawBody}`;
const expected = crypto
.createHmac('sha256', secret)
.update(signingBase)
.digest('hex');

const a = Buffer.from(parts.v1 || '', 'hex');
const b = Buffer.from(expected, 'hex');
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
Replay protection

Reject requests whose t is far from your own clock, and de-duplicate on id (the delivery ID) so a replayed request is only processed once. Always verify against the raw request body bytes — re-serializing the parsed JSON can change byte order and break the HMAC.


Retry Policy​

ScenarioBehavior
2xx responseSuccess, no retry
Non-2xx responseRetry with exponential backoff
Timeout (5 s)Retry
Retry schedule60s, 5m, 30m, 2h, 12h, 24h, then the delivery is dead-lettered (shown on the deliveries page, where it can be replayed)
Repeated failuresThe webhook stays enabled; watch the Deliveries page and disable it yourself from Edit if the endpoint is gone

Troubleshooting​

  • 400 "must not point to a private or internal address" for a public hostname. One of its DNS records (often an AAAA record) points into a blocked range, or the name resolves to nothing from the LumoAuth host. Check every record, not just the A record.
  • A URL that saved before now fails the test delivery. The blocked list was extended (CGNAT 100.64.0.0/10, TEST-NET, 6to4, NAT64 and multicast ranges). Move the endpoint to a public address.
  • http:// URL refused. Only https:// is accepted for webhook endpoints.

Standards used on this page​

Plain nameStandard
Private network address rangesRFC 1918

See the standards reference for what each one covers and how LumoAuth uses it.