Grofomo API
Browse the docs

Examples use a fictional festival, summer-fest. Sign in at events.grofomo.com and they switch to your own events.

Reserve tickets

POST/v1/events/{slug}/tickets/reserve

Hold the tickets in the cart and create the order. The hold is oversell-proof and lasts 15 minutes (reservedUntil); show the buyer a countdown, because details and pay refuse an expired order with 409 expired and the only way on is a fresh reserve. Nothing needs cancelling: an unpaid hold is released on its own.

Prices are rebuilt server-side from the release ids and quantities. Whatever your page showed, the order carries what the server computed, so read the totals off the response rather than off your own arithmetic.

checkoutToken is the credential for the order. Every later call (details, pay, complete-free, status) is addressed by it and nothing else. It authorises the checkout only: not the tickets, not a transfer, not the receipt, all of which reach the buyer by email. It expires 24 hours after the reserve. Keep it in memory or session storage and out of URLs, logs and analytics.

A zero-total order (a free tier, or a code that discounts everything) is finished with complete free instead of pay.

Reserve fails with 409 unavailable until the organiser has finished connecting Stripe. That is the organiser's setup to complete, not something a key or a domain can fix.

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 \
  -X POST \
  -H 'X-Grofomo-Key: pk_live_YOUR_PUBLISHABLE_KEY' \
  -H 'Content-Type: application/json' \
  -d '{"items":[{"releaseId":"9c7e5a3b-1f0d-4b2e-8a6c-4d2f0b8e6a14","quantity":2}],"promoCode":"EARLYBIRD","funnelSessionToken":"9d1f7c2a-3b6e-4a8d-9e0f-1c2b3a4d5e6f","attribution":{"channel":"social","utmSource":"instagram","referrer":"instagram.com"}}' \
  'https://api.grofomo.com/v1/events/summer-fest/tickets/reserve?surface=web'

Request body

itemsobject[]required
The cart. One entry per release; up to 20 releases.
items[].releaseIduuidrequired
A release id from the ticket availability feed.
items[].quantitynumberrequired
Units of that release. Must sit within the type's `minPerOrder` and `maxPerOrder`.
promoCodestring
A promo code to apply. An invalid code does not fail the reserve: the order is priced without it and `discountPennies` says so.
affiliateCodestring
An affiliate code, when the buyer arrived on an affiliate link. Credits the sale and reveals affiliate-gated tiers.
artistIduuid
The lineup artist the buyer arrived through, when your page came from an artist share link. Re-checked against the lineup server-side.
paymentPlanIduuid
A payment plan offer id from `paymentPlans` on every PAID tier in the cart (free tiers carry no offers and are ignored). Omit to pay in full. Refused with `plan_unavailable` (409) unless every paid tier lists it and it is still open.
attributionobject
Where the sale came from, for the organiser's sales-channel report. Optional and PII-free. The flat fields are the LAST touch; `firstTouch` is the first.
attribution.channel"short_link" | "poster" | "artist" | "affiliate" | …required
attribution.refIdstring
attribution.utmSourcestring
attribution.utmMediumstring
attribution.utmCampaignstring
attribution.utmTermstring
attribution.utmContentstring
attribution.referrerstring
attribution.gclidstring
attribution.fbclidstring
attribution.atnumber
attribution.firstTouchobject
The first tagged visit in your attribution window, when you track one. The flat fields are the last touch.
attribution.firstTouch.channel"short_link" | "poster" | "artist" | "affiliate" | …required
attribution.firstTouch.refIdstring
attribution.firstTouch.utmSourcestring
attribution.firstTouch.utmMediumstring
attribution.firstTouch.utmCampaignstring
attribution.firstTouch.utmTermstring
attribution.firstTouch.utmContentstring
attribution.firstTouch.referrerstring
attribution.firstTouch.gclidstring
attribution.firstTouch.fbclidstring
attribution.firstTouch.atnumber
funnelSessionTokenstring
The same opaque session token you send on funnel beacons, so the organiser's funnel report can join this checkout to the page views before it. Omit both, or send both.
deviceHandoffstring
Internal: a signed hand-off from the Grofomo mobile app. Leave it out.
Generated from ReserveRequest

Path parameters

slugstringrequired
The event's URL slug. Globally unique. It can change if the organiser renames the event, but an old slug keeps reaching the same event, so a stored one never breaks. 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`.

Response

200 OK
{
  "apiVersion": 1,
  "data": {
    "orderId": "c3a1e5d7-9b2f-4e6a-8c0d-2f4b6a8c0e12",
    "checkoutToken": "YzNhMWU1ZDctOWIyZi00ZTZhLThjMGQtMmY0YjZhOGMwZTEyOjE3ODk2NDM2MDAwMDA6dGt0LWNoZWNrb3V0.ZXhhbXBsZS1zaWduYXR1cmU",
    "reservedUntil": "2026-07-04T18:15:00.000Z",
    "currency": "GBP",
    "subtotalPennies": 22000,
    "discountPennies": 2200,
    "bookingFeePennies": 1535,
    "totalPennies": 21335,
    "quantityDiscount": null,
    "promoDiscountPennies": 2200,
    "paymentPlan": null,
    "crossEventOffers": []
  }
}
orderIduuid
The order's internal id. For your records; every later call uses `publicId`.
checkoutTokenstring
The credential for this order on the details, pay, complete-free and status endpoints: put it in the path where those show `{checkoutToken}`. It authorises the checkout and nothing else (not the tickets, not transfers, not the receipt: those reach the buyer by email), and it expires 24 hours after the reserve. Keep it in memory or session storage and out of URLs, logs and analytics.
reservedUntilstring
ISO instant the 15-minute inventory hold ends. Show a countdown; after it, details and pay answer 409 `expired` and the buyer starts again.
currencystring
ISO 4217 code every amount on this order is in.
subtotalPenniesnumber
Face value of the cart before discounts, in minor units.
discountPenniesnumber
Everything taken off: the automatic quantity discount plus the promo code, in minor units.
bookingFeePenniesnumber
The booking fee for the order, in minor units.
totalPenniesnumber
What the buyer pays: subtotal minus discount plus booking fee, in minor units. Zero means a free order: call complete-free instead of pay.
quantityDiscountobject | nullmay be absent
The automatic quantity discount that applied, or null when none did.
quantityDiscount.iduuid
The rule that gave most. When a giveaway of a different ticket combined with the deal that earned it, this is the larger of the two and `name` names both.
quantityDiscount.namestring
The discount's name, to show on the receipt line. Both names, joined with " + ", when a giveaway combined with a deal on the tickets that earned it.
quantityDiscount.discountPenniesnumber
Its share of `discountPennies`.
promoDiscountPenniesnumbermay be absent
The promo code's share of `discountPennies`, in minor units.
paymentPlanobject | null
The instalment plan the order is on, or null when paying in full.
paymentPlan.iduuid
The payment plan offer the order is on.
paymentPlan.namestring
The organiser's name for the plan, to show the buyer.
paymentPlan.depositPenniesnumber
What the payment step charges now: the deposit share of the face value plus the whole booking fee, in minor units.
paymentPlan.balancePenniesnumber
The remaining face value, collected over the instalments, in minor units.
paymentPlan.payByAtstring
ISO instant the plan must be fully paid by. The last instalment lands on or before it.
paymentPlan.installmentsobject[]
The exact schedule the saved card is charged on once the deposit is paid. Show it before the buyer pays.
paymentPlan.installments[].seqnumber
1-based position in the schedule.
paymentPlan.installments[].amountPenniesnumber
Charged on `dueAt`, in minor units.
paymentPlan.installments[].dueAtstring
ISO instant the instalment is taken.
crossEventOffersobject[]may be absent
Offers this cart earns on the organiser's other events. The personal codes are minted once the order completes and arrive on the status endpoint and by email.
crossEventOffers[].iduuid
The offer id.
crossEventOffers[].namestring
The organiser's label for the offer.
crossEventOffers[].targetEventNamestring
The event the code is spent on.
crossEventOffers[].targetEventSlugstring
Slug of the target event, for a link to its storefront.
crossEventOffers[].discountType"percentage" | "fixed"
How the code discounts: a percentage of the tier price, or a fixed amount off.
crossEventOffers[].discountValuenumber
Basis points for percentage (1000 = 10%), pennies for fixed.
crossEventOffers[].codeValidDaysnumber | null
The code expires this many days after purchase; null = never.
Generated from ReservedOrderResponse

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`.
bad_requestHTTP 400
The body did not match the schema.
unknown_releaseHTTP 400
A `releaseId` is not on this event or not visible to this caller.
not_on_saleHTTP 409
A release in the cart is closed, not yet open, or sold out.
per_order_minHTTP 400
Fewer units of a type than its `minPerOrder`.
per_order_maxHTTP 400
More units of a type than its `maxPerOrder`.
invalid_promoHTTP 400
The promo code does not exist or does not cover this cart.
expired_promoHTTP 400
The promo code is outside its dates.
promo_used_upHTTP 409
The promo code has reached its use limit.
promo_requiredHTTP 400
A release in the cart is gated behind a promo code and none was sent.
affiliate_requiredHTTP 400
A release in the cart is affiliate-gated and no valid `affiliateCode` was sent.
cancelledHTTP 410
The event has been cancelled.
sold_outHTTP 409
Somebody else took the last of a release between your quote and this call.
unavailableHTTP 409
The organiser has not finished connecting Stripe, so nothing can be sold yet.
plan_unavailableHTTP 409
The `paymentPlanId` is not open on every paid tier in the cart (free tiers are ignored).
too_many_holdsHTTP 429
Too many live reservations on this event from the same connection (or the same app device). Show the message and let the buyer try again in a few minutes; a hold lapses in 15 minutes.

Notes

  • Rate limited to roughly 20 requests per minute per caller. Guidance, not a contract — see caching and rate limits.
  • Cache-Control: no-store