Aller au contenu principal
Version 0.3Brouillon

Webhooks and events API

Webhook push is not available yet

Today LuxID emits a single event type, ClaimValuesChanged, and only through the pull model (Partner API - Event Hub). The webhook push model described on this page, the subscription-management endpoints, and the other event types are planned and not available yet. Do not build against them until you have confirmed availability with LuxID.

Two ways to receive LuxID events

LuxID delivers the same stream of user-related events two ways: push (webhooks, LuxID calls your endpoint) and pull (Event Hub, your back-end polls). Both use the same on-the-wire envelope.

LuxID exposes the same stream of user-related events through two complementary delivery models. Which one you pick depends on the operational model that fits your back-end best:

ModelHow it worksWhere it is documented
Pull (Event Hub)Your back-end polls a time-range query endpoint and reads events from the response. You control the cadence and back-pressure.Partner API - Event Hub
Push (Webhooks)LuxID calls a URL you host whenever an event occurs. You receive events without polling, but must keep the endpoint highly available.This page

Both models surface events with the same on-the-wire envelope (id, timestamp, type, version, sphere, subscriber, application, subscription, payload). The difference is who initiates the call and how retries are handled. The canonical envelope reference is the Partner API - Event Hub page.

For the event catalogue, see Events and Event Hub. The authoritative event types and payload shapes are maintained in the LuxID OpenAPI document.


Choosing between push and pull

When push (webhooks) is preferable

  • Events are infrequent and you do not want to run a polling job at all.
  • You want sub-minute latency from event to action without consuming request budget on polling.
  • You already operate a webhook receiver for other vendors and can integrate LuxID with minimal new infrastructure.

When pull (Event Hub) is preferable

  • Your back-end cannot guarantee high availability for a receiving endpoint.
  • You prefer to control back-pressure and handle bursty volumes yourself.
  • You need to back-fill historical events on demand - the Event Hub's time-range search handles this naturally.
  • You are building an audit trail or reconciliation job where ordering and completeness matter more than latency.

You may use both in the same integration: for example, use webhooks for low-latency consent revocation reactions and the Event Hub for nightly audit reconciliation.


Webhook Subscription management

Webhook subscriptions are managed via the Events API, which is part of the LuxID Partner API. Authenticate every management call the same way as any other Partner API call: with your Partner's static X-Client-Id and X-Client-Secret headers (not an OAuth token or a user access token). See Partner API - getting started for how authentication works.

Base URL is indicative

Verify the base URL with your LuxID Account manager before relying on it.

https://api.luxid.lu/events/subscriptions

Create a Subscription

POST /events/subscriptions HTTP/1.1
Host: api.luxid.lu
Content-Type: application/json
X-Client-Id: <your-client-id>
X-Client-Secret: <your-client-secret>

{
"target_url": "https://your-app.example.lu/webhooks/luxid",
"secret": "your-webhook-signing-secret-min-32-chars",
"event_filters": [
"user.consent.revoked",
"subscription.created",
"subscription.revoked",
"user.email.changed",
"user.phone.changed"
],
"description": "Production webhook for MyApp consent and subscription events"
}

Request fields

FieldTypeRequiredDescription
target_urlstringYesHTTPS URL that LuxID will POST events to. Must be publicly reachable and use TLS.
secretstringYesShared secret used to sign event payloads. Minimum 32 characters. Store securely - treat as a credential.
event_filtersarrayYesList of event types to receive. An empty array subscribes to all event types.
descriptionstringNoHuman-readable label for the subscription (useful in the Console).

Response

{
"id": "ws-7f3e9b1d-2a4c-4d5e-8f6a-1b2c3d4e5f6a",
"target_url": "https://your-app.example.lu/webhooks/luxid",
"event_filters": [
"user.consent.revoked",
"subscription.created",
"subscription.revoked",
"user.email.changed",
"user.phone.changed"
],
"status": "active",
"created_at": "2026-05-22T10:00:00Z"
}

List Subscriptions

curl -s \
-H "X-Client-Id: $LUXID_CLIENT_ID" \
-H "X-Client-Secret: $LUXID_CLIENT_SECRET" \
https://api.luxid.lu/events/subscriptions

Get a Subscription

curl -s \
-H "X-Client-Id: $LUXID_CLIENT_ID" \
-H "X-Client-Secret: $LUXID_CLIENT_SECRET" \
https://api.luxid.lu/events/subscriptions/ws-7f3e9b1d-2a4c-4d5e-8f6a-1b2c3d4e5f6a

Update a Subscription

curl -s -X PATCH \
-H "Content-Type: application/json" \
-H "X-Client-Id: $LUXID_CLIENT_ID" \
-H "X-Client-Secret: $LUXID_CLIENT_SECRET" \
--data-raw '{"event_filters":["subscription.revoked","user.consent.revoked"]}' \
https://api.luxid.lu/events/subscriptions/ws-7f3e9b1d-2a4c-4d5e-8f6a-1b2c3d4e5f6a

Delete a Subscription

curl -s -X DELETE \
-H "X-Client-Id: $LUXID_CLIENT_ID" \
-H "X-Client-Secret: $LUXID_CLIENT_SECRET" \
https://api.luxid.lu/events/subscriptions/ws-7f3e9b1d-2a4c-4d5e-8f6a-1b2c3d4e5f6a

Event payload structure

Every webhook delivery uses the same envelope regardless of event type:

{
"id": "evt-3a4b5c6d-7e8f-9a0b-c1d2-e3f4a5b6c7d8",
"timestamp": "2026-05-22T14:23:45.123Z",
"type": "subscription.revoked",
"version": "1",
"sphere": "MyApp",
"subscriber": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"application": "app-9z8y7x6w",
"subscription": "sub-app-9z8y7x6w-user-42",
"payload": {
"revoked_at": "2026-05-22T14:23:44.000Z",
"reason": "user_initiated"
}
}

This is the same envelope as the pull Partner API - Event Hub. Webhook-specific delivery metadata (event id, type, timestamp, nonce, and the matching subscription) is carried in the X-LuxID-* HTTP headers, not in the body - see Payload signing and verification.

Envelope fields

FieldTypeDescription
idstringGlobally unique event identifier. Use for idempotency checks.
timestampstringISO 8601 timestamp of when the event occurred.
typestringEvent type. The authoritative catalogue and payload shapes are in the LuxID OpenAPI document (see Events and Event Hub).
versionstringPayload schema version - increments when the payload shape changes.
sphere, subscriber, application, subscriptionstringOptional context fields, filled in when applicable. subscriber matches the sub in your tokens.
payloadobjectEvent-type-specific data. Shape varies by type.

Payload signing and verification

LuxID signs each webhook payload using JWS (RS256) with the LuxID signing key. The signature is included in the X-LuxID-Signature HTTP header as a compact JWS token.

Signature header example

POST /webhooks/luxid HTTP/1.1
Host: your-app.example.lu
Content-Type: application/json
X-LuxID-Signature: eyJhbGciOiJSUzI1NiIsImtpZCI6IkxVWElELTIwMjYtMDEifQ...
X-LuxID-Event-ID: evt-3a4b5c6d-7e8f-9a0b-c1d2-e3f4a5b6c7d8
X-LuxID-Event-Type: subscription.revoked
X-LuxID-Timestamp: 2026-05-22T14:23:45.123Z
X-LuxID-Nonce: n8k2p7qx

The JWS token in X-LuxID-Signature carries:

  • digest: SHA-256 of the raw request body (hex-encoded), prefixed SHA-256:.
  • timestamp: same as X-LuxID-Timestamp in the header.
  • nonce: same as the X-LuxID-Nonce header.

Verify the signature using the LuxID public signing key from the JWKS endpoint:

https://login.luxid.lu/mga/sps/oauth/oauth20/jwks/OIDC-LUXID

Verification steps

  1. Fetch the LuxID JWKS (cache it; rotate only when kid changes).
  2. Parse the JWS token in X-LuxID-Signature and extract the kid header.
  3. Find the matching key in the JWKS.
  4. Verify the JWS signature using RS256.
  5. Compare the digest in the JWS payload against your own SHA-256 of the raw request body.
  6. Verify the timestamp is within 5 minutes of your server clock.
  7. Check the nonce has not been seen before (within the replay window).
Reject the event if any step fails

Respond with 400 Bad Request to signal the delivery failure.

Webhook verification

import hashlib
from datetime import datetime, timezone
import jwt # PyJWT >= 2.0

JWKS_URL = "https://login.luxid.lu/mga/sps/oauth/oauth20/jwks/OIDC-LUXID"
REPLAY_WINDOW_SECONDS = 300 # 5 minutes

jwks_client = jwt.PyJWKClient(JWKS_URL)
seen_nonces = set() # Use Redis or a DB in production


def verify_luxid_webhook(request_body: bytes, headers: dict) -> bool:
signature_token = headers.get("X-LuxID-Signature")
event_timestamp = headers.get("X-LuxID-Timestamp")
nonce = headers.get("X-LuxID-Nonce")

if not all([signature_token, event_timestamp, nonce]):
return False

# Verify JWS signature
try:
signing_key = jwks_client.get_signing_key_from_jwt(signature_token)
claims = jwt.decode(
signature_token,
signing_key.key,
algorithms=["RS256"],
options={"verify_aud": False},
)
except jwt.PyJWTError:
return False

# Verify body digest
expected_digest = "SHA-256:" + hashlib.sha256(request_body).hexdigest()
if claims.get("digest") != expected_digest:
return False

# Verify timestamp within replay window
try:
event_time = datetime.fromisoformat(event_timestamp.replace("Z", "+00:00"))
age_seconds = (datetime.now(timezone.utc) - event_time).total_seconds()
if abs(age_seconds) > REPLAY_WINDOW_SECONDS:
return False
except ValueError:
return False

# Replay protection via nonce
if nonce in seen_nonces:
return False
seen_nonces.add(nonce)

return True

Delivery semantics

LuxID webhook delivery guarantees at-least-once delivery: your endpoint may receive the same event more than once, so your handler must be idempotent.

This means:

  • LuxID will retry failed deliveries - your endpoint may receive the same event more than once.
  • Your handler must be idempotent: processing the same event twice must produce the same outcome as processing it once. Use the id field as your idempotency key.

Retry policy

LuxID retries delivery with exponential backoff on the following conditions:

  • HTTP response code 5xx (server error).
  • HTTP response code 429 (too many requests).
  • Connection timeout (no response within 10 seconds).
  • No response at all (connection refused, DNS failure).

LuxID does not retry on 2xx (success) or 4xx other than 429.

Retry schedule (approximate)

AttemptDelay
1Immediate
230 seconds
32 minutes
410 minutes
51 hour

After all retries are exhausted, the event is marked as permanently failed. You can retrieve missed events via the Event Hub.


Replay protection

To prevent replay attacks (an attacker re-sending a captured webhook payload):

  1. Timestamp check: reject events where X-LuxID-Timestamp is more than 5 minutes in the past or future. Ensure your server clock is synchronised via NTP.
  2. Nonce check: record the X-LuxID-Nonce value for every accepted event. Reject duplicates. Store nonces for at least 10 minutes.
  3. Signature verification: the JWS signature binds the nonce and timestamp to the body. A replayed event with a modified body or timestamp will fail signature verification.

Responding to webhook deliveries

Your endpoint must respond with an HTTP 2xx status within 10 seconds. Acknowledge immediately and process asynchronously:

# FastAPI example - immediate acknowledgement, async processing
from fastapi import FastAPI, Request, BackgroundTasks, HTTPException
import json

app = FastAPI()


@app.post("/webhooks/luxid")
async def receive_webhook(request: Request, background_tasks: BackgroundTasks):
raw_body = await request.body()

if not verify_luxid_webhook(raw_body, dict(request.headers)):
raise HTTPException(status_code=400, detail="Invalid signature")

event = json.loads(raw_body)
background_tasks.add_task(process_event, event)
return {"status": "accepted"}


async def process_event(event: dict):
if event.get("type") == "subscription.revoked":
pass # Clear user session, update subscription state, etc.

Common event types

Event typeDescription
subscription.createdA user has authorised your application for the first time
subscription.revokedA user has revoked your application's access
user.consent.revokedA user has withdrawn consent for specific claims
user.email.changedThe user's email address has changed
user.phone.changedThe user's phone number has changed
user.account.deletedThe user's LuxID Account has been deleted
user.identity.verifiedThe user has completed LuxID Verified (LuxTrust)

For the full event catalogue with payload schemas, see Events and Event Hub.


Integration checklist

  • Use HTTPS with a valid TLS certificate on your webhook endpoint.
  • Verify the X-LuxID-Signature on every incoming request before processing.
  • Reject events older than 5 minutes (X-LuxID-Timestamp check).
  • Track nonces to prevent replay attacks.
  • Return 200 OK within 10 seconds - process asynchronously if needed.
  • Make your handler idempotent using id as the idempotency key.
  • Store your webhook secret as a credential (secret manager, environment variable) - never in source code.
  • Back-fill missed events via the Event Hub after any downtime.

Mise à jour le 2026-05-22