Grofomo API
Browse the docs

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-Key identifies the organisation. Most organisers now require it; some older ones do not yet. Send it either way — see Authentication.
  • ?surface=web tells 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 answer 404. 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();

  // startTime is an ISO instant, not a wall-clock string — always format it
  // in the EVENT's timezone (from event detail), or a Berlin visitor sees
  // your Bristol set times shifted an hour.
  const setTime = new Intl.DateTimeFormat('en-GB', {
    hour: '2-digit',
    minute: '2-digit',
    timeZone: 'Europe/London', // event detail: data.timezone
  });

  return (
    <ol>
      {lineup.slots.map((slot) => (
        <li key={slot.id}>
          <strong>{slot.artist?.name ?? slot.placeholderLabel}</strong>
          {' — '}
          {slot.stage} · {slot.day} ·{' '}
          {slot.startTime ? setTime.format(new Date(slot.startTime)) : 'TBA'}
        </li>
      ))}
    </ol>
  );
}

Two details in the render worth copying:

  • slot.artist?.name ?? slot.placeholderLabel — an unannounced set has no artist yet, and rendering "undefined" on a public festival page is a bad afternoon.
  • Times are formatted in the event's timezone, never the visitor's. A set that crosses midnight keeps its day on the festival day it belongs to, so group by day rather than by the date inside the timestamp.

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.