Gareth Wilson Gareth Wilson

Guide to Luma Webhooks: Features and Best Practices

Published


Luma webhooks tell your systems what happens on your Luma calendar: an event is created or canceled, a guest registers or is checked in, a ticket is bought, or someone follows or unsubscribes from the calendar. If you run events on Luma, the event hosting and ticketing platform (not Luma AI), webhooks push that activity to your CRM, email tool or database as it happens.

This guide covers how Luma webhooks are created and signed, the events and payloads you will receive, the short retry window that makes Luma unforgiving of downtime, and the practices that keep a Luma receiver reliable.

What are Luma webhooks?

A Luma webhook belongs to a calendar. You create it in the calendar's settings with a URL and the action types you want, and Luma sends a JSON POST to the URL whenever one of them happens. Webhooks are available only on Luma Plus.

Each body has two top-level fields: type, the event name, and data, the event, guest, ticket or person it concerns.

Every request carries three headers: Webhook-Signature, Webhook-Id and Webhook-Timestamp. The names and the whsec_ secret look like Standard Webhooks, but the scheme is different. Webhook-Signature has the form t=<timestamp>,v1=<signature>, the signature is a hex HMAC-SHA256 of the timestamp and the raw body, and the key is the whole secret string. A Standard Webhooks library will reject every request.

Luma webhook features

FeatureDetails
AvailabilityLuma Plus subscribers
ConfigurationCalendar Settings > Developer > Webhooks
SubscriptionSelected action types, or All Actions
Signature headerWebhook-Signature: t=<unix seconds>,v1=<hex>
Other headersWebhook-Id (unique per event), Webhook-Timestamp (Unix seconds)
SchemeHMAC-SHA256, hex
Signed content<t>.<raw body>
Secretwhsec_..., generated when you create the webhook, used whole as the key
Event typetype in the body
Response window5 seconds
RetriesUp to 3, at 1, 2 and 4 minutes
Auto-pauseA 410 Gone response pauses the webhook
ManagementPause, resume, delete, and view recent deliveries

Luma does not publish source IPs.

Common events

Luma documents 11 event types.

AreaEventFires when
Calendarcalendar.event.addedAn event appears on your calendar, including syndicated and external events
Calendarcalendar.event.submittedAn event is submitted to your calendar and is pending approval
Calendarcalendar.person.subscribedSomeone follows your calendar
Calendarcalendar.person.unsubscribedA person opts out of hearing from your calendar
Eventsevent.createdA new event is created on your calendar
Eventsevent.updatedEvent details change, or a ticket type is added, edited or removed
Eventsevent.canceledAn event is canceled
Guestsguest.registeredSomeone registers, a host adds a guest, or a guest registers again after canceling
Guestsguest.updatedA registration is created or changes, such as check-in, approval status, answers or tickets
Guestsguest.refundedA host refunds a guest through Luma, including partial refunds
Ticketsticket.registeredEach individual ticket is purchased or registered

A few of these behave in ways worth planning for:

  • guest.registered fires once per person. ticket.registered fires once per ticket, so a guest who buys three tickets produces three. Use the first for welcome emails and the second for per-ticket badges or QR codes.
  • guest.updated also fires for new registrations, so a new guest triggers both guest.registered and guest.updated. It does not fire when a guest edits their Luma profile.
  • event.updated does not fire for guest registrations.
  • guest.refunded does not fire for refunds issued directly from the Stripe dashboard.
  • calendar.person.unsubscribed covers opt-outs only from the moment you create the webhook. Its payload includes the person's email address, which makes it useful for mirroring suppressions to other email tools.
  • The help centre's list of action types does not include Calendar Event Submitted, although the API reference documents it. Check that it is offered when you create the webhook.

Luma publishes a schema for each event in its API reference, but no complete example body. Every body has the same envelope:

{
  "type": "guest.registered",
  "data": { }
}

data is a guest object for guest.* events, an event object for event.* and calendar.event.*, a guest with its ticket for ticket.registered, and a calendar person for calendar.person.*. For the mapping between dashboard labels such as Going or Pending and the approval_status values in guest payloads, see Luma's Guest Status Values page.

Setting up Luma webhooks

  1. Open the Calendars Home and select your calendar.
  2. Go to Settings > Developer.
  3. In the Webhooks section, click Create.
  4. Enter your HTTPS endpoint URL.
  5. Select the action types you want, or All Actions.
  6. Click Create, and store the generated whsec_ secret server-side, for example as LUMA_WEBHOOK_SECRET.

From the same page you can open a webhook to see its recent deliveries (delivered, pending or failed), pause and resume it, or delete it.

Securing Luma webhooks

Split Webhook-Signature on commas and read t and v1. Build the string <t>.<raw body>, compute an HMAC-SHA256 with the full whsec_... secret as the key, hex-encode it, and compare it with v1 in constant time.

const crypto = require('crypto');
const express = require('express');

function verifyLumaSignature(rawBody, header, secret) {
  if (!header || !secret) return false;
  let timestamp;
  const signatures = [];
  for (const part of header.split(',')) {
    const i = part.indexOf('=');
    if (i < 0) continue;
    const key = part.slice(0, i).trim();
    const value = part.slice(i + 1).trim();
    if (key === 't') timestamp = value;
    else if (key === 'v1') signatures.push(value);
  }
  if (!/^\d+$/.test(timestamp ?? '') || signatures.length === 0) return false;

  const expected = Buffer.from(
    crypto.createHmac('sha256', secret).update(`${timestamp}.`).update(rawBody).digest('hex'),
    'utf8'
  );
  return signatures.some((sig) => {
    const received = Buffer.from(sig.toLowerCase(), 'utf8');
    return received.length === expected.length && crypto.timingSafeEqual(received, expected);
  });
}

const app = express();

app.post('/webhooks/luma', express.raw({ type: '*/*' }), (req, res) => {
  const secret = process.env.LUMA_WEBHOOK_SECRET;
  if (!secret) return res.sendStatus(500); // fail closed

  if (!verifyLumaSignature(req.body, req.get('webhook-signature'), secret)) {
    return res.sendStatus(401);
  }

  // Optional: reject t values far from the current time. Choose a window
  // generous enough for Luma's retries and for any replay you run yourself.

  const event = JSON.parse(req.body.toString('utf8'));
  // Dedupe on req.get('webhook-id'), route on event.type, process async.
  res.sendStatus(200);
});

The same check in Python:

import hashlib
import hmac
import re


def verify_luma_signature(raw_body: bytes, header, secret: str) -> bool:
    if not header or not secret:
        return False
    timestamp, signatures = None, []
    for part in header.split(","):
        key, _, value = part.strip().partition("=")
        if key == "t":
            timestamp = value
        elif key == "v1":
            signatures.append(value)
    if not timestamp or not re.fullmatch(r"[0-9]+", timestamp) or not signatures:
        return False
    expected = hmac.new(
        secret.encode("utf-8"), timestamp.encode() + b"." + raw_body, hashlib.sha256
    ).hexdigest()
    return any(
        hmac.compare_digest(sig.lower().encode("utf-8"), expected.encode("utf-8"))
        for sig in signatures
    )


# In FastAPI: raw_body = await request.body()
# verify_luma_signature(raw_body, request.headers.get("webhook-signature"),
#                       os.environ["LUMA_WEBHOOK_SECRET"])

Luma's own samples build the signed string from the body as a string. Hashing the raw bytes, as above, avoids any difference introduced by decoding. Luma recommends rejecting timestamps "more than a few minutes old" but does not set a number, so treat any window as your own choice and keep it wide enough not to reject deliveries that arrive late for ordinary reasons.

Make Luma webhooks production-ready. Hookdeck Event Gateway verifies Luma's Webhook-Signature header against the timestamp and raw body, answers Luma with an immediate 200 OK, and durably queues every guest, ticket and event update before your service sees it.

Luma webhook limitations and pain points

Seven minutes of retries

The Problem: A short outage loses events for good.

Why It Happens: Luma retries a failed delivery at most three times, at 1, 2 and 4 minutes. If your endpoint is down for longer than about seven minutes, the event is not sent again, and Luma documents no way to replay it.

Workarounds:

  • Keep the receiver highly available and its work minimal: verify, store, return 2xx.
  • After an outage, reconcile registrations and events through the Luma API.
  • Check the webhook's recent deliveries for failures.

How Hookdeck Can Help: Hookdeck accepts each delivery within Luma's 5-second window and stores it, then retries delivery to your service for as long as you configure and lets you replay any event. Luma's short retry schedule stops being your outage budget.

Standard Webhooks look-alike

The Problem: A Standard Webhooks or Svix verifier rejects every Luma request.

Why It Happens: Luma uses Webhook-* header names and a whsec_ secret, but signs <t>.<body> with the whole secret string and sends hex in a t=,v1= header. Standard Webhooks signs <id>.<timestamp>.<body> with a decoded key and sends base64.

Workarounds:

  • Use a plain HMAC-SHA256 as shown above.
  • Do not strip whsec_ or base64-decode the secret.

How Hookdeck Can Help: Hookdeck's Luma source type implements Luma's scheme, so you paste the secret as issued and Hookdeck checks the signature.

A 5-second response window

The Problem: Handlers that call your CRM or send email time out and are retried.

Why It Happens: Luma expects a response within 5 seconds, which is shorter than many providers allow.

Workarounds:

  • Acknowledge first and move every external call into a background job.
  • Never return 410 unless you want the webhook paused.

How Hookdeck Can Help: Hookdeck responds to Luma at once and delivers to your service with its own timeout, so slow downstream calls no longer cause Luma retries.

Overlapping events

The Problem: One registration triggers several deliveries, and naive handlers act on each of them.

Why It Happens: guest.registered and guest.updated both fire for a new registration, and ticket.registered fires once per ticket. Luma also notes that the same event can occasionally arrive twice.

Workarounds:

  • Subscribe only to the events you handle.
  • Choose one event per action, such as guest.registered for welcome emails.
  • Deduplicate on Webhook-Id.

How Hookdeck Can Help: Hookdeck filters can route each type to a different destination, and deduplication can drop repeats.

Best practices

Verify against the raw body

The signature covers the exact bytes after the timestamp. Use express.raw() in Express, await request.text() in a Next.js route handler and await request.body() in FastAPI.

Deduplicate on Webhook-Id

Webhook-Id is a unique ID for the event. Record it before processing and skip ids you have already handled. See our guide to webhook idempotency.

Pick guest or ticket events deliberately

Use guest.registered for once-per-person actions and ticket.registered for once-per-ticket actions. Handling both for the same purpose sends duplicates.

Fetch profile details when you need them

guest.updated does not fire for profile edits, so names and emails in your copy can go stale. Fetch the guest through the API when current profile data matters.

Acknowledge within 5 seconds

Verify, store the event, return 200, and process afterwards. See why to process webhooks asynchronously.

Conclusion

Luma signs each webhook with Webhook-Signature: t=...,v1=..., a hex HMAC-SHA256 of the timestamp and raw body keyed with the whole whsec_ secret. Verify with a plain HMAC rather than a Standard Webhooks library, deduplicate on Webhook-Id, choose between guest and ticket events on purpose, and answer within 5 seconds, because three retries over seven minutes leave little room for downtime.

Hookdeck Event Gateway sits in front of your receiver, checks every Luma signature, and holds each event until your service has processed it.

Get started with Hookdeck for free and handle Luma webhooks reliably in minutes.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.