Responses and errors
The response envelope, the error shape, and how to tell them apart.
Examples use a fictional festival, summer-fest. Sign in at events.grofomo.com and they switch to your own events.
Every endpoint — success or failure — returns the same envelope.
Success
{
"apiVersion": 1,
"data": { }
}Failure
{
"apiVersion": 1,
"error": {
"code": "not_found",
"message": "Event not found"
}
}code is a stable, machine-readable string — branch on it. message is written for a developer reading a log, and its wording may change; never parse it.
Handling both
Because the shape is uniform, one helper covers the whole API:
async function grofomo(path) {
const res = await fetch(`https://api.grofomo.com${path}`, {
headers: { 'X-Grofomo-Key': process.env.GROFOMO_SECRET_KEY },
});
const body = await res.json();
if (!res.ok) {
throw new Error(`${body.error.code}: ${body.error.message}`);
}
return body.data;
}Read the body once. res.json() consumes the stream, so calling it in both branches throws on the second call.
Status codes
| Status | Code | What it means |
|---|---|---|
| 200 | — | Fine. |
| 400 | bad_request | The request body or a parameter did not validate. |
| 400 | turnstile_failed | The bot-check token was missing or invalid. |
| 401 | unauthorized | No key, or a key that is invalid, revoked, or out of scope. |
| 403 | origin_not_allowed | The browser's domain is not on the organiser's allowlist. |
| 404 | not_found | No such event on this surface. Check ?surface=web. |
| 429 | — | Rate limited. Honour Retry-After. |
| 500 | fetch_failed | Our fault. Safe to retry. |
Retry 429 and 5xx. Never retry 401, 403 or 404 — they mean something is configured wrong, and a retry loop just turns one problem into a traffic problem.
Versioning
apiVersion is 1 and will stay 1 while changes are additive. New fields can appear at any time, so ignore fields you do not recognise rather than validating strictly against a fixed shape.
A breaking change would arrive as a new version, announced in the changelog before it lands.