Ticket availability
GET
/v1/events/{slug}/ticketsThe ticket types and release tiers a visitor is allowed to see, with live availability.
Visibility is resolved server-side, so this only ever returns what the public should see: time-gated tiers appear when they open, and hidden sold-out tiers stay hidden.
Read soldOut rather than inferring from available. available is null both when a tier is unlimited and when the organiser has chosen not to publish counts — soldOut is always authoritative.
Selling tickets from your own site is not part of this API yet. Use this feed to show prices and availability, and link to the Grofomo ticket page to sell; an embeddable checkout is coming.
Send ?surface=webOmit it and the request is treated as coming from the mobile app, which is gated on a different visibility setting — so a perfectly live event can return
404 not_found for no visible reason.Request
curl -sS \
-H 'X-Grofomo-Key: pk_live_YOUR_PUBLISHABLE_KEY' \
'https://api.grofomo.com/v1/events/summer-fest/tickets?surface=web'Path parameters
slugstringrequired- The event's URL slug. Globally unique, and fixed once the event is created. 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`.
promostring- A live unlock code, to reveal promo-gated ticket types.
Response
200 OK
{
"apiVersion": 1,
"data": {
"categories": [
{
"id": "6f1c8e2a-4b3d-4c9e-9f21-8a7d5e0b1c35",
"title": "Weekend",
"description": "All three days, with camping."
}
],
"types": [
{
"id": "b2d4a6f8-1c3e-4a5b-8d7f-2e9c0a1b3d46",
"categoryId": "6f1c8e2a-4b3d-4c9e-9f21-8a7d5e0b1c35",
"title": "Weekend camping ticket",
"description": "Entry Friday to Sunday, plus a camping pitch from Friday 10:00.",
"descriptionDoc": {
"type": "doc",
"content": [
{
"type": "paragraph",
"content": [
{
"type": "text",
"text": "Entry Friday to Sunday, plus a camping pitch from Friday 10:00."
}
]
}
]
},
"ticketsPerGroup": 1,
"minPerOrder": 1,
"maxPerOrder": 6,
"isHighlighted": true,
"highlightMessage": "Most popular",
"paymentPlan": {
"depositBps": 2500,
"installments": 3
},
"releases": [
{
"id": "3e5a7c9b-0d2f-4e6a-9b8c-1d3f5a7e9c03",
"label": "Tier 1",
"pricePennies": 9500,
"currency": "GBP",
"available": null,
"capacity": null,
"closesAt": null,
"opensAt": null,
"upcoming": false,
"soldOut": true,
"closed": false
},
{
"id": "9c7e5a3b-1f0d-4b2e-8a6c-4d2f0b8e6a14",
"label": "Tier 2",
"pricePennies": 11000,
"currency": "GBP",
"available": 340,
"capacity": 1500,
"closesAt": "2026-07-10T22:59:00.000Z",
"opensAt": null,
"upcoming": false,
"soldOut": false,
"closed": false
},
{
"id": "47a9e1c3-5b8d-4f20-9c6e-3a1b7d5f9e03",
"label": "Final release",
"pricePennies": 12500,
"currency": "GBP",
"available": null,
"capacity": null,
"closesAt": null,
"opensAt": "2026-07-11T09:00:00.000Z",
"upcoming": true,
"soldOut": false,
"closed": false
}
]
}
],
"quantityDiscounts": [
{
"id": "d1f3b5a7-9c2e-4d80-8b6f-5a3c1e7d9b05",
"name": "Group of four",
"kind": "tiered_percent",
"applicableReleaseIds": [
"9c7e5a3b-1f0d-4b2e-8a6c-4d2f0b8e6a14"
],
"tiers": [
{
"minQuantity": 4,
"value": 1000
}
],
"combinableWithPromo": false
}
]
}
}categoriesobject[]- Optional grouping for the types below.
categories[].iduuid- Referenced by `categoryId` on a ticket type.
categories[].titlestring- Section heading, e.g. "Weekend".
categories[].descriptionstring | nulltypesobject[]- Only what this caller is allowed to see. Hidden and promo-gated types are filtered server-side.
types[].iduuidtypes[].categoryIduuid | null- Groups this type under a category. Null when ungrouped.
types[].titlestringtypes[].descriptionstring | null- Plain-text description, for clients that render no rich text.
types[].descriptionDocobject | null- The same description as a ProseMirror document. Render this when present, otherwise `description`.
types[].descriptionDoc.type"doc"types[].descriptionDoc.contentobject | object | object | object | object | object[]may be absenttypes[].ticketsPerGroupnumber- Admissions issued per unit bought. 1 is a single ticket; higher means a table or group ticket.
types[].minPerOrdernumber- Fewest units one order may contain.
types[].maxPerOrdernumber- Most units one order may contain.
types[].isHighlightedboolean- The organiser wants this type visually promoted.
types[].highlightMessagestring | null- Badge text for a highlighted type, e.g. "Most popular".
types[].paymentPlanobject | null- Set when this type can be bought on a payment plan. Null otherwise.
types[].paymentPlan.depositBpsnumber- Deposit as basis points of the total, so 2500 is 25%.
types[].paymentPlan.installmentsnumber- Number of payments after the deposit.
types[].releasesobject[]- Price tiers, in sale order. Only tiers this caller is allowed to see are included.
types[].releases[].iduuidtypes[].releases[].labelstring- Release name, e.g. "Tier 2" or "Final release".
types[].releases[].pricePenniesnumber- Price in minor units, so 9500 is £95.00.
types[].releases[].currencystring- ISO 4217 code, e.g. "GBP".
types[].releases[].availablenumber | null- Tickets left. Null means EITHER unlimited OR the organiser has chosen not to publish counts — so this can never gate a buy button on its own. Use `soldOut`.
types[].releases[].capacitynumber | nullmay be absent- Total allocation, hidden under the same rule as `available`. With it you can show percent sold.
types[].releases[].closesAtstring | nullmay be absent- When this release stops selling. Null means no cut-off.
types[].releases[].opensAtstring | nullmay be absent- When this release starts selling. Set alongside `upcoming`, and the target for a countdown.
types[].releases[].upcomingbooleanmay be absent- A teaser: listed but not yet purchasable. Takes precedence over `soldOut` and `closed`, which are both false while it holds.
types[].releases[].soldOutboolean- Authoritative. Gate your buy button on this, never on `available`.
types[].releases[].closedboolean- The sale window has ended. Still shown, no longer purchasable.
quantityDiscountsobject[]may be absent- Automatic discounts, in enough detail to advertise ("Buy 4+ and save 10%") and preview locally. The server stays authoritative at checkout.
quantityDiscounts[].iduuidquantityDiscounts[].namestring- Organiser-facing name, e.g. "Group of four".
quantityDiscounts[].kind"tiered_percent" | "tiered_fixed" | "buy_n_get_m" | "bundle_price"- Which of the fields below apply, and how `tiers[].value` should be read.
quantityDiscounts[].applicableReleaseIdsuuid[]- Releases this rule applies to. Empty means the whole order.
quantityDiscounts[].tiersobject[]may be absent- Present for the two tiered kinds.
quantityDiscounts[].tiers[].minQuantitynumber- Quantity at which this tier starts applying.
quantityDiscounts[].tiers[].valuenumber- Basis points off for `tiered_percent` (1000 = 10%), or pennies off per ticket for `tiered_fixed`.
quantityDiscounts[].buyQuantitynumbermay be absent- The "buy N" of a `buy_n_get_m` rule.
quantityDiscounts[].freeQuantitynumbermay be absent- The "get M free" of a `buy_n_get_m` rule.
quantityDiscounts[].bundleSizenumbermay be absent- Tickets per bundle, for `bundle_price`.
quantityDiscounts[].bundlePricePenniesnumbermay be absent- Price of one whole bundle, for `bundle_price`.
quantityDiscounts[].combinableWithPromoboolean- Whether a promo code can stack on top of this discount.
Generated from EventTicketsResponse
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`.
Notes
- Rate limited to roughly 60 requests per minute per caller. Guidance, not a contract — see caching and rate limits.