Quickstart
Your first request, and a working lineup page in about ten minutes.
Examples use a fictional festival, summer-fest. Sign in at events.grofomo.com and they switch to your own events.
You need two things before you start: an event slug and a publishable key. Both come from the organiser — if you are the organiser, see For organisers.
The slug is the last part of the event's Grofomo link. For grfm.to/e/summer-fest the slug is summer-fest.
Your first request
curl -sS \
-H 'X-Grofomo-Key: pk_live_YOUR_PUBLISHABLE_KEY' \
'https://api.grofomo.com/v1/events/summer-fest/lineup?surface=web'Two details in that command do real work:
X-Grofomo-Keyidentifies the organisation. Most organisers now require it; some older ones do not yet. Send it either way — see Authentication.?surface=webtells the API you are a website. Leave it off and you get the mobile-app surface, which is gated on a different visibility setting, so a perfectly live event can answer404. This trips up nearly everyone once. See Surfaces.
What comes back
Every response is wrapped in the same envelope:
{
"apiVersion": 1,
"data": { "eventSlug": "summer-fest", "slots": [] }
}Errors use the same envelope with error instead of data:
{
"apiVersion": 1,
"error": { "code": "unauthorized", "message": "A valid API key is required" }
}So the check is always: look at res.ok, then read body.data or body.error. Nothing else varies.
A lineup page
This is a complete Next.js server component. It runs on your server, so it uses the secret key and lets the framework cache the result — a busy festival page then costs you one API call a minute, not one per visitor.
async function getLineup() {
const res = await fetch(
'https://api.grofomo.com/v1/events/summer-fest/lineup?surface=web',
{
headers: { 'X-Grofomo-Key': process.env.GROFOMO_SECRET_KEY },
next: { revalidate: 60 },
},
);
const body = await res.json();
if (!res.ok) throw new Error(`${body.error.code}: ${body.error.message}`);
return body.data;
}
export default async function LineupPage() {
const lineup = await getLineup();
return (
<ol>
{lineup.slots.map((slot) => (
<li key={slot.id}>
<strong>{slot.artist?.name ?? slot.placeholderLabel}</strong>
{' — '}
{slot.stage} · {slot.day} · {slot.startTime}
</li>
))}
</ol>
);
}Note slot.artist?.name ?? slot.placeholderLabel. An unannounced set has no artist yet, and rendering "undefined" on a public festival page is a bad afternoon.
Free SEO
On the web surface the lineup, FAQ and news responses each carry a ready-made schema.org block in schemaOrg. Drop it into the page and search engines can read your running order without you modelling any of it:
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(lineup.schemaOrg) }}
/>What to build next
- Event detail — dates, venue, stages and brand colours. Usually the first call a site makes.
- FAQs — answers arrive as sanitised HTML, ready to inject.
- News — announcement posts, newest first.
- Ticket availability — live prices and what is sold out.
- Content version — a cheap probe so you can poll for changes without re-pulling the lineup.