Funnel beacon
/v1/events/{slug}/tickets/funnelOptional. The organiser's sales report shows a funnel from viewing tickets to buying them; the reserve and the payment write their own steps, but the top of the funnel only exists if your page reports it.
Mint an opaque token per visit (a UUID is fine), send event_viewed when the ticket list renders and ticket_selected the first time a quantity goes above zero, and pass the same token as funnelSessionToken on reserve so the steps join up. Send both or neither: beacons without the reserve token, or the reverse, show a funnel that never converts.
Fire and forget: the response is always { ok: true }, and a failure must never block a checkout. navigator.sendBeacon or fetch with keepalive both work.
404 not_found for no visible reason.Request
curl -sS \
-X POST \
-H 'X-Grofomo-Key: pk_live_YOUR_PUBLISHABLE_KEY' \
-H 'Content-Type: application/json' \
-d '{"step":"event_viewed","sessionToken":"9d1f7c2a-3b6e-4a8d-9e0f-1c2b3a4d5e6f"}' \
'https://api.grofomo.com/v1/events/summer-fest/tickets/funnel?surface=web'Request body
step"event_viewed" | "ticket_selected" | "checkout_started" | "purchased"required- Send `event_viewed` when the ticket list renders and `ticket_selected` the first time a quantity goes above zero. `checkout_started` and `purchased` are recorded server-side from the reserve and the payment; do not send them.
sessionTokenstringrequired- An opaque per-visit token you mint (a UUID is fine), kept for the visit and sent again as `funnelSessionToken` on reserve. Not a credential and never shown to anyone.
orderPublicIdstring- The order's `publicId` once one exists. Optional.
deviceHandoffstring- Internal: a signed hand-off from the Grofomo mobile app. Leave it out.
Path parameters
slugstringrequired- The event's URL slug. Globally unique. It can change if the organiser renames the event, but an old slug keeps reaching the same event, so a stored one never breaks. Example: `summer-fest`.
Query parameters
surface'web'required- Must be `web` for a website. Omit it and you get the mobile-app surface, which is gated on a different visibility setting — so a perfectly live event can return 404. Example: `web`.
Response
{
"apiVersion": 1,
"data": {
"ok": true
}
}okboolean- Always `true`, even when nothing was recorded.
Errors
unauthorizedHTTP 401- No key was sent and the organiser requires one — or the key is invalid, revoked, belongs to another organisation, or is scoped to a different event.
origin_not_allowedHTTP 403- Your site's domain is not on the organiser's allowed list. Browser calls only: a server sends no Origin header and is unaffected.
not_foundHTTP 404- No event with that slug is published on this surface. Check the slug, and check you sent `?surface=web`.
bad_requestHTTP 400- The body did not match the schema.
Notes
- Rate limited to roughly 120 requests per minute per caller. Guidance, not a contract — see caching and rate limits.
Cache-Control: no-store