Grofomo API
Browse the docs

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 rendered message string. 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.