Webhooks
Receive ticket sales and signups on your own endpoint, with signature verification.
Examples use a fictional festival, summer-fest. Sign in at events.grofomo.com and they switch to your own events.
Webhooks push events to you as they happen, so you do not have to poll for a ticket sale.
They are configured by the organiser, in EventOS → Settings → Webhooks: your URL, which events to send, and a format. You cannot register one yourself — the organiser owns the data and therefore the decision.
Formats
json— the structured payload plus a renderedmessagestring. This is the one to pick for your own endpoint.slack—{"text": "…"}, ready to post straight into a Slack incoming webhook.
Verifying a delivery
Every delivery carries an HMAC-SHA256 signature of the raw request body, keyed on the organiser's signing secret:
X-Eventos-Signature: hmac-sha256=<hex digest>Verify it before trusting anything in the body. Use the raw body, not a re-serialised object — JSON.parse followed by JSON.stringify will not reproduce the same bytes, and the signature will not match.
import { createHmac, timingSafeEqual } from 'node:crypto';
export function verify(rawBody, headerValue, signingSecret) {
const expected = createHmac('sha256', signingSecret)
.update(rawBody)
.digest('hex');
const received = (headerValue ?? '').replace('hmac-sha256=', '');
if (received.length !== expected.length) return false;
// Constant-time compare: a plain === leaks the digest one byte at a time.
return timingSafeEqual(Buffer.from(received), Buffer.from(expected));
}Retries
A delivery that fails is retried up to five times with backoff — roughly 1 minute, 5 minutes, 30 minutes, 2 hours, then a final attempt — before being marked dead.
So: return 2xx fast. Acknowledge first and do the work afterwards. A slow handler will be retried while it is still running, and the same event will arrive twice. Make your handler idempotent.
Events
ticket.sale — Ticket sale
A paid ticket order completes (web, app, or door).
Placeholders: {{buyer_name}}, {{buyer_first_name}}, {{buyer_email}}, {{event_name}}, {{items}}, {{ticket_count}}, {{order_total}}, {{order_number}}
merch.sale — Merch sale
A merch order is paid via checkout.
Placeholders: {{buyer_name}}, {{buyer_email}}, {{event_name}}, {{items}}, {{item_count}}, {{order_total}}, {{order_number}}
ticket.refund — Ticket refund
A ticket order is fully or partially refunded.
Placeholders: {{buyer_name}}, {{event_name}}, {{order_number}}, {{refund_amount}}, {{refund_total}}, {{order_total}}, {{refund_scope}}
merch.refund — Merch refund
A merch order is fully or partially refunded.
Placeholders: {{buyer_name}}, {{event_name}}, {{order_number}}, {{refund_total}}, {{order_total}}, {{refund_scope}}
comp.issued — Comps issued
Complimentary tickets are issued by the organiser team.
Placeholders: {{guest_name}}, {{quantity}}, {{ticket_type}}, {{event_name}}
comp.redeemed — Artist comp redeemed
An artist sends a comp ticket from their remittance-offer allocation.
Placeholders: {{artist_name}}, {{recipient_name}}, {{event_name}}, {{remaining}}
prelaunch.registration — Prelaunch registration
An attendee registers interest on a prelaunch page.
Placeholders: {{event_name}}, {{source_event_name}}, {{first_name}}, {{contact_email}}, {{channels}}
mailing_list.signup — Mailing list signup
Someone joins the mailing list via a signup form.
Placeholders: {{event_name}}, {{form_title}}, {{contact_email}}
A note on email placeholders
Several events expose an email placeholder, but no default template uses one. That is deliberate: a subscriber's address should not land in a shared Slack channel because nobody thought about it. An organiser who adds {{contact_email}} to a template is making that choice on purpose.