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}}, {{artist_name}}, {{artist_credit}}

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}}

site.content_changed — Website content changed

Content the public website reads has changed: a news article, the lineup or the FAQs. Point this at a static host’s deploy hook (Cloudflare Pages, Netlify, Vercel) and a custom-built site rebuilds itself whenever you publish, instead of waiting for someone to push. Changes are batched: each one waits about two minutes for the next, so a whole editing session arrives as a single notification and a bulk edit triggers one rebuild rather than fifty. A change made on its own therefore reaches you a couple of minutes after you make it, and a long session is never more than fifteen minutes behind.

Placeholders: {{content_type}}, {{event_name}}, {{event_slug}}, {{changed_at}}

social.reconnect_required — Social account disconnected

A connected social account can no longer publish, because its token expired or was revoked. Scheduled posts to it will fail until someone reconnects it in Settings.

Placeholders: {{platform}}, {{account_handle}}, {{reason}}, {{settings_url}}

whatsapp.template_decision — WhatsApp template decision

Meta has approved, rejected, paused or switched off one of your WhatsApp message templates. A paused or rejected template cannot be used in a campaign until it is fixed or replaced.

Placeholders: {{template_name}}, {{language}}, {{decision}}, {{reason}}, {{sender_number}}, {{templates_url}}

whatsapp.campaign_held — WhatsApp campaign held back

A scheduled WhatsApp campaign did not send, because Meta now reports many complaints about the message it uses and is close to stopping that message sending. Nobody was messaged and nothing was lost: the campaign stays scheduled until the message is changed or the campaign is cancelled.

Placeholders: {{template_name}}, {{language}}, {{scheduled_for}}, {{recipient_count}}, {{campaign_url}}

whatsapp.allowance_change — WhatsApp sending allowance changed

Meta has changed how many different people one of your WhatsApp numbers can reach in any 24 hours. It goes up on its own when campaigns are well received, and down when too many recipients block or report them. A campaign bigger than the allowance is delivered in daily batches rather than failing.

Placeholders: {{direction}}, {{allowance}}, {{previous_allowance}}, {{summary}}, {{sender_number}}, {{settings_url}}

whatsapp.account_alert — WhatsApp notice from Meta

Meta has raised a notice about one of your WhatsApp numbers that needs a person: a hold or refusal on raising its sending allowance, a decision on the official business account badge, or a profile photo that has been removed. Sent once per notice; the same notice also appears on your WhatsApp settings page until it is cleared or acknowledged.

Placeholders: {{alert}}, {{severity}}, {{status}}, {{summary}}, {{what_to_do}}, {{meta_note}}, {{sender_number}}, {{settings_url}}

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.