Agent skill
Luma Webhooks Skill
Receive and verify Luma (lu.ma events platform, not Luma AI) webhooks. Use when setting up Luma webhook handlers, debugging Webhook-Signature verification, or handling events like guest.registered, ticket.registered, event.created, or calendar.person.unsubscribed.
Install this skill
npx skills add hookdeck/webhook-skills --skill luma-webhooks
Luma is the events and calendar platform (lu.ma / luma.com), not Luma AI. Webhooks are available to Luma Plus subscribers.
When to Use This Skill
- How do I receive Luma webhooks?
- How do I verify the Luma
Webhook-Signatureheader? - How do I handle
guest.registered,ticket.registered, orevent.createdevents? - How do I sync Luma unsubscribes (
calendar.person.unsubscribed) to my CRM or email tool? - Why is my Luma webhook signature verification failing?
Essential Code (USE THIS)
Luma sends Webhook-Signature: t=<unix seconds>,v1=<hex>, plus Webhook-Id and Webhook-Timestamp. Despite the webhook-* header names and the whsec_ secret, this is not Standard Webhooks: do not use the standardwebhooks or svix libraries, and do not strip or base64-decode the whsec_ prefix.
- Signed string:
"<t>.<raw request body>" - Key: the whole secret string (including
whsec_) as UTF-8 v1= hex HMAC-SHA256, compared in constant timeWebhook-Idis not part of the signed content (use it for idempotency)
Verification (Node.js)
const crypto = require('crypto');
// rawBody: Buffer of the exact request bytes (express.raw, not express.json)
function verifyLumaWebhook(rawBody, header, secret) {
if (!header || !secret) return false;
const parts = header.split(',').map((p) => p.trim().split('='));
const t = parts.find(([k]) => k === 't')?.[1];
const sigs = parts.filter(([k]) => k === 'v1').map(([, v]) => v);
if (!t || !/^\d+$/.test(t) || sigs.length === 0) return false;
// Optional replay window, off by default: Hookdeck retries and replays keep the original t
const expected = crypto.createHmac('sha256', secret).update(`${t}.`).update(rawBody).digest('hex');
return sigs.some((s) => {
try { return crypto.timingSafeEqual(Buffer.from(s), Buffer.from(expected)); } catch { return false; }
});
}
Verification (Python)
import hashlib, hmac
def verify_luma_webhook(raw_body: bytes, header: str | None, secret: str) -> bool:
if not header or not secret:
return False
pairs = [p.strip().split("=", 1) for p in header.split(",") if "=" in p]
t = next((v for k, v in pairs if k == "t"), None)
sigs = [v for k, v in pairs if k == "v1"]
if not t or not t.isdigit() or not sigs:
return False
expected = hmac.new(secret.encode(), t.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
return any(hmac.compare_digest(s, expected) for s in sigs)
Luma suggests rejecting timestamps more than a few minutes old. The examples leave that off by default, because Hookdeck's retries and replays keep the original t, and Luma doesn't say whether its own retries send a new one. Set LUMA_ to a positive number of seconds to opt in.
For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.
Common Event Types
The body is { "type": "<event type>", "data": { ... } }. Subscribe to specific types (or all) when creating the webhook.
| Event | Description |
|---|---|
calendar.event.added | An event was added to your calendar (created on it, approved, transferred in, or made public) |
calendar.event.submitted | An event was submitted to your calendar and awaits approval |
calendar.person.subscribed | Someone followed your calendar |
calendar.person.unsubscribed | A person opted out of hearing from your calendar |
event.created | A new event was created on your calendar |
event.updated | Event details or a ticket type changed |
event.canceled | An event was canceled |
guest.registered | Someone registered, or a host added a guest |
guest.updated | A registration changed (check-in, approval status, answers, tickets) |
guest.refunded | A host refunded a guest's payment through Luma |
ticket.registered | One ticket was purchased (3 tickets = 3 events) |
For payload schemas, see Luma API webhook types and references/overview.md.
Important Headers
| Header | Description |
|---|---|
Webhook-Signature | t=<unix seconds>,v1=<hex HMAC-SHA256> |
Webhook-Id | Unique ID for the webhook event (idempotency key) |
Webhook-Timestamp | Unix timestamp (seconds) when the webhook was sent |
Environment Variables
LUMA_WEBHOOK_SECRET=whsec_xxxxx # Shown when you create the webhook; used whole as the HMAC key
LUMA_TIMESTAMP_TOLERANCE_SECONDS=0 # Optional; a positive value opts in to the replay window
Local Development
# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 luma --path /webhooks/luma
Reference Materials
- references/overview.md - Luma webhook concepts, events, retries
- references/setup.md - Creating a webhook in Luma
- references/verification.md - Signature verification details and gotchas