Changelog
What changed, and how versioning works.
Examples use a fictional festival, summer-fest. Sign in at events.grofomo.com and they switch to your own events.
How versioning works
The API is at apiVersion: 1, and stays there while changes are additive — new fields, new endpoints, new optional parameters.
Which means: ignore fields you do not recognise. A client that validates strictly against today's exact shape will break on a change designed not to break anything.
A genuinely breaking change would ship as a new version, announced here first, with the old one kept alive during the overlap.
Changes
8 October 2026 - artist links reach your own ticket page
An artist's Grofomo link (grfm.to/e/<event>/a/<artist>) for an event
that sells on your own site now arrives there with ?artist=<artistId>
and the link's other tags, where before it arrived bare and the artist was
lost. checkout.js 1.10.0 reads it: the drop-in lists the tickets kept
for that artist's fans and credits the sale to them, and it now also sends
the ambassador code when it loads the catalog, so an affiliate-gated ticket
is listed from the start. A site with its own checkout gets
GrofomoCheckout.artist(), and api.catalog takes the buyer's
credentials as a second argument (see Selling tickets).
Additive. Not part of the /v1 contract.
5 October 2026 - say whether the person who shared the link may see the buyer's name
The details step takes a new optional boolean, showNameToReferrer. When a
buyer came through an artist's, Ambassador's or other affiliate's link, that
person can now see who came through it by first name and surname initial
("Matt S"), with the ticket count, the time, and whether they finished
buying. Send true only when your checkout printed a notice saying so and
the buyer did not tick a box to hide it; leave it out otherwise and the order
stays anonymous to them, exactly as before. Additive: a client that never
sends it sees no change.
30 September 2026 - lineup artists say whether they are verified
Every artist on a lineup, and the
artist detail response, has a new boolean,
verified. It is true when the artist has claimed their Grofomo
profile, so the name, photo and bio come from the artist themselves rather
than the organiser's notes. Show a verified mark beside the name, as the
hosted event sites now do. Additive: on the lineup it is always sent, and a
client that ignores it sees no change.
30 September 2026 - checkout.js tags its pixel events with the event
checkout.js 1.9.2 adds grofomo_event (the event's slug) to the
ViewContent and Purchase it reports to your page's Meta Pixel, and the
purchase Grofomo sends server-side carries it too. The custom audiences a
Grofomo campaign builds match on it, so people who viewed or bought through
the drop-in on your own site now count in the event's retargeting and buyer
audiences; before this those audiences only counted the Grofomo ticket page.
A page firing its own events (pixel: false) should add it as well. Additive
(see Selling tickets). Not part of the /v1 contract.
30 September 2026 - artists have a page of their own
Two additive shapes for the attendee app, documented here because they are
part of the /v1 contract. GET /v1/artists/{slug} is the artist
behind a free artist page (grfm.to/a/{slug}) outside any event: profile,
socials, follower count, and their released upcoming shows in events the
app can open. GET and PUT /v1/devices/me/artist-follows let a
device follow an artist across events, on the signed device-token surface
the app already uses for favourites. Neither needs an organiser API key.
30 September 2026 - search cards carry the logo's colour
Every result from event search, and each of an
organiser's upcomingEvents, has two new optional fields.
logoBackgroundColor is the hex colour to sit the card's logo on (the
event's page background, else its first brand colour), so a listing can
paint the thumbnail in its brand colour straight away instead of waiting
for the event's own record. logoVectorUrl is the hosted SVG logo when
the organiser uploaded one. Both are null when there is nothing to send.
29 September 2026 - prelaunch.registration says what it confirms
prelaunch.registration still fires once per proven channel, so a
registrant who types the SMS code and later clicks the email link arrives
twice. Two new message tags tell the two apart: {{confirmed_channels}}
is the channel this notification confirms (where {{channels}} is
everything granted so far), and {{registration_kind}} is New the
first time and Updated after. The default message now uses both. A
resubmission of the form that grants nothing new no longer fires at all.
JSON subscribers get the same two facts as additive payload fields:
registrationKind (new or updated) and confirmedChannels
(e.g. ["whatsapp"]).
29 September 2026 - carer tickets
Four new endpoints let an organiser's own site offer free carer tickets:
carer tickets says whether the form is
open and carries its words, then match,
upload proof and
apply are the form's three steps. The
organiser approves or denies each application in Grofomo; approval emails
the carer a free ticket in the attendee's own order, so the door checks the
pair in together. Three webhook events go with it, carer.applied,
carer.approved and carer.denied (see webhooks). Additive:
nothing changes unless the organiser switches carer tickets on.
29 September 2026 - a public MCP server for the event directory
https://api.grofomo.com/mcp/events is a read-only, keyless MCP server
with three tools: search_events, get_event and get_lineup. It reads
what the directory and /v1 already publish, so an assistant can answer
from the source and cite the event page. Not part of the /v1 contract;
see MCP server.
29 September 2026 - the server-side Purchase to Meta needs the buyer's consent
Reserve and details
take adConsent: whether the buyer agreed to advertising measurement on
your page. Grofomo posts the purchase to the organiser's Meta Pixel
server-side only when it is true, now with the buyer's email hashed for
matching, and posts nothing for an order without it. Before this the
purchase was sent for every order. Additive on the wire, but a page that
sends nothing stops being reported to Meta: checkout.js 1.9.1 reads your
cookie banner and sends it for you (see Selling tickets).
29 September 2026 - checkout.js hands your page the amount paid
checkout.js 1.9.0 reports to a Meta Pixel the page has already loaded:
ViewContent when the tickets are listed and Purchase when the order
completes, with the order id as the event id so it dedupes against the
purchase Grofomo sends server-side. It loads no pixel itself; pixel: false
on mount turns the reporting off. It also adds totalPennies (what the
buyer paid today, the deposit on a payment plan) and currency to the
onComplete callback, so a page can count the sale on its own analytics
without a second request. Additive (see Selling tickets). Not
part of the /v1 contract.
24 September 2026 - quote prices every payment plan the cart can take
Quote now returns paymentPlans: each plan the
cart can take right now, with the deposit, the balance and the schedule
reserve would write, in the same shape as paymentPlan on reserve.
Additive. A client that worked the figures out from the offer's terms could
disagree with reserve: a fixed per-ticket deposit counts only paid units, so
a ticket a buy-N-get-M deal makes free carries none, and counting it could
push the instalments under the £5 floor and hide a plan reserve would have
accepted. checkout.js 1.8.1 uses the quote's figures in the drop-in and
in GrofomoCheckout.planOptions, falling back to its own estimate only
against a server that does not send them.
24 September 2026 - checkout.js sells payment plans
checkout.js 1.8.0 offers an event's payment plans in the drop-in. When
every paid tier in the basket lists a plan, the buyer chooses between paying
in full and each plan, priced for their basket, and sees the exact schedule
from reserve before paying the deposit. A plan that closes before reserve
falls back to paying in full with an explanation. Sites that render their
own checkout can price the same choice with the new
GrofomoCheckout.planOptions(catalog, items, quote). No code change is
needed on your side.
24 September 2026 - checkout.js carries tags on your links, and hands a custom page its funnel token
checkout.js 1.7.0 adds three things for a site that renders its own
checkout on the client. GrofomoCheckout.withTags(href) returns a link to
another of your pages with this page's tags added, so a sale later in the
same visit is credited without storing anything. GrofomoCheckout.affiliate()
returns the ambassador code to send as affiliateCode, sticky across pages
once the visitor has agreed.
GrofomoCheckout.funnelToken(event) returns the funnel token the drop-in
uses, kept for the tab only with consent, and forgotten when consent is
withdrawn. client.event() reads the event record, and a failed
connection now throws GrofomoCheckout.Error with code network (status 0)
instead of the browser's own error. Otherwise additive: nothing changes for a
page that already loads the script.
24 September 2026 - checkout.js keeps its funnel token only with consent
checkout.js 1.6.1 keeps the funnel token for the tab only when it may
remember visits (your cookie banner's analytics answer, or
GrofomoCheckout.rememberVisits(true)). Without consent each load counts
as its own visit. A new What needs consent section lists what needs
asking about, what does not, and lines for your cookie notice. No code change
is needed on your side.
24 September 2026 - checkout.js follows your cookie banner
checkout.js 1.6.0 switches on its memory of tagged visits by itself when
your cookie banner grants analytics consent, read from Google Consent Mode,
Cookiebot, OneTrust or CookieYes, and switches it off again (forgetting what
it stored) if the visitor withdraws. GrofomoCheckout.rememberVisits()
still works, and a call to it now takes over from the banner for the rest of
the page. No code change is needed on your side.
24 September 2026 - checkout.js can remember where a buyer came from, and three new channels
checkout.js 1.5.0 can remember every tagged visit on any page that loads
it, in the browser's storage for your site, and send the latest with the
first as firstTouch on reserve, so a tag on your landing page survives the
click through to your tickets page. It is off until you call
GrofomoCheckout.rememberVisits(true), because that storage is yours to
disclose and, where your cookie notice says so, to ask about. It now reads
Grofomo's ?ref= (short links and poster QRs) and ?artist= as the hosted
page does, and adds GrofomoCheckout.attribution() for sites that render
their own checkout.
Three attribution channels are new on reserve: sms, ads (an ad
campaign built through Grofomo, tagged utm_medium=paid_social with the
campaign in utm_content) and discover (a click from the Grofomo
directory, utm_medium=discover). Grofomo's short links and event links now
pass ref, aff, artist, gclid and fbclid on to your site as well
as utm_*. Additive: a client that validates channel against the old
list should accept the new values.
23 September 2026 - An order card says why it was cancelled
Every order in GET /v1/devices/me/tickets and the ticket finder's order
list can now carry cancellation: a reason (event_cancelled,
payment_plan, refunded or cancelled) and at, when it happened,
or null where no date is recorded. It is null while the order still holds a
live ticket and its event is on. payment_plan is a plan that stopped
before it was paid off: its tickets are voided, but the order's own
status still reads completed, so check cancellation rather than
status to tell the buyer. Absent on older servers.
23 September 2026 - New: the Ambassador programme for your own site
Two endpoints and a drop-in for a festival's "bring your mates" page.
GET /v1/events/{slug}/ambassadors
returns the published reward ladder, the organiser's rules, whether ticket
buyers are enrolled automatically, and whether partner applications are open.
POST /v1/events/{slug}/ambassadors/applications
sends a promoter's application for a cash rate into the organiser's review
queue. ambassadors.js renders the tiers, rules and form into your page; see
the guide.
21 September 2026 - The abandoned-basket email returns the buyer to the page they left
Reserve takes an optional resumeUrl: the page running the checkout.
When that basket is abandoned, Grofomo's nudge sends the buyer back to it,
with ?basket= and ?promo= as before, instead of to the event's ticket
link or the hosted page. It is kept only when it is https, on one of the
organiser's allowed domains, and on the same origin as the calling page;
anything else is ignored, not refused, and the email falls back as it
always did. It is checked again when the email is sent, so a domain the
organiser has since removed is not linked to. A buyer who abandoned on the
hosted page now goes back to the hosted page even when the event names a
ticket link. checkout.js 1.4.0 sends it automatically; see
selling tickets.
21 September 2026 - A fixed plan deposit is no longer charged on a free ticket
A payment plan with a fixed deposit charges its amount per ticket. Until
now that counted every ticket in the basket, including a £0 ticket and one a
buy-N-get-M deal gives away. A buyer taking "buy one, get a free shuttle bus"
was charged a deposit on the free bus, which also came off the balance being
spread, so the instalments could drop under the minimum payment and reserve
answered plan_unavailable for a basket that should have qualified.
The deposit is now charged only on tickets the buyer pays for, the same count
the booking fee has always been priced on. For a basket with no free ticket
nothing changes. For one with a free ticket, reserve can now accept a plan it
used to refuse, and the depositPennies it returns is lower by one fixed
deposit per free ticket. Show the deposit reserve returns rather than working
it out yourself.
18 September 2026 - A reward claim takes a postal address in its own fields
POST /v1/devices/me/ambassador/reward-claims now takes
deliveryAddress: recipient (optional), line1, line2
(optional), city, postcode and country (ISO 3166-1 alpha-2,
defaulting to GB). line1, city and postcode are required
once an address is sent at all, because a parcel cannot be posted without them;
send deliveryAddress: null to clear it.
It replaces one free-text box that asked for "your address and postcode, or how to reach you" and so collected phone numbers and social handles too. Asking for the parts of an address separately means an app collects, and declares, exactly a postal address.
deliveryNote is unchanged and still accepted, so an app build already
installed keeps working: it is free text up to 500 characters, blank to clear.
When both are sent the address wins. The note is no longer only what someone
typed, though – the server composes it from the address, one field a line, and
that is what an organiser reads on their fulfilment screen. Do not assemble it
yourself.
Every reward in GET /v1/devices/me/ambassador/rewards now carries
deliveryAddress alongside deliveryNote, for prefilling the claim
form. It is null on a reward claimed before these fields existed, or claimed
through a surface that still writes free text, so fall back to
deliveryNote when showing where a reward is going. Additive.
18 September 2026 - site.content_changed is batched per editing session
The website-rebuild notification now waits for an editing session to finish instead of firing on every change.
Before, changes coalesced only when they landed inside the same wall-clock minute. An organiser working through a lineup produces a change roughly every minute, which slips between those buckets almost every time: one session on 17 September sent sixteen notifications in sixty-six minutes, each one a full site rebuild.
A change now waits about two minutes for the next one and the notification goes out once they stop, so that session would send four. A session that never goes quiet is capped: the notification goes out at most fifteen minutes after the first change of a burst, and the batch starts again from there.
Nothing about the payload changes, and the last change of a session always produces a notification. The one thing to expect differently: a change made on its own now reaches you a couple of minutes after it is made rather than immediately. If your build is triggered by this hook, that delay is added to your build time, not hidden inside it.
Changes are still batched per event and per contentType, so a lineup change
and a news article published together still arrive as two notifications, each
naming what it covers.
18 September 2026 - A hidden tier counts in the sell-out ladder, and a closed rung releases the next
A tier set to start when the previous one sells out waits for the tier above it
in sort_order even when that tier is hidden. Before, a hidden tier was
dropped from the feed entirely, so the tier after it became the first rung and
opened straight away: an event running a held-back pre-sale sold its public
tier at full price from the moment it went live, against the organiser's
explicit setting.
So a tier you were shown as purchasable may now arrive as upcoming: true,
and reserve answers not_on_sale for it, for every requester alike. The
ladder is a property of the event, not of who is looking, so holding the unlock
code for the hidden tier does not open the tier above it either. Read
upcoming rather than inferring that anything listed can be bought.
In the same pass, a rung is now DONE when it sells out or when its own sale-end passes, whichever comes first. Waiting only for the sell-out meant a tier that closed on its date without selling out shut every tier below it permanently, which an event running a held-back pre-sale could reach with no way back. This can only ever open a tier that was stuck, never close one that is open.
18 September 2026 - A promo code can now reveal a hidden TIER, not just a gated ticket
?promo= on GET /v1/events/{slug}/tickets already revealed ticket types
whose visibility is gated and which list a promo code as an unlock. It now also
reveals an individual hidden tier inside an otherwise ordinary ticket, when
the code names that tier. Those tiers arrive in the usual releases array and
are quoted and reserved like any other, as long as the same code rides on those
calls.
The scoping rule differs from the one for gated types, deliberately. A gated type names the credential KINDS that open it, so any live unlock code opens it. A hidden tier names nothing, so it is opened ONLY by a code scoped to it: an unlock code with no tier scope reveals no hidden tier at all. Nothing an organiser has hidden becomes visible without them saying so tier by tier.
Nothing changes for a request without a code, and no field changed shape. If you list tiers yourself, a cart mixing an unlocked tier with public ones is still one order: send the code on quote and reserve exactly as you send it on the feed.
One consequence for a page on the checkout contract: the abandoned-basket
recovery link now carries ?promo= when the basket was priced with a code.
Load the feed with it BEFORE clamping the ?basket= seed, or the clamp drops
the unlocked tier the buyer came back for. Selling tickets says
how; checkout.js already does it.
17 September 2026 - The booking fee is now exactly linear in quantity
The fee-inclusive price you advertise is now never exceeded. n of a tier cost
exactly n x the feeInclusivePricePennies on the ticket feed.
Before, the percentage component of the booking fee was rounded down once
against the whole order, while the advertised per-unit price rounded down per
ticket. Because floor(n*x) >= n*floor(x), an order's fee could exceed n x the
advertised unit price by up to n-1 pennies. An organiser who set a face value
so their tier advertised a round £30.00 found that two in the basket came to
£60.01.
bookingFeePennies on quote and reserve therefore changes for carts of more
than one, by pennies, always downward. If you show your own running total
before calling quote, and you compute it as quantity x the advertised price,
it now agrees with us exactly.
This covers a cart carrying a free extra, such as "buy one, get a free shuttle bus", because the paid items keep their advertised price and only the given-away unit changes. Those carts were charged the old way for a few hours after this shipped, and are not any more.
Two cases are unchanged, and neither has an advertised total left to hold to: a cart whose items were RE-PRICED, by a promo code or a tiered rule, and a policy whose per-order minimum fee fires, where an order pays less than n x the advertised figure. Less is always safe.
17 September 2026 - The prelaunch form's privacy link is now the organiser's own
privacyPolicyUrl on GET /v1/events/{slug}/prelaunch now resolves from the
organiser's legal settings, the same source the ticket feed's
checkout.legal.organiser.privacyPolicyUrl already used, rather than from a
separate field on the prelaunch form.
Nothing about the field's shape changes. It is still a nullable string and you still link it beside the consent wording. What changes is that it is now reliably the ORGANISER's notice. The old field was pre-filled with Grofomo's own policy, and most forms kept it, so a registrant was frequently pointed at our privacy policy for data the organiser controls.
If you cache or hardcode this value, refresh it. For most organisers it is about to change to a different URL, and the one it changes to is the correct one. It is also what gets frozen into the registration's proof of consent, so a stale copy on your page and the recorded one will disagree.
17 September 2026 - Prelaunch consent wording v5, and a number that opts itself in
Two changes to GET /v1/events/{slug}/prelaunch and its register endpoint.
formVersion is now prelaunch_form_v5.
The wording no longer promises a launch moment. v4's email notice said we would email you "the moment <event> tickets go live". That assumes a prelaunch list always comes before a sellable ticket, which is false for a priority release, where the form runs alongside a tier that is already selling and registrants go straight to checkout. It also described one event's ticket launch when the consent recorded has always been broader: registrants can be sent lineup news and news about the organiser's other events. The wording now matches what is actually collected, and carries no timing claim:
Registering signs you up for email updates about Twisted Roots 2027, plus other events from EVA Events. Unsubscribe anytime.
If you print consent.email verbatim, as you should, you get this
automatically. Registrations already captured keep the wording they were shown.
fields.phone gains consentMode and required. consentMode is
checkbox for every existing form, which is today's behaviour: a tick beside
the number, sent back as consentSms.
An organiser can now choose implied instead, for a signup where the number
is the priority-access check. There is no tick. Print consent.messaging as a
notice immediately beside the number field, send no consentSms, and the
number itself is the request: entering it and confirming the code we text is
the affirmative act. Sending a number with no consentSms is refused under
checkbox and accepted under implied, so branch on consentMode rather
than assuming.
required is always false. Keep your own field optional too. A mandatory
number makes the messaging consent a condition of registering when it is not
needed to register, and a consent that is not freely given is not consent, so
the list you build that way cannot be used.
The default submit-button label changed too, from "Email me when tickets go
live" to "Email me about tickets", for the same reason: it is a timing promise
that is false on a priority release. It is only a DEFAULT. An organiser who has
set their own ctaLabel is unaffected, and any organiser can change it in the
console. Render ctaLabel from the form endpoint rather than hardcoding it.
One behaviour note for direct callers: the consent-shape errors
(phone_without_sms_consent, no_consent) now resolve after the event and
form are looked up, because what a valid shape IS depends on the form's mode.
A malformed body aimed at a slug that does not exist now answers 404 rather
than 400.
17 September 2026 - No Turnstile token is required, and never was in production
Three write endpoints documented a Cloudflare Turnstile token as required
and listed a turnstile_failed error:
POST /v1/events/{slug}/subscribe,
POST /v1/events/{slug}/prelaunch/register and
POST /v1/forms/{key}/submissions.
The bot check is not enabled in production and has not been. The
verifier returns success when it is unconfigured, so a
turnstileToken is accepted and discarded, and turnstile_failed
cannot be returned by any of the three. The reference said otherwise,
the request examples told you to paste in a widget token, and at least
one integration was built around a widget whose token was never checked.
You do not need to render a Turnstile widget to call these. If you already do, nothing breaks and you can leave it: the field stays in the request schema and is still ignored. If you were about to add one for these endpoints, do not bother yet.
What actually gates them is unchanged and is stated on each page: your publishable key, the organiser's allowed-domains list for browser calls, and a per-address rate limit. None of that is a bot check, so treat these endpoints as you would any unauthenticated public write on your own site.
We will announce it here before a token starts being required, because switching it on would break every browser integration that is not already sending one. The site key needed to render a widget is a public value and will be published here when there is one.
Also corrected on the prelaunch registration page: consentEmail was
described as having to be true. The route requires at least one
consent to be granted, and refuses a number without consentSms (or
the tick without a number). Sending consentEmail: true remains what
you want, since registering is the email consent under the wording you
printed.
17 September 2026 - The organiser's own refund policy and privacy notice, on the ticket feed
GET /v1/events/{slug}/tickets?surface=web now carries a
checkout.legal.organiser block. It is additive and optional: absent on
older servers, and absent for an organiser who has set nothing yet.
It exists because a ticket sale is two supplies from two parties. Grofomo
charges the booking fee and supplies the booking service; the ORGANISER
sells the admission and is the data controller for everything your
checkout collects. checkout.legal already carried Grofomo's half. This
is theirs, and Grofomo's own policies do not discharge it.
{
"checkout": {
"legal": {
"organiser": {
"name": "Riot Promotions",
"privacyPolicyUrl": "https://riot.example/privacy",
"refundPolicyLine": "Free cancellation until 7 days before the event. You get the ticket price back; the booking fee is ours and stays with us.",
"refundCancellationLine": "If the event is cancelled, you get everything back, booking fee included. If it moves to a new date or venue, or changes significantly, and you no longer want to go, you are entitled to a refund. That is your legal right, not the organiser's decision."
}
}
}
}Print the two refund lines as two lines. Not one paragraph. The
booking fee comes back when an event is cancelled and does not when a
buyer simply changes their mind, so the two sentences contradict each
other when run together. refundPolicyLine is the organiser's
discretionary position and is absent when they have set none;
refundCancellationLine is the statutory floor, applies whatever the
line above says, and is always present.
Print them at ticket selection, not on the payment screen. A refund policy is material information and has to reach the buyer in time to inform the decision to buy.
Neither line names a fee amount, because the booking fee is a percentage of the subtotal plus a per-ticket charge, then capped and floored, so it is not a single figure until the basket is known. Show the amount on its own line from the quote, as you already do.
privacyPolicyUrl is the organiser's own notice. Link it in addition to
the Grofomo privacy link you already show, labelled so a buyer can tell
the two apart.
A merchandise object is defined on the block for surfaces that sell
goods. It is never sent on this endpoint: a ticket order cannot contain
merchandise.
Using the drop-in? checkout.js 1.4.0 prints all of this for you:
the two refund lines on the ticket step, and the organiser's privacy link
in the footer beside ours. Older versions ignore the block and print
neither, so upgrade if you rely on the drop-in to discharge this.
17 September 2026 - Event search answers a browser, from any origin
GET /v1/events/search returned a correct 200 with no
Access-Control-Allow-Origin header and answered the browser preflight
with a bare 204, so every browser call to it failed on every origin.
It now carries CORS and answers OPTIONS.
Unlike every other feed, it grants any origin rather than resolving yours against an organiser's allowed-domains list. It is the one endpoint that spans organisations, so there is no single organiser whose allowlist could apply. You do not need to be allowlisted anywhere to call this one from a browser, and adding a domain to an organiser's list makes no difference to it either way.
That is a CORS grant and nothing more. It confers no credential and
changes nothing about the rows returned: still only events whose
organisers opted them in to public discovery, still the same
?surface= rules, still no key required.
The same caution as the five feeds above applies. The failure was
invisible from the outside, so a fetch wrapped in a .catch() that
falls back to placeholder content has been serving the placeholder and
looking healthy. If you call search from a browser, confirm you are
rendering what the API returned. If you call it from a server there was
never anything to fix.
17 September 2026 - Event detail, version, updates, artist and organiser feeds answer a browser
Five endpoints returned a correct 200 with no
Access-Control-Allow-Origin header, and answered the browser preflight
with a bare 204 naming no allowed origin. Every browser call to them
failed, on every origin, including allowlisted ones, while the same
event's tickets, FAQ and lineup answered normally:
GET /v1/events/{slug}
GET /v1/events/{slug}/version
GET /v1/events/{slug}/broadcasts
GET /v1/events/{slug}/artists/{artistId}
GET /v1/organisers/{slug}They now carry the same CORS headers as every other feed, resolved
against the same allowed-domains list, and answer OPTIONS. Nothing
about the response body, the visibility rules or the ?surface=
parameter changed, and no key is needed that was not needed before:
these five stay ungated.
Worth re-checking your own code. The symptom was silent from the
outside. A browser refuses the response without the page ever seeing a
failed status, so a fetch wrapped in a .catch() that falls back to
placeholder content will have been serving the placeholder this whole
time and looking healthy. If you call any of these five from a browser,
confirm you are rendering what the API returned rather than your
fallback, and make the fallback log when it fires.
If you call them from a server there was never anything to fix: a
server sends no Origin, so CORS never applied.
GET /v1/events/{slug} and GET /v1/events/{slug}/version gated on
the event's app visibility whatever surface was asked for, so an event
published to the web, with its tickets, lineup and FAQ all answering,
still said 404 not_found on both to the very site its ticket link
pointed at. They now read the website visibility for ?surface=web,
the rule every other event endpoint already applied. Send
?surface=web on these two calls too; the reference pages now say so.
17 September 2026 - The ticket feed carries the legal notices a checkout must print
The checkout block on
GET /v1/events/{slug}/tickets gains a legal object, alongside the
consent wording it already carried. Two parts, both required on any
checkout you build: attestation, the 18+ and Grofomo Terms
confirmation that goes beside the button committing the buyer to pay,
and attribution, which names Grofomo on the page. attestation.linkText
is a verbatim substring of attestation.text, so you wrap that substring
in a link to termsUrl rather than reassembling the sentence.
A ticket order is two supplies: admission from the organiser as principal, and the booking fee charged by Grofomo in its own right. The hosted ticket page has always shown the buyer both. A checkout that omits these notices sells the buyer a service from a company they were never shown, under terms they were never given. The selling tickets guide has the rendering snippet.
Additive, so nothing breaks, but treat its absence from your checkout as
a bug rather than as an option. legal is deliberately not covered
by formVersion: that names the consent wording, which is evidence and
is snapshotted onto the buyer's consent record, whereas these notices are
notice and change independently.
17 September 2026 - Reserve enforces promo limits, hidden tiers and a hold quota
A limited-use promo code is now counted at reserve, under a lock, so a
one-use code admits one live hold; a second cart on it gets
409 promo_used_up at reserve rather than at completion. A hidden tier
is refused by id (400 unknown_release), like a deleted one. Too many
live reservations on one event from one connection or app device answer
429 too_many_holds with a Retry-After; a hold lapses in 15 minutes.
On the details step, a returning customer's marketing choices are now
recorded when their order completes rather than when the form is
submitted, and for the form versions this API publishes wording for, a
consentTextSnapshot that differs from that wording is refused with
400 consent_text_mismatch.
17 September 2026 - The reserve response carries a checkout token, not the order id
POST /v1/events/{slug}/tickets/reserve now returns checkoutToken
in place of publicId, and the details, pay, complete-free and
order-status paths take {checkoutToken}. The token authorises this
checkout only and expires 24 hours after the reserve. The order's public id
was also the buyer's lasting management credential (the hosted order page,
ticket transfers, payment-plan card changes, the receipt), so returning it
to a page that only needed to sell was more than the page needed; a
security review said so within a day of publication, and no integration
had been built on it yet. checkout.js 1.1.0 uses the token. The checkout
writes also now require a credential on every surface: an anonymous call
that says ?surface=app gets the organiser's key policy like any other.
16 September 2026 - checkout.js prints the two required notices
checkout.js 1.3.0 reads checkout.legal off the ticket feed and renders
both notices itself: the 18+ and Terms attestation immediately above the
button that commits the buyer, and the "Ticketing by Grofomo · Terms ·
Privacy" attribution under every step. A site on the drop-in needs nothing;
a site that renders its own UI still owes both, and selling
tickets now opens with the full list of what a checkout page must
show.
The words come off the wire rather than out of the script, so a change to them reaches an organiser's site without anyone redeploying it. If an older server omits the block, the drop-in prints its own copy rather than printing nothing: the notices are not optional.
16 September 2026 - Grofomo's emails link to your checkout, and checkout.js reads the basket deep link
When an event's ticket link points at your page, the abandoned-basket
nudge now sends the buyer there instead of to the hosted page, with the
basket they left as ?basket=<releaseId>:<qty>,... and the usual
attribution tags. A page on this contract must honour both, and
selling tickets now says how. checkout.js 1.2.0 reads the
basket link itself, clamped against the feed, so a site using the drop-in
needs nothing.
16 September 2026 - A free ticket no longer removes the payment plan
A payment plan offer has to be open on every tier in the basket. A free tier can carry no offer, because there is nothing to spread, so a basket with one in it shared no offer at all: adding a £0 under-5 ticket silently took the plan away from the adult tickets beside it, on the page and at reserve, with nothing to explain why.
Free tiers are now ignored by that rule, in paymentPlans on the feed
and in the plan_unavailable check. A basket of nothing but free tiers
still has no plan, because there is nothing to pay.
16 September 2026 - A free-ticket giveaway now combines with the deal that earned it
Automatic deals used to be single-winner: of every rule a basket qualified for, the one that saved most applied and the rest did not. That was right for two deals on the same tickets, and wrong for a giveaway of a different one. "5 adults for the price of 4" beat "buy 4 adults, get a child ticket free", so the child was charged for and the offer read as broken.
A rule with freeReleaseId now combines with the best deal on the
tickets that earned it, when the two discount different lines. Deals on
the same tickets still do not stack. The reserve response's
quantityDiscount.name names both when both applied, and id is the
one that gave more. Nothing changes for a basket that qualified for one
rule.
16 September 2026 - checkout.js, the drop-in ticket checkout
One script tag renders the whole purchase on your own page:
https://developers.grofomo.com/checkout.js, then
GrofomoCheckout.mount('#tickets', { event, key }). Ticket choice,
promo codes, the buyer's details and consent, the card through Stripe,
and the confirmation, in a plain style your CSS overrides. The same file
exposes GrofomoCheckout.client(...) for a site that renders its own
cart. See selling tickets.
16 September 2026 - Selling tickets from your own site
Seven endpoints join the reference: quote,
reserve, details,
pay, complete free,
order status and the
funnel beacon. They are the write flow the
hosted ticket page runs on, now a supported contract, gated like every
event endpoint on the organiser's allowed domains and publishable key.
The selling tickets guide walks the whole purchase, Stripe
Payment Element included. Reference pages now show a request body table
generated from the wire schema, and openapi.json carries typed
request bodies.
16 September 2026 - The ticket feed carries the checkout consent wording
GET /v1/events/{slug}/tickets gains a checkout block on the web
surface: the three consent labels a checkout must print (the organiser's
email box, the organiser's SMS and WhatsApp box, Grofomo's own email box),
how each is shown, and the formVersion to send back with the buyer's
details. It is the wording the hosted ticket page prints, from the same
source. Every field on the quote, reserve, details, pay and order-status
schemas now carries a description, ahead of those endpoints joining this
reference.
15 September 2026 - The prelaunch form is available to your own website
Three new event-scoped endpoints let a site render an organiser's prelaunch
registration in its own design: GET /v1/events/{slug}/prelaunch returns
the published form (copy, which fields to ask, and the exact consent wording
to print), POST /v1/events/{slug}/prelaunch/register takes the
registration, and POST /v1/events/{slug}/prelaunch/verify finishes its
double opt-in (the SMS code and the resends). They are gated like every event
endpoint, on the organiser's allowed domains and publishable key. The hosted
page at events.grofomo.com/prelaunch/{slug} is unchanged.
15 September 2026 - Mailing-list signups are double opt-in
POST /v1/events/{slug}/subscribe and a mailing-list form submitted through
POST /v1/forms/{key}/submissions still save the signup at once, but the
address is not on the organiser's list until the visitor opens the
confirmation email we now send. Both responses gain a verification object
(email: sent, not_needed or failed; phone; phoneHint).
Show "check your inbox" when verification.email is sent.
On the subscribe endpoint, attaching a number with { submissionId, phone }
now texts it a code where phone verification is on (verification.phone is
sent); post { submissionId, code } to confirm it, and
{ submissionId, resend: "email" | "code" } to send either again. The
mailing_list.signup webhook fires when the email is confirmed rather than
at submission, and its payload gains confirmedAt.
15 September 2026 - A deal can give a different ticket away
A buy_n_get_m entry in quantityDiscounts may now carry
freeReleaseId: the release given free ("buy 1 early bird, get a free
shuttle bus"). Every full group of buyQuantity paid units in the rule's
scope frees freeQuantity units of that release, but only units the
visitor has put in the basket. Nothing is added for them, so a site that
advertises the deal should add the free ticket to the cart alongside the paid
one. The label reads "Buy 1, get a free Shuttle bus". A rule without
freeReleaseId is unchanged: the cheapest units in scope go free. The rule
is omitted for a caller who cannot see the free release. Additive.
15 September 2026 - Cross-event offers on reserve
POST .../tickets/reserve now carries crossEventOffers: the offers this
cart has earned on the organiser's other events (each with the target event's
name and slug, the discount, and how long the code lives). Buying a qualifying
tier mints the buyer a personal, single-use promo code for the target event
once the order finalises, and emails it. New endpoint
GET .../tickets/orders/{checkoutToken} returns those codes once minted (an
empty list until then), so a confirmation page can show the code as well as
the email. A code is bound to the buyer: collect-details on the target event
refuses it for any other email address with promo_not_yours.
15 September 2026 - Prelaunch registrations are double opt-in
POST /v1/prelaunch-register no longer grants marketing consent at
submission. A new email address gets a confirmation link and a new mobile
number gets an SMS code; the matching consents are granted when the proof
comes back. The response gains a verification object: a token for the
follow-up calls, email and phone states (sent, not_needed,
failed, and for phone unavailable), and a phoneHint. A registration
with an address or number this registrant already proved is granted at once,
as before.
New endpoint POST /v1/prelaunch-verify finishes the job with one of four
action values: confirm_email (the link's token), check_code (the
form token plus the code), resend_email and resend_code. Wrong and
expired codes come back as an outcome, not an error.
If you build your own registration form on this endpoint, show the code
step when verification.phone is sent and tell the person to check
their inbox when verification.email is sent. The
prelaunch.registration webhook now fires each time a channel is proven
(the email link, the SMS code) rather than at submission, carrying the
consents as they stand, so a consumer may see it twice for one registration.
16 September 2026 - Tracking can be added after a reward is posted
The organiser can now edit carrier and trackingNumber on a reward
they have already marked sent, so an Ambassador whose parcel went before the
number existed still gets it. Two consequences for a consumer of
GET /v1/devices/me/ambassador/rewards: dispatch is no longer fixed
once sentAt is set, so re-read it rather than caching it on first sight;
and sentAt is now the instant the reward was marked sent and no longer
moves when the courier or number is edited afterwards.
A FIRST tracking number pushes the Ambassador (type: 'ambassador_dispatched' again, with its own wording), at most once per
reward for all time. A correction, a courier swapped, or a number cleared
sends nothing. Additive: no field changed shape.
16 September 2026 - A posted reward carries its tracking
Every reward in GET /v1/devices/me/ambassador/rewards now carries
dispatch: carrier (the courier's name, or whatever the organiser typed
for one we have no page for), trackingNumber, trackingUrl (the courier's
own tracking page for that number, null when there is none) and sentAt. It
is null on every reward except one the organiser posts on, and until they mark
it sent; both the courier and the number are optional, so a sent reward can
carry a date and nothing else. Marking it sent also pushes the Ambassador
(type: 'ambassador_dispatched', deep-linked to that event's ladder), once
per reward, subject to their Event updates preference. Additive.
16 September 2026 - A reward sent on afterwards has a claim deadline
POST /v1/devices/me/ambassador/reward-claims now refuses with
claim_closed (422) once the reward's claimUntil has passed: the
organiser's own date, else a month after the event ends. claimUntil and
claimUntilDate were already on every reward in
GET /v1/devices/me/ambassador/rewards, so stop offering the claim form once
claimUntil is in the past rather than letting someone fill it in for
nothing. The organiser can still mark the reward sent by hand: the deadline
bounds what the Ambassador can tell them, not the debt itself. Additive.
15 September 2026 - Ambassadors can claim a reward that is sent to them
New endpoint POST /v1/devices/me/ambassador/reward-claims (device token)
claims a reward the organiser sends on afterwards: the answer to its
question, such as a shirt size, and where to send it. Body
{ affiliateId, rewardDefinitionId, selectedOption?, deliveryNote? }.
selectedOption is required when the reward has optionChoices and must be
one of them exactly as offered, and is refused when it has none;
deliveryNote is free text up to 500 characters, blank to clear. Nothing is
minted, and both can be changed until the organiser marks the reward sent.
Refusals: reward_not_found (404), reward_not_claimable (409, already sent
or taken back), reward_not_sent_later (422, a reward collected at the event
answers when its code is minted instead), option_required (422) and
option_not_offered (422). Each reward in
GET /v1/devices/me/ambassador/rewards now carries selectedOption and
deliveryNote, what they said, or null. Additive.
15 September 2026 - A ticket refund reward says where its refund is
A reward of type ticket_refund in GET /v1/devices/me/ambassador/rewards
now carries refund: status (pending until the event ends, then
refunded with refundedAt, or failed), amountPennies and
currency. Null on every other kind of reward. The refund itself is paid
automatically after the event to the card the Ambassador bought with; their
ticket stays valid. Additive.
15 September 2026 - Ambassador reward wallet carries the event's time zone
Each event in GET /v1/devices/me/ambassador/rewards now carries
timezone, the event's IANA zone ("Europe/London"), so a client can show a
reward's availableFrom, availableUntil and claimUntil as a wall-clock
time at the venue rather than in the phone's zone. Additive.
15 September 2026 - Ambassador tiers can be named, and a used reward says when
Each tier in GET /v1/devices/me/ambassador/rewards now carries name,
the organiser's name for it ("Ringmaster") or null, and every reward carries
the same value as tierName. Read it as "<name> level"; when it is null,
say what the tier pays instead. Every reward also carries lastUsedAt and
lastUsedDate (YYYY-MM-DD in the event's own zone), the most recent scan,
null until it has been used. Additive: existing fields are unchanged.
15 September 2026 - Ambassador rewards carry a use-by date
Each reward in GET /v1/devices/me/ambassador/rewards now carries
claimUntil (an instant) and claimUntilDate (YYYY-MM-DD in the event's
own zone): the last moment it can be used. When the organiser set no date, the
event's own end fills it for anything collected at the event, and a month
after the event for anything sent afterwards or a refund. availableFrom and
availableUntil are now effective the same way, with the event's start and
end filling blanks for anything collected at the event, so a code asked for
after the event closes is refused as outside_window rather than served.
Additive: existing fields are unchanged.
15 September 2026 - Ambassador reward wallet carries the event's dates
Each event in GET /v1/devices/me/ambassador/rewards now carries
eventStartDate and eventEndDate (YYYY-MM-DD in the event's own zone,
the same shape as dates on the event detail) and programmeDescription
(the organiser's own words about the programme, or null). A client can head
the ladder without fetching the event detail, which an event that is not yet
public does not have. Each reward on a tier also carries cooldownMinutes,
the organiser's gap between uses of that one reward (0 when none), so a client
can say "one every 20 minutes" before the second tap is refused as
cooling_down. Additive: existing fields are unchanged.
10 September 2026 - Old event slugs keep working
An organiser can rename an event, which changes its slug. Every
/v1/events/{slug}/... endpoint, and the device endpoints that take an event
slug, now accept a slug the event used to carry and answer exactly as they do
for the current one. There is no redirect: the same request simply succeeds, so
CORS and POST requests behave as before. On the public site, old
grfm.to/e/{slug} links 308 to the current address.
Nothing breaks if you ignore this, but the docs used to promise a slug was fixed
once created, and that was never enforced. If you store a slug, refresh it from
the slug field of GET /v1/events/{slug}, which always carries the current
one.
10 September 2026 - Set notes on line-up slots
Each slot in GET /v1/events/{slug}/lineup now carries setNote: a short
note from the organiser on how that one set is played, such as "DJ set", "Live"
or "Rave set", at most 80 characters. It is null when there is none, including
on every line-up published before today. Show it beside the act's name, not in
place of it: "Uncle Dugs · Rave set".
A note belongs to the set, not the artist, so the same act can be "Live" on Friday and "DJ set" on Sunday. It is additive, so a client that ignores it keeps working.
7 September 2026 - Payment plans are now offers on each price tier
Each release in GET /v1/events/{slug}/tickets (and the tickets block of
GET /v1/events/{slug}) now carries paymentPlans: the plan offers a
buyer can take that tier on, each with the deposit (percent of the face
value in basis points, or fixed pennies per ticket), the number of monthly
payments, payByAt and dueDates. The dates are the monthly anniversaries
of an order placed now, at 09:00 in the event's timezone, so a buyer who signs
up just after payday is charged on that day every month. Organisers set these
per tier: a release on sale a year out can carry a ten-month plan, and the
final release a fortnight out carries none.
An offer can disappear from a tier with no organiser action. Only offers
that can still be repaid before payByAt are listed, so as the event
approaches a tier's paymentPlans shrinks and eventually empties. Re-read it
with the catalog, as you do availability, and never cache it across a session.
To buy on a plan, pass paymentPlanId on POST .../tickets/reserve (the old
plan: true is no longer read). Every tier in the cart must list that offer;
otherwise the reserve answers plan_unavailable (409). The reserved order's
paymentPlan now carries the exact schedule the order will be charged on:
installments[] with seq, amountPennies and dueAt, plus payByAt
and the offer's id and name. It is snapshotted at reserve, so what you
show the buyer is what the plan is created with.
The type-level paymentPlan field is kept for older clients but is now
derived and deprecated: set only when every purchasable tier of the type
shares one open percent-deposit offer, and null otherwise (including when the
tiers differ). Read the release field. Additive; apiVersion stays 1.
4 September 2026 - Artist photos now agree across every surface
When an organiser sends an artist a retouched photo for an event and the artist
adopts it, that photo is what the event shows. It already worked that way on
GET /v1/events/{slug}/lineup; it now works the same on
GET /v1/events/{slug}/artists/{artistId}, which used to return the artist's
own profile photo instead. Tapping a lineup card and landing on a different
face was the visible symptom.
No shape changes and apiVersion stays 1: imageUrl is the same field, it
is now resolved the same way in both places. Worth knowing if you cache
artist images: the value can change without the artist editing their profile,
and the URL always changes with it. Key any local image cache on the URL rather
than on the artist id, or you will serve whichever photo you fetched first.
The photo is scoped to the event it was approved for, and it only applies when the artist chose to adopt it as their photo. An artist who files it in their press kit without adopting it, or who adopts it only until the event is over, keeps their own photo on the bill.
1 September 2026 - New webhook: whatsapp.campaign_held
A scheduled WhatsApp campaign no longer sends if Meta has marked its message
down since it was scheduled. Instead the campaign is held: nobody is messaged,
it stays scheduled, and the organiser is told so they can change the message or
cancel it. The new
whatsapp.campaign_held event fires when that happens,
carrying template_name, language, scheduled_for, recipient_count
and campaign_url.
Nothing about /v1 changes and apiVersion stays 1. It is listed here
because the webhook surface is part of what we document, and an integration
watching for a campaign to complete now has a state that means "waiting on the
organiser" rather than "sent" or "failed". Subscribe to it the same way as any
other event; existing subscriptions are unaffected.
26 August 2026 - News article detail: bodyHtml is now always populated
GET /v1/events/{slug}/news/{articleSlug} documented bodyHtml as an
optional pre-rendered projection of bodyMarkdown, but in practice it was
always null: nothing populated it, so consumers rendering it (including our
own event pages) showed articles with no body. It now falls back to a
server-side render of the canonical markdown (headings, paragraphs, lists,
emphasis and links, escaped-first), so it is always a non-empty projection of
bodyMarkdown. The field keeps its nullable type on the wire; treat null as
absent as before. Additive; apiVersion stays 1.
26 August 2026 - Event detail: capacity is the enforced venue capacity
GET /v1/events/{slug} still returns capacity in the same place and the same
shape, but it now reads the venue-capacity figure the ticketing system actually
enforces sales against, rather than a separate display field an organiser could
edit independently. In practice the two already agreed on every live event, so
no response changes today. From here the number cannot drift from the one that
stops ticket sales. Same for the maximumAttendeeCapacity in schemaOrg. No
shape change; apiVersion stays 1.
24 August 2026 - Event detail: ready-made schemaOrg, and a word on null
GET /v1/events/{slug} now carries a schemaOrg field on the web surface: a
complete schema.org MusicEvent block with the description, the full venue
address, the organiser and one Offer per ticket tier, priced fee-inclusive
with sale windows and availability. Serialise it straight into a
<script type="application/ld+json"> tag instead of hand-building your own.
Null on the app surface. Additive; apiVersion stays 1.
Empty fields on the event are dropped from the block rather than published blank, so what Google sees is always valid, just thinner until the organiser fills things in.
Which is a good moment to state a convention that has always held: a null field anywhere in this API means the organiser has not filled it in. Drop it; never render it blank. An address with an empty street reads worse than no address, on a page and in a search result alike.
21 August 2026 - Booking-fee minimum is now 50p per order (was £1.00)
The per-order floor on the platform default booking fee drops from £1.00
to 50p: bookingFeePolicy.minPennies on the
tickets endpoint now reports 50 where it
reported 100. The rate itself is unchanged at 7.5% + 25p per ticket.
The floor fires whenever 7.5% of face plus 25p falls under it, and at £1.00 that was every ticket under £10: an £8 early-bird ticket paid a 12.5% fee while a £40 ticket paid 8.1%. At 50p it still clears the platform's cost on the cheapest possible order, so lowering it changed nothing for most carts and removed the worst number on the pricing page.
No field changed shape; the number inside one did. If you compute fees
client-side, keep reading minPennies from the payload rather than
hard-coding it: as with the rate, the figure legitimately differs from one
event to the next.
21 August 2026 - a ticket given away by a deal no longer pays a booking fee
The per-ticket component of the booking fee is charged on units the buyer
pays for. A buy_n_get_m rule's free unit no longer counts.
It used to. The free ticket lost its face value but kept its per-ticket fee, so a cart of five under "5 for the price of 4" cost slightly more than a cart of four - on the fee-inclusive basis this API tells you to advertise on. The label said one thing and the till said another. Now five costs exactly what four costs.
Only buy_n_get_m is affected. A tiered rule discounts units that are all
still paid for, and bundle_price charges one price for the whole bundle, so
neither changes.
If you compute fees client-side from bookingFeePolicy, ticketCount is
now "units paid for": exclude £0 tiers, as before, and exclude the units a
buy-N-get-M rule frees.
21 August 2026 - New: label on every quantity discount
GET /v1/events/{slug}/tickets now returns a label on each entry in
quantityDiscounts - ready-to-display wording like "5 for the price of 4".
Additive: existing fields are unchanged and apiVersion stays 1.
Use it instead of composing a string from the numbers. buy_n_get_m is the
one that catches people: buyQuantity: 4, freeQuantity: 1 looks like
"buy 4, get 1 free", but the rule needs five in the basket before the
cheapest goes free. A visitor who adds four gets nothing. Our own storefront
shipped that mistake, which is why the server now states the deal rather than
leaving every site to derive it.
For bundle_price the label is fee-inclusive, so "5 for £80" is what the
buyer actually pays - the same drip-pricing reasoning as
feeInclusivePricePennies.
21 August 2026 - writes are no-store, as documented
Every write endpoint under /v1 now returns Cache-Control: no-store.
That was always the documented behaviour (see caching), but
the implementation did not match: writes inherited the default read header,
public, s-maxage=60. Because a CDN keys on the URL alone, a cached reply to
one POST was replayed for the next POST to the same path - a different body,
and a different caller. On the ticket checkout that meant a cart quote could
show the previous cart's fee and total for up to a minute.
Nothing about the request or response shape changes. If you cached or de-duplicated write responses yourself on the assumption they were stable, stop: each write now answers for itself.
20 August 2026 - New: the photo gallery feed
GET /v1/events/{slug}/gallery returns up to 25 photos an organiser
hand-picked in Grofomo, in the order they arranged them. Additive: a new
endpoint, nothing existing changes, and apiVersion stays 1.
The gallery belongs to the event series, so every edition of a festival
answers with the same photos. An event with no series, or an organiser who has
picked none, answers photos: []. Treat empty as a normal state.
Two things worth building for rather than around. Each photo carries an
optional focalX/focalY (fractions of width and height) so you can crop
to any shape without losing the subject; null means centre. And credit,
where set, is the photographer the organiser asked to be named: show it
wherever you show the photo.
Photos can vanish between requests without warning. Eligibility is re-checked on every read, so a photo whose consent was withdrawn or which the organiser deleted stops being served at once. Do not cache the image URLs beyond your own build, and do not treat a shrinking gallery as a fault.
20 August 2026 - ticketUrl falls back to the Grofomo checkout
GET /v1/events/{slug} previously returned ticketUrl: null whenever the
organiser had not typed a link of their own, even while their Grofomo
releases were on sale. The field was documented as "null when no tickets are
on sale", so the honest reading of a live event was that it was not selling.
ticketUrl now resolves in two steps: the organiser's own link if they set
one, otherwise their Grofomo checkout (/e/{slug}/tickets) when the
event has published ticket types. It is null only when neither exists. This
matches what Grofomo's own hosted event pages have always done.
ticketUrl is still not a statement about whether a release is open right
now. A tier between sale windows, sold out, or gated keeps the destination,
because the checkout explains that state better than a missing link. Gate a
buy button on soldOut and upcoming from the tickets endpoint, as
before.
Additive in shape, and apiVersion remains 1: the field's type is
unchanged, but a site that treated null as "not selling" will now see a URL
where it previously saw nothing.
20 August 2026 - Booking fee is now 7.5% + 25p per ticket
The platform default booking fee moves from 3% + 75p per ticket to 7.5% + 25p per ticket, with the per-order minimum rising from 75p to £1.00. It stays uncapped, and it is still calculated on the post-discount subtotal, so a promo never inflates it.
The shape has changed as well as the level: the fee is now weighted toward the percentage rather than the per-ticket amount, so it scales with the value of what is being sold rather than with the number of tickets. Whether a given order pays more or less than before depends on its face value, so recompute rather than assuming a direction.
No field changed shape; the numbers inside them did. bookingFeePolicy on
the tickets endpoint reports the new figures,
every release's feeInclusivePricePennies moves to match (a £20.00 face
value now advertises £21.75, where it advertised £21.35), and
bookingFeePennies on reserve and quote responses changes accordingly.
Events that were already selling keep the previous fee. Advertised prices include the booking fee, so raising it mid-campaign would move the displayed price of a ticket someone has already promoted. Any event that had taken a sale, or had a release on sale, when this shipped stays on 3% + 75p for its lifetime. New events get the new rate.
That means the fee legitimately differs from one event to the next, and
two events can report a different bookingFeePolicy and a different
feeInclusivePricePennies for the same face value. This is not a bug and it
needs no new field: the tickets endpoint has always resolved the policy per
event, so read the fee from the event you are pricing and never carry one
event's policy over to another.
Re-read quotes rather than caching them. A total you computed or stored before this change may no longer match what checkout will charge, in either direction, and the server stays authoritative at reserve.
Organisations and events on a negotiated fee override are unaffected: they keep the rate that was agreed with them.
The booking fee is VAT-inclusive and Grofomo accounts for the VAT on it, so no separate tax line is added on top at checkout. Face value continues to be the organiser's, handled under the organiser's own VAT position.
This changes values only and apiVersion remains 1.
18 August 2026 — Event search: web surface gate + websiteUrl
GET /v1/events/search accepts an optional surface=web parameter: web
consumers (like the Grofomo event directory) then see events whose
website visibility is public, while the default continues to gate on
app visibility, so existing app clients are unchanged. Search results (and
an organiser's upcomingEvents) gain an optional nullable websiteUrl —
the event's own site when the organiser runs one; link there in preference
to a Grofomo-hosted page.
18 August 2026 — Free tickets pay no booking fee
A zero-subtotal order now pays no booking fee at all (previously the
per-order minimum still applied, so a "free" ticket checked out at £0.75),
and free tickets in a mixed cart no longer attract the per-ticket fee
component. A release's feeInclusivePricePennies therefore equals 0 for a
free tier.
Because a fully free order has nothing to charge, the pay endpoint still
responds nothing_to_pay for it - instead, POST the new
/v1/events/{slug}/tickets/orders/{checkoutToken}/complete-free endpoint
after collecting details. It finalizes the order without a payment step
(tickets issued, confirmation email sent) and returns { completed: true }.
Replay-safe: retrying after a timeout returns the same success. Orders with
any amount owed, and payment-plan orders, are refused with
payment_required.
This is additive and apiVersion remains 1.
18 August 2026 — Fee-inclusive ticket prices
The tickets endpoint now states the price a buyer
actually pays. Each release gained feeInclusivePricePennies — the face value
plus the mandatory booking fee, computed at the type's minimum order size — and
the payload gained bookingFeePolicy, the resolved fee policy
(perTicketPennies, percentBps, capPennies, minPennies) so a client
can compute the exact fee for any cart before reserving.
If you display ticket prices, switch your headline figure to
feeInclusivePricePennies. UK drip-pricing rules (the DMCC Act, in force
since April 2025) require an advertised ticket price to include mandatory fees;
pricePennies remains the face value and the difference between the two is
the per-unit booking fee, if you want to show a breakdown. Reserve and quote
responses already itemised bookingFeePennies; nothing changed there.
This is additive and apiVersion remains 1. Both fields are absent on
responses served by older deployments, so fall back to pricePennies when
feeInclusivePricePennies is missing.
3 October 2026 – Ticket sale webhooks say where the sale came from
The ticket.sale webhook payload's attribution object gained
six fields: source, the last touch in a few words ("a Meta ad", "an email",
"a short link", or the artist's name for an artist link); the campaign tags
the buyer's link carried, utmSource, utmMedium, utmCampaign and
utmContent; and ad, the name of the ad for a sale from an ad campaign
pushed through Grofomo (those ads carry it on their link as utm_term), in
which case source reads "a Meta ad (The Offer video)". Each is null when
the order does not carry it.
Two placeholders come with it, {{source}} and {{source_credit}}, and the
default message now ends with {{source_credit}} in place of
{{artist_credit}}. A sale from an artist's link reads exactly as before
("… totalling £45.00 – via DJ Nova"); a sale from anywhere else that can be
placed now says so ("… totalling £45.00 – from a Meta ad"); a sale nobody can
place reads as it always did. A Slack digest of several sales gains a
"Where from" line counting them.
This is additive and apiVersion remains 1. {{artist_credit}} still works,
and organisers who have customised their ticket.sale message keep it exactly
as written; adding {{source_credit}} to it is opt-in.
15 August 2026 — Ticket sale webhooks name the artist who drove the sale
The ticket.sale webhook payload gained an attribution object:
the last-touch channel, plus an artist ({ id, name }) when the buyer
arrived on an artist's share link — null for every other sale. Two new
placeholders come with it, {{artist_name}} and {{artist_credit}}, and the
default message now ends with the credit phrase, so an artist-driven sale reads
"… totalling £45.00 – via DJ Nova" without the organiser editing anything.
This is additive and apiVersion remains 1. Organisers who have customised
their ticket.sale message keep it exactly as written; adding
{{artist_credit}} to it is opt-in.
11 August 2026 — Account linking reports reconciliation
The attendee app's authenticated device-link response now includes a
reconciliation object describing which existing favourites, uploads,
ticket-customer records, organisation contacts and Ambassador payees were
connected to the verified account.
This is additive and apiVersion remains 1. Existing clients may ignore the
new field; clients that need reconciliation diagnostics can adopt it when they
next update their mobile schema package.
5 August 2026 — These docs launched
The API as documented here is the baseline. The entry below predates this site, and is included because it affects integrations built before it existed; anything older is folded into the baseline rather than listed.
4 August 2026 — API keys introduced
The API gained authentication: two key tiers — pk_live_… publishable keys for browser code, sk_live_… secret keys for servers — sent in the X-Grofomo-Key header.
What it means for an existing integration:
- If your site predates keys, nothing has changed for you yet. Organisations that existed before this date stay in
openmode, where requests without a key are still answered. - New organisations start in
key_requiredmode, with a publishable key minted at creation. A site built for one must send a key from day one. - Add a key now either way. In
openmode it is accepted and ignored, and your integration is already correct on the day the organiser flips the switch. How organisers mint one.
Alongside keys, browser calls from a domain the organiser has not allowlisted became an explicit 403 origin_not_allowed — see CORS and allowed domains.
Coming
Key requirements will extend to the remaining open endpoints. A handful of reads — event detail, content version, artist detail, announcements and organiser profile — currently answer without a key, because the Grofomo mobile app calls them without one. That will change once the app carries a credential.
We are measuring key adoption on those endpoints before making the switch, so it lands as a measured decision rather than a hopeful one. Send a key to them now: it is accepted and ignored today, so an integration built that way needs no change on the day.