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-Signature header?
  • How do I handle guest.registered, ticket.registered, or event.created events?
  • 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 time
  • Webhook-Id is 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_TIMESTAMP_TOLERANCE_SECONDS 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.

EventDescription
calendar.event.addedAn event was added to your calendar (created on it, approved, transferred in, or made public)
calendar.event.submittedAn event was submitted to your calendar and awaits approval
calendar.person.subscribedSomeone followed your calendar
calendar.person.unsubscribedA person opted out of hearing from your calendar
event.createdA new event was created on your calendar
event.updatedEvent details or a ticket type changed
event.canceledAn event was canceled
guest.registeredSomeone registered, or a host added a guest
guest.updatedA registration changed (check-in, approval status, answers, tickets)
guest.refundedA host refunded a guest's payment through Luma
ticket.registeredOne ticket was purchased (3 tickets = 3 events)

For payload schemas, see Luma API webhook types and references/overview.md.

Important Headers

HeaderDescription
Webhook-Signaturet=<unix seconds>,v1=<hex HMAC-SHA256>
Webhook-IdUnique ID for the webhook event (idempotency key)
Webhook-TimestampUnix 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


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 5, 2026

View on GitHub →