Skip to content

API documentation

Your site, our data, in three lines

VenueSpine owns the events, the seat map, the holds and the checkout. Your website owns the look. Drop in a custom element, or read the API directly and build the whole flow yourself.

Base URLhttps://api.venuespine.co.uk/api/public/v1

On this page

What you can build

Seat map as one custom element

Three lines of HTML gets you a rendered floor plan, live availability over SSE, seat holds, buyer capture and card checkout. You never rebuild a seat canvas.

Live availability, not polling

Availability streams over server-sent events, so a seat someone else just took goes grey on your page without a refresh and without you writing a poller.

Two key types, scoped per account

A pk_ key is browser-safe and read-mostly. An sk_ key never leaves your server. Both are issued per account and per scope, and both are revocable.

Bar orders from the seat

Your app posts what a paid guest wants; the till accepts it into a tab at their table. Progress streams back over server-sent events, and the venue decides per event when ordering is open.

Additive by contract

New parameters are optional and new routes are additive. An integration written against today’s API keeps working tomorrow.

Quickstart

Three requests take you from nothing to a paid booking. Everything below is that, expanded.

Terminal
# 1. List what is on sale
curl "https://api.venuespine.co.uk/api/public/v1/events?window=upcoming&rows=12" \
  -H "X-API-KEY: pk_live_xxxxxxxxxxxxxxxxxxxxxxxx"

# 2. Read one event, including its seating mode
curl "https://api.venuespine.co.uk/api/public/v1/events/8f2c14e0-3a9b-4e77-9c11-2b6f0d5a7e31" \
  -H "X-API-KEY: pk_live_xxxxxxxxxxxxxxxxxxxxxxxx"

# 3. Create a booking and get a checkout redirect
curl -X POST "https://api.venuespine.co.uk/api/public/v1/bookings" \
  -H "X-API-KEY: pk_live_xxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "eventId": "8f2c14e0-3a9b-4e77-9c11-2b6f0d5a7e31",
    "buyer": { "firstName": "Olivia", "lastName": "Bennett", "phone": "+447700900123" },
    "items": [{ "ticketTypeId": "b41f…", "quantity": 2 }],
    "successUrl": "https://yoursite.com/book/complete"
  }'

Not building a booking flow? The built-in storefront gives you the same thing as a hosted, themeable site with no code at all.

Authentication

Every request carries an API key. Keys are issued per account and per scope in VenueSpine, are hashed at rest, and can be revoked at any time. There are two types, and the difference matters.

pk_

Publishable

Browser-safe by design. Reads published events, seats and catalogue, and creates bookings. Ship it in your front-end - that is what it is for.

sk_

Secret

Server-side only. Does everything a publishable key does, plus writes enquiries and customers, and sends a reservation request or books a stay without a captcha. If it ever reaches a browser, revoke it and issue a new one.

Terminal and JavaScript
# Either header works. Use X-API-KEY unless you have a reason not to.
curl "https://api.venuespine.co.uk/api/public/v1/events" -H "X-API-KEY: pk_live_xxxxxxxxxxxxxxxxxxxxxxxx"
curl "https://api.venuespine.co.uk/api/public/v1/events" -H "Authorization: Bearer pk_live_xxxxxxxxxxxxxxxxxxxxxxxx"

# EventSource cannot send headers, so the SSE stream accepts ?key=
# This is safe only with a publishable key.
new EventSource(
  "https://api.venuespine.co.uk/api/public/v1/events/<id>/seats/stream?key=pk_live_xxxxxxxxxxxxxxxxxxxxxxxx"
);

Scopes are enforced, not advisory. A key scoped to events gets a 403 API_KEY_FORBIDDEN on a catalogue route, even though the key is valid.

Conventions

Base URL
https://api.venuespine.co.uk/api/public/v1
Money
Pounds, as a number with at most two decimals: £22.00 is 22, £4.50 is 4.5. Every amount is set by the API; you never send one. Round to pence before comparing.
Dates
ISO 8601 in UTC. Format for the venue’s timezone on your side.
Pagination
first and rows in, { data, total } back. total reflects the filtered set, which is what makes a pager correct. rows is capped at 50.
Filtering
Server-side, always. Do not fetch everything and filter in the browser - the cap will silently cut your list short.
Ids
Opaque strings. Do not parse them or assume a format.
Compatibility
Additive. New parameters are optional and new routes are additive, so today’s calls keep working.

Events

Everything published to web, in the window you ask for. Draft, unpublished and cancelled events are never returned, so you cannot accidentally leak one.

GET /events

List published events

Returns a page slice plus a total that reflects the filtered set, which is what makes a pager correct. Only events published to web are ever returned. Read ticketSales before you render a Book button: an event whose tickets are sold on Ticketmaster reports bookable false and carries externalTicketUrl instead.

Keyspk_sk_Scopeevents

Parameters

window'upcoming' | 'recent' | 'archived' | 'past'
upcoming is future events ascending. recent is the N most-recent past events. archived is past events beyond that recent window. past is every past event, flat. Default upcoming
firstinteger >= 0
Zero-based offset of the first row to return. Default 0
rowsinteger 1-50
Page size. Capped at 50 server-side. Default 20
recentCountinteger 1-50
Where the recent window ends and archived begins. Send the same value to both or the two windows drift and an event appears twice or not at all. Default 9
searchstring 1-120
Case-insensitive match on the event name.
typestring 1-120
Exact published event-type name, as returned by /events/types.
sort'date_asc' | 'date_desc' | 'name_asc' | 'name_desc'
Omit it and each window keeps its natural order, so the parameter is backward-compatible.

Returns{ data: PublicEvent[], total: number }

GET /events/types

List event types that have events

Only types that actually have published events in the requested window. Use this to build a filter that never offers an option returning zero results - never derive the list from one page of events.

Keyspk_sk_Scopeevents

Parameters

window'upcoming' | 'past'
Which side of today to look at. Default upcoming

Returns{ data: { name: string, color: string | null }[] }

GET /events/:id

Read one event in full

The only place seatingMode, ticket tiers, bundles and live remaining counts are returned. Always read this before starting a booking - list rows do not carry seating. acceptsPromoCodes is true when the event has a live promo code, so show a code box only then.

Keyspk_sk_Scopeevents

Parameters

idstring (path)required
The event id.

ReturnsPublicEventDetail

Request and response
GET https://api.venuespine.co.uk/api/public/v1/events?window=upcoming&first=0&rows=2&sort=date_asc

{
  "data": [
    {
      "id": "8f2c14e0-3a9b-4e77-9c11-2b6f0d5a7e31",
      "slug": "summer-sessions",
      "name": "Summer Sessions",
      "type": "Live music",
      "typeColor": "#0085c0",
      "venue": "The Courtyard",
      "start": "2026-09-12T18:00:00.000Z",
      "end": "2026-09-12T23:30:00.000Z",
      "allDay": false,
      "capacity": 240,
      "status": "scheduled",
      "bookable": true,
      "ticketSales": "self",
      "externalTicketUrl": null,
      "ticketSalesChannels": "venue",
      "ticketmasterUrl": null,
      "imageUrl": "https://cdn.venuespine.co.uk/…/summer-sessions.jpg",
      "tickets": [
        { "id": "…", "name": "General", "description": null, "price": 18, "available": 112, "soldOut": false }
      ],
      "bundles": [],
      "media": []
    },
    {
      "id": "b71d09a4-52e8-41c6-9f0a-6d3c8e1477bb",
      "name": "Comedy Night",
      "bookable": false,
      "ticketSales": "ticketmaster",
      "externalTicketUrl": "https://www.ticketmaster.co.uk/comedy-night-tickets/event/1F006123AB45CD67",
      "ticketSalesChannels": "ticketmaster",
      "ticketmasterUrl": "https://www.ticketmaster.co.uk/comedy-night-tickets/event/1F006123AB45CD67"
    }
  ],
  "total": 47
}

An event can be listed by VenueSpine but sold somewhere else. When ticketSales is "ticketmaster", bookable is false, externalTicketUrl is the page to send the buyer to, and POST /bookings for that event is refused with EVENT_SOLD_EXTERNALLY. Treat externalTicketUrl as the Book button: link out to it rather than opening your own checkout. Integrations written before this field existed keep working - they only read bookable, which is already false. ticketSalesChannels says where the venue sells the event (venue, ticketmaster or both), and ticketmasterUrl is the linked Ticketmaster page whenever Ticketmaster is one of them. Read those two when your rule is to sell only what the venue alone sells: book venue events yourself and send ticketmaster and both to ticketmasterUrl. allDay is true for an event that runs whole days: start is the first day at midnight and end the last day at 23:59:59 in the venue timezone, so show the dates without times.

VenueSpine owns the boundary between recent and archived, so send the same recentCount to both calls or the two windows drift and an event shows up twice.

Seating and live availability

Read seatingMode from the event first. When it is reserved, sell by seat using the endpoints below. When it is unreserved, sell by quantity against ticket tiers and ignore this section entirely.

GET /events/:id/seats

Availability snapshot

The current status of every bookable seat or table. Use it to paint the map first, then keep it current from the stream. It is also the resync path after a dropped connection.

Keyspk_sk_Scopeevents

Parameters

idstring (path)required
The event id.

Returns{ objects: { id, capacity, taken, available, status: 'available' | 'held' | 'sold', price }[] }

GET /events/:id/seats/stream

Live availability over SSE

A server-sent event stream that pushes availability as it changes, so a seat someone else takes greys out on your page without a poller. It sends a snapshot event on connect and a seats event, with the same shape as GET /events/:id/seats, every time availability changes. The key goes in the query because EventSource cannot send headers; the bar streams take theirs the same way.

Keyspk_sk_Scopeevents

Parameters

idstring (path)required
The event id.
keystring (query)
Your publishable key, when you cannot set a header. Never put a secret key in a query string.

Returnsevent: snapshot | seats data: { objects: [...] }

Response and live updates
GET https://api.venuespine.co.uk/api/public/v1/events/<id>/seats

{
  "objects": [
    { "id": "s_a1", "capacity": 1, "taken": 0, "available": 1, "status": "available", "price": 22 },
    { "id": "s_a2", "capacity": 1, "taken": 1, "available": 0, "status": "held",      "price": 22 },
    { "id": "t_k",  "capacity": 8, "taken": 8, "available": 0, "status": "sold",      "price": 22 }
  ]
}

// Live updates over SSE - a "snapshot" event on connect, then a "seats" event
// with the same shape every time availability changes.
const stream = new EventSource(
  "https://api.venuespine.co.uk/api/public/v1/events/<id>/seats/stream?key=pk_live_…"
);
const repaintFrom = (event) => repaint(JSON.parse(event.data).objects);
stream.addEventListener("snapshot", repaintFrom);
stream.addEventListener("seats", repaintFrom);

Paint from the snapshot, then keep it current from the stream. On reconnect, re-read the snapshot before trusting the stream again - a dropped connection may have missed a change.

Bookings and checkout

One call creates the booking, holds the selection for fifteen minutes and hands back a payment redirect. Money settles into the venue’s own Stripe account - VenueSpine never holds it, and neither do you.

POST /bookings

Create a booking and start checkout

Holds the selection for 15 minutes and returns a Stripe Checkout redirect URL. Send at least one of items, bundles or seats; the API prices them from the event, so the body carries no amounts. From a publishable key the request is a browser form, so it carries a Turnstile captchaToken where the API has Turnstile enabled and a hidden hp honeypot that must stay empty; a secret key from your server needs neither. One address may hold at most three open bookings per event at a time.

Keyspk_sk_Scopeevents

Body

eventIdstringrequired
The event being booked.
captchaTokenstring
Required from a publishable key when the API has Turnstile enabled: the token the Turnstile widget hands you. The seat-map embed sends it for you when it has a site key.
hpany
A honeypot. Render it hidden and send it empty; a filled value is answered with a success-shaped body and nothing is booked.
buyer.firstNamestring 1-100required
Buyer first name.
buyer.lastNamestring 1-100required
Buyer surname.
buyer.phonestring 1-50required
Required - this is how the venue reaches the buyer on the night.
buyer.emailstring email
Optional, but the ticket cannot be emailed without it.
buyer.marketingOptInboolean
Records marketing consent with its source and evidence. Leave it out rather than defaulting it true.
items[]{ ticketTypeId, quantity 1-100 }
General-admission tickets. Up to 50 entries.
bundles[]{ bundleId, quantity 1-100 }
Ticket bundles, where the event offers them.
seats[]{ id, quantity, ticketTypeId? }
Reserved seating. 1 to 50 entries, each a seat or table id from /seats.
promoCodestring
A promo code the buyer entered, in any case. It is checked again when the booking is made, so a code that ran out after your quote is refused with its own error rather than silently ignored.
successUrlurl
Where the buyer returns after paying. This and the two below must be https, on the VenueSpine app, the storefront, or an origin listed under the key’s Return origins (up to ten per key); anything else is refused with RETURN_URL_NOT_ALLOWED.
cancelUrlurl
Where the buyer returns if they cancel at the payment page.
failureUrlurl
Where the buyer returns if payment fails.

Returns{ bookingId, reference, status: 'pending_payment' | 'paid', redirectUrl }

POST /bookings/quote

Price a selection, with or without a promo code

Prices exactly what POST /bookings would charge, so you can show a promo code’s discount before the buyer pays. It holds nothing and books nothing: the booking checks the code again. subtotal is the amount the booking will charge, the same figure GET /bookings/:id returns; gross is before the code. A code counts in tickets and never discounts a bundle. Attempts that carry a code are rate limited per address.

Keyspk_sk_Scopeevents

Body

eventIdstringrequired
The event being booked.
items[] / bundles[] / seats[]as for POST /bookingsrequired
The same selection you will book. Send at least one.
promoCodestring
The code to apply. Leave it out for plain pricing.

Returns{ currency, gross, discount, subtotal, promoCode: { code, discountType, discountValue, tickets } | null, lines: [{ name, detail, quantity, unitPrice, lineTotal, discount }] }

GET /bookings/:id

Poll a booking

Read the booking after the buyer returns from checkout. Poll this rather than trusting the redirect - the redirect tells you the buyer came back, not that the money arrived. subtotal is the amount in pounds; the tickets themselves come from GET /bookings/:id/tickets with a secret key.

Keyspk_sk_Scopeevents

Parameters

idstring (path)required
The booking id.

Returns{ id, reference, status: 'pending_payment' | 'paid' | 'failed' | 'expired' | 'cancelled' | 'refunded', subtotal, discount, promoCode, currency }

GET /bookings/:id/tickets

Read the tickets on a paid booking

The issued tickets with the code the door scanner reads, the seat label where the event is reserved seating, and whether each has been scanned in. Secret key only: a code is the ticket, so this never goes to a browser. Tickets are issued on demand the first time a paid booking is read. A booking that is not paid answers with an empty list.

Keyssk_Scopeevents

Parameters

idstring (path)required
The booking id.

Returns{ tickets: [{ id, code, label, ticketTypeName, seatLabel, sequence, status, checkedInAt }] }

Request and response
POST https://api.venuespine.co.uk/api/public/v1/bookings
Content-Type: application/json
X-API-KEY: pk_live_xxxxxxxxxxxxxxxxxxxxxxxx

{
  "eventId": "8f2c14e0-3a9b-4e77-9c11-2b6f0d5a7e31",
  "buyer": {
    "firstName": "Olivia",
    "lastName": "Bennett",
    "email": "olivia@example.co.uk",
    "phone": "+447700900123",
    "marketingOptIn": false
  },
  "seats": [{ "id": "s_a1", "quantity": 1 }],
  "promoCode": "COMEDY-20",
  "successUrl": "https://yoursite.com/book/complete",
  "cancelUrl": "https://yoursite.com/book/cancelled"
}

// 201 Created
{
  "bookingId": "9c4d…",
  "reference": "BK-3F9A21C7D0",
  "status": "pending_payment",
  "redirectUrl": "https://checkout.stripe.com/c/pay/…"
}

// promoCode is optional. Show the discount before the buyer pays with POST /bookings/quote,
// which takes the same selection and code and returns { gross, discount, subtotal, promoCode }.
// A booking with nothing left to pay skips payment and comes back status: "paid" with redirectUrl: null.
// Otherwise send the buyer to redirectUrl, then poll:
//   GET https://api.venuespine.co.uk/api/public/v1/bookings/9c4d…
//   -> { "id", "reference", "status": "pending_payment" | "paid" | "failed" | "expired" | "cancelled" | "refunded",
//        "subtotal": 17.6, "discount": 4.4, "promoCode": "COMEDY-20", "currency": "GBP" }

The redirect back to your site means the buyer returned, not that the money arrived. Always confirm with GET /bookings/:id before you show a ticket or send a confirmation.

Catalogue

For accounts selling something other than a ticket - a caterer’s packages, a kitchen’s products. Requires a key scoped to catalog.

GET /catalog/categories

List catalogue categories

The published category tree for a catalogue-led account.

Keyspk_sk_Scopecatalog

Returns{ data: Category[] }

GET /catalog/products

List products and packages

Published products with pricing, filterable by category. Each product carries its allergens and diets as keys, and its modifierGroups: the choices a guest makes when ordering it (a side, a doneness, extras), each group with minChoices, maxChoices and modifiers that each carry a priceDelta added to the product price. A group with minChoices of 1 or more is a required choice. Show the groups on your menu page; ordering with them from the seat is not yet open on the public bar order endpoint.

Keyspk_sk_Scopecatalog

Parameters

categorystring
Restrict to one category.
firstinteger
Offset. Default 0
rowsinteger
Page size. Default 20

Returns{ data: Product[], total: number }

GET /catalog/products/:id

Read one product

Full product detail including variants, package contents and modifierGroups (see the list endpoint for the shape).

Keyspk_sk_Scopecatalog

Parameters

idstring (path)required
The product id.

ReturnsProduct

Bar menu

Publish the drinks list straight from the till’s own stock, so the menu on your site is the one the bar is actually pouring. Prices, categories and specials come through; cost prices, stock levels and ingredients the till does not sell never do. Requires a key scoped to bar.

GET /bar/menu/categories

List bar menu categories

The categories that hold something the till sells right now: an active special, or an active item that is not an ingredient. Any other category is left out, so your page never renders an empty section.

Keyspk_sk_Scopebar

ReturnsCategory[]

GET /bar/menu

List bar menu items

Active stock items and specials with their prices, and only what the till sells: an ingredient (an item with "Sell it on the till" unticked, such as flour or mince) is left out, and total does not count it. Items are sold on the till unless marked, and specials always are. Cost prices and stock levels are never exposed. Where a category is poured by the tot or the glass, the per-serving price comes through resolved, whether it is set on the item or inherited from its category.

Keyspk_sk_Scopebar

Parameters

kind'stock' | 'special'
Restrict to ordinary items or to specials. Both are returned by default.
categoryIdstring
Restrict to one category.
searchstring
Match on name or SKU.
sort'name_asc' | 'name_desc' | 'price_asc' | 'price_desc'
Ordering. Default name_asc
firstinteger
Offset. Default 0
rowsinteger
Page size, up to 50. Default 20

Returns{ data: MenuItem[], total: number }

GET /bar/menus

List published designed menus

The menus the account has designed in VenueSpine and published to a public page, each with the hosted page address. Render the page in a frame or a web view, or fetch its JSON with no key at /api/public/menus/{accountSlug}/{slug} - the document, the live price snapshot behind it and a QR code for the page - to draw it natively. Up to fifty, by name.

Keyspk_sk_Scopebar

Returns{ menus: [{ id, name, description, accountSlug, slug, publicUrl, publishedAt }] }

GET /bar/ordering

Is the bar taking guest orders for an event?

Whether guests at the event may order from the bar through your app or site right now. The venue sets a mode per event - off, during the show, whenever a till is open, or only once the bar opens it from the till - and ordering is open only inside that window and while a POS session is trading on the event. reason says which of those is missing: off, outside_window, no_open_session or not_opened (the bar has not opened it yet). Read it when your ordering screen opens, then follow GET /bar/ordering/stream rather than polling; it is never cached.

Keyspk_sk_Scopebar

Parameters

eventIdstringrequired
The event id or slug. Must be published to web on the account behind your key.

Returns{ enabled: boolean, window: 'off' | 'show' | 'always' | 'manual', opensAt, closesAt, tableService: boolean, note, reason: 'off' | 'outside_window' | 'no_open_session' | 'not_opened' | null }

GET /bar/ordering/stream

Follow whether the bar is taking orders, live

A text/event-stream that sends the same shape as GET /bar/ordering as a status event on connect and again every time it changes - the venue flips the mode, a till opens, closes, locks or unlocks, or the bar opens ordering from the till. Keep it open while your ordering screen is on screen and close it when the guest leaves, instead of polling. The key goes in the query because EventSource cannot set headers; publishable is fine.

Keyspk_sk_Scopebar

Parameters

eventIdstringrequired
The event id or slug.
keystring (query)required
Your publishable key. Never a secret key in a URL.

Returnsevent: status data: { enabled, window, opensAt, closesAt, tableService, note, reason }

Bar ordering

Let a guest order from their seat. Your app sends what they want against their paid booking; the bar sees it on the till with their name and table, accepts it into a tab or declines it with a reason, and the guest follows it live. Nothing is charged through the API - the bar takes payment at the table as it always does. The venue decides per event whether ordering is on at all, so read GET /bar/ordering before you show an order button. Requires a key scoped to bar.

POST /bar/orders

Place a bar order for a paid guest

Send what a guest wants to the bar that is trading on the event. The guest is identified by a paid booking on the same event, so the till knows who ordered and, where the event is reserved seating, which table they sit at. Nothing is charged: the bar accepts the order into a tab and takes payment as it always does. Items are priced from the current menu at the moment the bar accepts. Requires an Idempotency-Key header; send the same key when you retry and you get the same order back with Idempotent-Replay: true. Secret key only.

Keyssk_Scopebar

Body

eventIdstringrequired
The event the guest is at.
bookingIdstringrequired
A paid booking on that event, made through your key. The order carries its buyer and seat.
service'table' | 'collect'
Bring it to the table, or the guest collects at the bar. table is refused when the venue has switched table service off for the event. Default table
tableLabelstring
Where the guest says they are, up to 60 characters. Ignored when the booking already has a seat; the seating plan wins.
items{ itemId, quantity, saleUnit?, note? }[]required
One to thirty lines from GET /bar/menu, up to twenty of each. saleUnit is unit or serving (serving only where the item is poured by the tot or glass; otherwise SERVING_NOT_SOLD). note is up to 255 characters and the bar sees it on the line.
notesstring
A note for the whole order, up to 500 characters.
customerIdstring
A VenueSpine customer id to attach when the booking itself has none. Ignored otherwise.

Returns{ id, reference, status, service, tableLabel, total, items: [{ name, quantity, unitPrice, lineTotal }], declinedReason, createdAt, cancellableUntil, updatedAt, statusToken }

GET /bar/orders/:id

Read an order back

The order as the bar sees it now. Needs the statusToken that came back when the order was placed; without it, or with the wrong one, the order does not exist as far as the caller is concerned. total is the accepted total once the bar has accepted, so it can differ from what was quoted if the menu changed in between.

Keyspk_sk_Scopebar

Parameters

idstring (path)required
The order id.
tokenstringrequired
The statusToken from the create response.

Returns{ id, reference, status: 'received' | 'accepted' | 'preparing' | 'ready' | 'delivered' | 'declined' | 'cancelled', service, tableLabel, total, items[], declinedReason, createdAt, cancellableUntil, updatedAt }

POST /bar/orders/:id/cancel

Let the guest cancel their order

Cancels an order the guest changed their mind about. Allowed for two minutes after it was placed - the read shape carries the deadline as cancellableUntil, so show a countdown and hide the button when it passes - and never once the bar has delivered it or taken payment for it. If the bar had already accepted the order into a tab, the unpaid tab is discarded and the till is told. Cancelling twice is harmless. Needs the statusToken. Secret key only.

Keyssk_Scopebar

Parameters

idstring (path)required
The order id.

Body

tokenstringrequired
The statusToken from the create response.

ReturnsThe order, as GET /bar/orders/:id, with status: cancelled

GET /bar/orders/:id/stream

Follow an order over server-sent events

A text/event-stream that sends a status event immediately and again every time the bar moves the order on - accepted, preparing, ready, delivered, or declined with the reason on the next read. Open one per live order and close it when the order is delivered or declined. The publishable key and the token go in the query because EventSource cannot set headers; the seat stream uses the same key parameter.

Keyspk_sk_Scopebar

Parameters

idstring (path)required
The order id.
tokenstringrequired
The statusToken from the create response.
keystring (query)required
Your publishable key, in the query because EventSource cannot set headers. Never a secret key.

Returnsevent: status data: { status, updatedAt }

Terminal and JavaScript
# 1. Is the bar taking orders for this event right now? (publishable key is fine)
curl "https://api.venuespine.co.uk/api/public/v1/bar/ordering?eventId=<eventId>" \
  -H "X-API-KEY: pk_live_xxxxxxxxxxxxxxxxxxxxxxxx"

{ "enabled": true, "window": "show", "tableService": true, "note": null, "reason": null, ... }

# ...or follow it live while the ordering screen is open, and stop polling
const ordering = new EventSource(
  `https://api.venuespine.co.uk/api/public/v1/bar/ordering/stream?eventId=<eventId>&key=pk_live_xxx`,
);
ordering.addEventListener('status', (e) => showOrderButton(JSON.parse(e.data).enabled));

# 2. Place the order - SECRET key, server side, with a fresh Idempotency-Key per attempt
curl -X POST "https://api.venuespine.co.uk/api/public/v1/bar/orders" \
  -H "X-API-KEY: sk_live_xxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "Idempotency-Key: 6f1c1d2e-…" \
  -H "Content-Type: application/json" \
  -d '{
    "eventId": "<eventId>",
    "bookingId": "<paid booking id>",
    "service": "table",
    "items": [
      { "itemId": "<menu item id>", "quantity": 2 },
      { "itemId": "<menu item id>", "quantity": 1, "saleUnit": "serving", "note": "no ice" }
    ],
    "notes": "Interval please"
  }'

# 201 - keep statusToken with the order; it is the only way to read it back.
# A replay of the same Idempotency-Key answers 200 with Idempotent-Replay: true and no statusToken.
{ "id": "…", "reference": "BO-4F2A9C", "status": "received", "service": "table",
  "tableLabel": "12", "total": 13, "items": [...], "declinedReason": null,
  "createdAt": "2026-09-16T19:02:11.000Z", "cancellableUntil": "2026-09-16T19:04:11.000Z",
  "updatedAt": "2026-09-16T19:02:11.000Z", "statusToken": "…" }

# 3. Follow it - one EventSource per open order, the publishable key and the token in the query
const source = new EventSource(
  `https://api.venuespine.co.uk/api/public/v1/bar/orders/<id>/stream?token=<statusToken>&key=pk_live_xxx`,
);
source.addEventListener('status', (e) => {
  const { status } = JSON.parse(e.data); // received | accepted | preparing | ready | delivered | declined | cancelled
});

Placing an order needs a secret key, so it happens on your server, with a fresh Idempotency-Key for every attempt and the same key on every retry of it. Keep the statusToken with the order on the device that placed it - it is the only way to read the order back.

Enquiries

Post your own contact form straight into the sales pipeline. Read the form definition first and render it dynamically, and the venue can add a field in VenueSpine without you shipping anything.

GET /enquiry-form

Read the enquiry form definition

Account owners can add their own enquiry fields in VenueSpine. Read the definition and render it, and your form keeps matching theirs without a redeploy.

Keyspk_sk_Scopeenquiries

Returns{ fields: EnquiryField[] }

POST /enquiries

Submit an enquiry

Writes straight into the sales pipeline. Secret key only, so it must be called from your server - never from the browser. Limited to 30 submissions a minute per key. The answer carries a url that opens the enquiry in VenueSpine for a signed-in member with access to the account’s enquiries: use it in your own staff alerts and keep it on your server.

Keyssk_Scopeenquiries

Body

contactNamestring 1-150required
Who is enquiring.
contactEmailstring email
Where the venue replies. Up to 180 characters.
contactPhonestring
A contact number. Up to 40 characters.
companyNamestring
Their company. Up to 150 characters.
eventNamestring
What they are planning. Up to 180 characters.
preferredDatestring YYYY-MM-DD
The date they would like.
alternativeDatestring YYYY-MM-DD
A second date that would also work.
guestCountinteger 1-100000
Expected guests.
budgetMinnumber
Lower end of the budget, in pounds.
budgetMaxnumber
Upper end of the budget, in pounds. Must be at least budgetMin when both are sent, or the answer is a 400 on budgetMax.
messagestring
The enquiry itself. Up to 5 000 characters.
fields{ [key]: string | number | boolean }
Values for the account’s custom fields, keyed by the field key from /enquiry-form. A key the account has not declared is still kept on the enquiry.
eventDetails{ label: string, value: string }[]
Facts about the event in your own words, such as { "label": "Start time", "value": "18:00" }, shown in order on the enquiry’s Event details card and its PDF. Up to 30 entries, labels up to 80 characters and values up to 1 000; an entry with an empty label or value is skipped. Use it for answers VenueSpine has no field for, and keep sending a real date as preferredDate. Each entry shows beside VenueSpine’s own fields, unless it repeats one of them that already has a value, so when you forward what a visitor typed, send only the labels your form uses. Enquiry triage reads the message and fields, not these entries.
marketingOptInboolean
True only when the person ticked your own marketing consent box. Adds them to the account’s Customers as opted in, with this enquiry as the proof.
hpstring
Honeypot. Leave it out of the form a person sees; any filled value is answered with a fake enquiry and nothing is saved. The fake answer looks real, url included, so skip your own staff alert when your form’s hidden field is filled.

Returns{ id: string, reference: string, url: string }

Terminal
# Read the account's enquiry form definition (publishable key is fine)
curl "https://api.venuespine.co.uk/api/public/v1/enquiry-form" \
  -H "X-API-KEY: pk_live_xxxxxxxxxxxxxxxxxxxxxxxx"

# Submit an enquiry - SECRET key, server side only
curl -X POST "https://api.venuespine.co.uk/api/public/v1/enquiries" \
  -H "X-API-KEY: sk_live_xxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "contactName": "Olivia Bennett",
    "contactEmail": "olivia@example.co.uk",
    "contactPhone": "+447700900123",
    "preferredDate": "2027-03-20",
    "guestCount": 120,
    "message": "Do you have a Saturday in March for 120 people?",
    "eventDetails": [
      { "label": "Start time", "value": "18:00" },
      { "label": "Seating layout", "value": "Chairs around tables" }
    ]
  }'

Writing an enquiry needs a secret key, so it must happen on your server. Proxy your browser form through your own backend - never put an sk_ key in a page.

Customers

Your website or app has its own sign-up. Send each person here once and the venue’s CRM, marketing audiences and attendance history see one customer whether they arrived through your site, a booking or an enquiry. Matching, merging, consent and the audit trail are handled on this side - you make one call per signup or profile change.

POST /customers

Create or link a customer

Mirror a person from your own sign-up into the account’s Customers. VenueSpine matches by your id first, then by email, then by phone digits, so a later email change still finds the same customer and a repeat call changes nothing. Matching is scoped to the key’s account: a record another account holds is never matched or named, and a conflict with one answers 409 CUSTOMER_UNAVAILABLE. Secret key only, called from your server once you have verified the person. Limited to 120 calls a minute per key.

Keyssk_Scopecustomers

Body

sourcestringrequired
A short name for your system ([a-z0-9._-], 2-60 chars). Scopes externalRef.
externalRefstringrequired
Your own id for the person. Never reused for anyone else.
firstNamestringrequired
Given name.
lastNamestring
Family name.
emailstring email
One of email or phone is required so the record can be matched to bookings.
phonestring
Matched on digits only.
consent{ optIn: boolean, proof?: string }
true subscribes the person to marketing with your proof text; false unsubscribes; omitted leaves consent alone.
sourceLabelstring
Shown as "created by" on a customer this call creates.

Returns{ customerId: string, reference: string, created: boolean, matchedBy: "externalRef" | "email" | "phone" | null }

Terminal
# Mirror a signup - SECRET key, server side only, after you have verified the person
curl -X POST "https://api.venuespine.co.uk/api/public/v1/customers" \
  -H "X-API-KEY: sk_live_xxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "source": "my-website",
    "externalRef": "user-42",
    "firstName": "Jane",
    "lastName": "Doe",
    "email": "jane@example.co.uk",
    "phone": "0821234567",
    "consent": { "optIn": true, "proof": "Newsletter box ticked at signup, 2026-09-02T10:15:00Z" },
    "sourceLabel": "My website"
  }'

# 201 when a customer was created, 200 when an existing one was linked
{ "customerId": "3b2f…", "reference": "CUS-2026-00041", "created": false, "matchedBy": "email" }

Only a secret key with the customers scope is accepted, and only after you have verified the person yourself - an OTP-confirmed signup or a signed-in profile edit. A clash returns a named 409 (CUSTOMER_EXTERNAL_REF_TAKEN or CUSTOMER_ALREADY_LINKED) that tells you which customer already holds the id.

Reservations

A restaurant’s book, open to its own website. Read the services and the times that are open for a party, send the request, and show the guest the floor has confirmed it. The request lands beside the ones taken by phone, capacity is the floor plan the venue drew, and nothing is booked twice.

GET /reservations/services

List the services open to the website

The service windows the account takes web requests for - a lunch, a dinner - with the ISO weekdays they run on (1 is Monday), their hours, slot length, turn, largest party and deposit per head when one is set. A service the account has switched off, or has kept for the phone only, is left out.

Keyspk_sk_Scopereservations

Returns{ data: Service[] }

GET /reservations/availability

Open times for a date and party

Every slot on that date, per service, with whether it is open for a party of that size and how many seats are left. Capacity is the house floor plan: a slot is open when a free table seats the party for the whole turn. capacityKnown is false when the account has not drawn a floor yet, in which case every slot is open. A party above a service’s largest party shows every slot closed. Dates are the venue’s own: a date before its today is refused with RESERVATION_IN_PAST, and on its today a slot whose time has come is closed, with seatsLeft null.

Keyspk_sk_Scopereservations

Parameters

datestring YYYY-MM-DDrequired
The day to check: the venue’s today or later.
partyintegerrequired
How many guests.
servicestring
Restrict to one service.

Returns{ date, partySize, capacityKnown, services: [{ id, name, turnMinutes, maxPartySize, depositPerHead, slots: [{ time, available, seatsLeft }] }] }

POST /reservations

Request a table

Lands in the account’s book as a request and is held for the service’s hold window until someone on the floor confirms it; a request nobody confirms in time is cancelled on its own. The guest becomes a customer on the account, matched by email and phone so a regular is not duplicated, and is emailed that the request is in, then again when the floor confirms or cancels it, so your site need not send its own. A publishable key must send a captchaToken from the Turnstile widget; a secret key from your server needs none. Three open requests per email at a time.

Keyspk_sk_Scopereservations

Body

serviceIdstringrequired
A service from GET /reservations/services.
datestring YYYY-MM-DDrequired
A day the service runs on: the venue’s today or later.
timestring HH:MMrequired
A slot the availability call marked open; on the venue’s today, one still to come.
partySizeintegerrequired
How many guests, up to the service’s largest party.
namestringrequired
Who the table is for.
emailstring emailrequired
Where the confirmation goes.
phonestring
A number the floor can reach them on.
notesstring
What the guest asked for - an occasion, a window seat, a high chair.
marketingOptInboolean
true subscribes the guest to marketing with the request as proof.
captchaTokenstring
Required with a publishable key: the token from the Turnstile widget.
hpstring
Honeypot. Leave it out of the form a person sees; a filled value is answered with a fake reference and nothing is booked.

Returns{ id, reference, status: "requested", date, time, partySize, serviceName, heldUntil }

GET /reservations/:reference

Read a request by reference

The status of one request - requested, confirmed, seated, completed, no_show or cancelled - so your page can tell the guest the floor has confirmed it. Only the account the key belongs to can see its references.

Keyspk_sk_Scopereservations

Parameters

referencestring (path)required
The RSV- reference from the request.

Returns{ id, reference, status, date, time, partySize, serviceName, heldUntil }

Request
# 1. What can we offer a party of four on the 13th?
GET https://api.venuespine.co.uk/api/public/v1/reservations/availability?date=2027-03-13&party=4
X-API-KEY: pk_...

# 2. Request the 19:30 slot (browser: include the Turnstile token)
POST https://api.venuespine.co.uk/api/public/v1/reservations
X-API-KEY: pk_...
Content-Type: application/json

{
  "serviceId": "svc_dinner",
  "date": "2027-03-13",
  "time": "19:30",
  "partySize": 4,
  "name": "Jane Doe",
  "email": "jane@example.com",
  "phone": "0821234567",
  "notes": "Birthday",
  "captchaToken": "<turnstile token>"
}

# 201 -> { "reference": "RSV-2027-0042", "status": "requested", "heldUntil": "2027-02-27T13:00:00Z", ... }

# 3. Has the floor confirmed it yet?
GET https://api.venuespine.co.uk/api/public/v1/reservations/RSV-2027-0042
X-API-KEY: pk_...

A request from a browser with a publishable key must carry a captchaToken from the Turnstile widget; a request from your server with a secret key needs none. A request nobody on the floor confirms within the service’s hold window is cancelled on its own, so poll GET /reservations/:reference and tell the guest to call if it comes back cancelled.

Rooms

A property’s rooms, open to its own website. Ask which room types are free for a set of nights and what they cost, book the stay, and show the guest it is on the tape chart. The booking lands beside the ones the desk takes, priced from the same rate plans, and a room type is never oversold.

GET /rooms/availability

Room types free for a set of nights

Every room type on sale that sleeps the party, with how many rooms are free for those nights, the nightly rate for each night from the account’s rate plans (the base rate where no plan covers a night), the total and the average. A type with nothing free is still listed with free: 0 so your page can say so.

Keyspk_sk_Scoperooms

Parameters

checkInstring YYYY-MM-DDrequired
The first night: the property’s today or later.
checkOutstring YYYY-MM-DDrequired
The morning the guest leaves; after checkIn and at most 365 nights on.
adultsinteger
How many adults (default 2).
childreninteger
How many children (default 0). Adults plus children must fit what the type sleeps.

Returns{ checkIn, checkOut, nights, guests, roomTypes: [{ id, name, description, sleeps, capacity, free, nights, nightly: number[], total, averageRate }] }

POST /stays

Book a room type for the nights

Lands on the account’s tape chart as reserved, priced from the rate plans at the moment of booking, with a STY- reference. The desk assigns the room at check-in. The guest becomes a customer on the account, matched by email and phone so a regular is not duplicated, and is emailed the booking (and later any cancellation and the check-out summary), so your site need not send its own. A publishable key must send a captchaToken from the Turnstile widget; a secret key from your server needs none. Three open stays per email at a time.

Keyspk_sk_Scoperooms

Body

roomTypeIdstringrequired
A room type from GET /rooms/availability with rooms free.
checkInstring YYYY-MM-DDrequired
The first night: the property’s today or later.
checkOutstring YYYY-MM-DDrequired
The morning the guest leaves, at most 365 nights after checkIn.
adultsinteger
How many adults (default 2).
childreninteger
How many children (default 0).
namestringrequired
Who is staying.
emailstring emailrequired
Where the confirmation goes.
phonestring
A number the desk can reach them on.
notesstring
What the guest asked for - a cot, a late arrival, a quiet room.
marketingOptInboolean
true subscribes the guest to marketing with the booking as proof.
captchaTokenstring
Required with a publishable key: the token from the Turnstile widget.
hpstring
Honeypot. Leave it out of the form a person sees; a filled value is answered with a fake reference and nothing is booked.

Returns{ id, reference, status: "reserved", checkIn, checkOut, nights, roomTypeName, guestName, total }

GET /stays/:reference

Read a stay by reference

The status of one stay - reserved, checked_in, checked_out, no_show or cancelled - with its nights and total. Only the account the key belongs to can see its references.

Keyspk_sk_Scoperooms

Parameters

referencestring (path)required
The STY- reference from the booking.

Returns{ id, reference, status, checkIn, checkOut, nights, roomTypeName, guestName, total }

Request
# 1. What is free for two adults over the 20th to the 23rd?
GET https://api.venuespine.co.uk/api/public/v1/rooms/availability?checkIn=2027-03-20&checkOut=2027-03-23&adults=2
X-API-KEY: pk_...

# 200 -> { "nights": 3, "roomTypes": [{ "id": "rt_garden", "name": "Garden room", "free": 2, "total": 450, ... }] }

# 2. Book the garden room (browser: include the Turnstile token)
POST https://api.venuespine.co.uk/api/public/v1/stays
X-API-KEY: pk_...
Content-Type: application/json

{
  "roomTypeId": "rt_garden",
  "checkIn": "2027-03-20",
  "checkOut": "2027-03-23",
  "adults": 2,
  "name": "Jane Doe",
  "email": "jane@example.com",
  "phone": "0821234567",
  "notes": "Arriving after 20:00",
  "captchaToken": "<turnstile token>"
}

# 201 -> { "reference": "STY-2027-0042", "status": "reserved", "nights": 3, "total": 450, ... }

# 3. Has the desk checked them in yet?
GET https://api.venuespine.co.uk/api/public/v1/stays/STY-2027-0042
X-API-KEY: pk_...

A booking from a browser with a publishable key must carry a captchaToken from the Turnstile widget; a booking from your server with a secret key needs none. The stay is reserved the moment the call returns - there is no hold to confirm - so show the reference and let the guest know the desk will be in touch.

Seat-map embed

Rendering a floor plan, holding a seat while someone types their card details and releasing it when they wander off is where booking integrations go wrong. That part is ours. Drop in one custom element and it handles the map, the live availability, the buyer details and the handoff to payment.

HTML
<script src="https://api.venuespine.co.uk/embed/seat-map.js"></script>

<venuespine-seat-map
  publishable-key="pk_live_xxxxxxxxxxxxxxxxxxxxxxxx"
  event-id="8f2c14e0-3a9b-4e77-9c11-2b6f0d5a7e31"
  success-url="https://yoursite.com/book/complete">
</venuespine-seat-map>

Attributes

publishable-keyrequired
The account’s pk_ key. Browser-safe by design.
event-idrequired
The event to sell.
api-base
Point the widget at a non-production API while you are testing. Default https://api.venuespine.co.uk/api
success-url
Where the buyer returns after paying. Default current page
cancel-url
Where the buyer returns if they cancel. Default success-url
failure-url
Where the buyer returns if payment fails. Default success-url
show-prices
Show a price (or "from" price) caption under every bookable table. Default false
buyer-first-name, buyer-last-name, buyer-email, buyer-phone
Prefill the buyer fields for a host that already knows the buyer, such as a signed-in guest. The fields stay editable.
buyer-locked
With the four buyer attributes present, show the buyer as fixed text instead of fields, so the ticket always goes to the signed-in account. Default false
promo-code
Prefill the promo code box, for a code that arrived with a link. The buyer still presses Apply. The box only shows when the event has a live code.

DOM events

seat-selected
Fires whenever the buyer changes their selection. Payload detail.selected: string[]
booking-created
Fires once the booking exists, just before the redirect to payment. Payload detail: { bookingId, redirectUrl, … }
promo-changed
Fires when the buyer applies or removes a promo code. code is null once removed. Payload detail: { code, discount, subtotal }
es-error
A human-readable failure you can surface in your own styling. Payload detail.message: string
JavaScript
const widget = document.querySelector('venuespine-seat-map');

widget.addEventListener('seat-selected', (event) => {
  console.log('chosen seats', event.detail.selected);
});

widget.addEventListener('booking-created', (event) => {
  console.log('booking', event.detail.bookingId, event.detail.redirectUrl);
});

widget.addEventListener('es-error', (event) => {
  showBanner(event.detail.message);
});

Theming

CSS
/* Shadow DOM keeps the widget isolated. Theme it with custom properties.
   The defaults are a dark, stage-lit look; a light host overrides the surfaces and the ground. */
venuespine-seat-map {
  --es-font: 'Inter', sans-serif;
  --es-brand: #c8102e;              /* stage wash, focus ring, CTA gradient */
  --es-brand-deep: #8f0b20;
  --es-brand-bright: #ff5c73;
  --es-ink: #1f2937;  --es-muted-ink: #6b7280;  --es-faint-ink: #9ca3af;
  --es-surface: #ffffff;  --es-surface-2: #f3f4f6;  --es-surface-3: #e5e7eb;
  --es-border: #e5e7eb;  --es-hover: rgba(0,0,0,.06);
  --es-input-bg: #ffffff;  --es-input-ink: #1f2937;
  --es-ground-top: #f8fafc;  --es-ground-bottom: #e2e8f0;  --es-grid: rgba(0,0,0,.05);
  --es-available-1: #ffffff;  --es-available-2: #f1f5f9;  --es-available-3: #cbd5e1;
  --es-available-ink: #1f2937;  --es-available-stroke: #94a3b8;
  --es-decor: rgba(0,0,0,.04);  --es-decor-stroke: rgba(0,0,0,.12);  --es-decor-text: #64748b;
}

/* Also: --es-font-display, --es-radius, --es-radius-lg, --es-btn-radius, --es-btn-bg, --es-btn-ink,
   --es-focus, --es-danger, --es-menu-bg, --es-cs-sel-bg, --es-cs-sel-ink, --es-gold, --es-gold-bright,
   --es-tint-gold, --es-hatch, --es-selected-1/2/3, --es-selected-ink, --es-selected-stroke,
   --es-held-1/2, --es-sold-1/2, --es-sold-ink, --es-sold-stroke, --es-stage-stroke, --es-bar-fill,
   --es-bar-stroke, --es-dance-fill, --es-dance-stroke, --es-line-fill, --es-badge-fill. */

/* The map, the legend and the panel are exposed as parts. */
venuespine-seat-map::part(map) {
  border-radius: 16px;
}
venuespine-seat-map::part(panel) {
  margin-top: 24px;
}

Before it will render

  • The account has Events with Ticketing switched on, and a publishable key issued
  • A seating plan is linked to the event and set to Reserved
  • The bookable seats are priced - a tier, a per-seat price, or the plan default
  • The event is published to web and is not draft or cancelled
  • The event sells its own tickets - one set to sell on Ticketmaster shows a link out instead of a map
  • The account has Stripe connected, for anything that is not free

Errors

Every failure comes back with the same shape and a stable machine-readable code. Branch on the code, show the message.

JSON
// Every failure has the same shape.
{
  "error": {
    "code": "API_KEY_FORBIDDEN",
    "message": "This API key cannot be used for this request."
  }
}
BAD_REQUEST400
The body or query failed validation. Read message - it names the field. Check required parameters and value ranges.
EVENT_SOLD_EXTERNALLY400
The event sells its tickets somewhere else, so VenueSpine will not take the booking. Read ticketSales on the event and send the buyer to externalTicketUrl instead of opening your checkout.
API_KEY_MISSING401
No key on the request. Send X-API-KEY, or Authorization: Bearer.
API_KEY_INVALID401
The key does not exist or was revoked. Issue a new key in VenueSpine under the account’s API keys.
API_KEY_INACTIVE401
The key is fine, but the account or organisation that owns it is no longer active. Nothing on your side. The tenant has been suspended or archived in VenueSpine.
CAPTCHA_REQUIRED400
A booking from a publishable key arrived without a valid Turnstile token. Render the Turnstile widget and send its token as captchaToken, or call the endpoint from your server with a secret key.
RETURN_URL_NOT_ALLOWED400
A success, cancel or failure URL points at an origin the key does not allow. Use https, and add your site’s origin under Return origins on the key in VenueSpine.
PROMO_CODE_INVALID400
The promo code does not exist for this event, or it was paused or deleted. Show message next to the code box and let the buyer try again or remove it.
PROMO_CODE_NOT_STARTED400
The code is valid but its window has not opened yet. Show message. The venue sets when a code starts.
PROMO_CODE_EXPIRED400
The code’s window has closed. Show message and book without the code.
PROMO_CODE_EXHAUSTED409
Every ticket the code could discount has been booked. Show message and book without the code. Tickets come back to a code when a checkout lapses or a booking is refunded.
PROMO_CODE_NOT_APPLICABLE400
None of the chosen tickets qualify for the code, for example it covers other ticket types, or only bundles were chosen. Show message so the buyer can change their tickets or remove the code.
BOOKING_HOLD_LIMIT429
This address already holds three open bookings on the event. Let a hold lapse or complete one before starting another. It is per buyer, not per key.
API_KEY_FORBIDDEN403
Wrong key type or wrong scope for this route. Enquiry, customer and ticket-code reads and writes need a secret key. A key scoped to events cannot read the catalogue or the bar.
MODULE_NOT_IN_PLAN403
The account behind the key does not have the feature this route belongs to. Events, bookings and the seat map need Events with Ticketing switched on for that account, on its plan and on its Modules page.
NOT_FOUND404
No such record, or it is not published. Confirm the id, and that the event is published to web.
BOOKING_NOT_FOUND404
No booking with that id on the account behind your key. A booking is only visible through the key of the account it was made on.
IDEMPOTENCY_KEY_REQUIRED400
POST /bar/orders was sent without an Idempotency-Key header. Mint a UUID per order attempt, send it on every retry of that attempt, and mint a new one for a new order.
IDEMPOTENCY_KEY_REUSED422
That Idempotency-Key was already used for a different order body. A key belongs to one order. Retries must send the identical body; a new order needs a new key.
BAR_ORDERING_CLOSED409
The bar is not taking guest orders for this event right now, or not to tables. Check GET /bar/ordering first and show its reason: the venue switched it off, the show window has not opened, no till is trading, or the bar has not opened ordering yet. service: table is refused when tableService is false.
BOOKING_NOT_AT_EVENT409
The booking is not a paid booking on this event through your key. Only a guest with a paid booking on the event can order. Confirm the booking status and that eventId matches.
ITEM_OFF_SALE409
An item on the order is not on sale right now. Refresh GET /bar/menu and let the guest pick again.
UNKNOWN_ITEM400
An itemId is not on this account’s menu. Item ids come from GET /bar/menu on the same key.
SERVING_NOT_SOLD400
saleUnit was serving for an item that is only sold by the unit. Offer the serving option only where GET /bar/menu shows a serving price for the item.
GUEST_ORDER_NOT_FOUND404
No order with that id and token on the account behind your key. The token that came back on create is the only way to read an order; a wrong token reads as no order, never as forbidden.
GUEST_ORDER_STATUS409
The order is in a state the action does not apply to, for example cancelling one the bar already declined. Refresh the order and show its status.
GUEST_ORDER_LIMIT429
The booking already has three orders waiting at the bar. Wait for one to be accepted or delivered before letting the guest order again.
GUEST_ORDER_CANCEL_WINDOW409
More than two minutes have passed since the order was placed. Hide the cancel button once cancellableUntil passes; after that the guest asks at the bar.
GUEST_ORDER_DELIVERED409
The order has already been delivered, so there is nothing to cancel. Refresh the order and show its status.
GUEST_ORDER_PAID409
The bar has already taken payment for this order. Nothing to do from the app; the bar can void the receipt if it was a mistake.
SSE_LIMIT429
Too many open order streams from one address. Close streams for orders that are delivered or declined, and the ordering stream when the screen closes. Eight per address are allowed.
CONFLICT409
Someone took the seat while your buyer was typing. Re-read /seats, repaint, and ask the buyer to choose again.
RESERVATION_NO_CAPACITY409
The floor is full at that time for a party of that size. Re-read /reservations/availability and offer the slots still marked open.
RESERVATION_OUTSIDE_SERVICE400
The time or the day is outside the service window, or past its last seating. Only offer the slots the availability call returns for that date.
RESERVATION_PARTY_TOO_LARGE400
The party is bigger than the service takes. Show the service’s maxPartySize and ask larger parties to call.
RESERVATION_IN_PAST400
date is before today where the venue is, on the availability call or the request, or the request is for a time today that has already come. Start your date picker on the venue’s today, offer only the slots the availability call still marks open, and read it again if the guest takes a while to choose.
RESERVATION_LIMIT429
That email already has three requests waiting or confirmed. Ask the guest to call to change an existing booking rather than request another.
STAY_NO_AVAILABILITY409
Every room of that type is taken on one of the nights. Re-read /rooms/availability and offer the types still showing rooms free.
STAY_PARTY_TOO_LARGE400
Adults plus children is more than the room type sleeps. Show sleeps from the availability call and offer a bigger type.
STAY_TOO_LONG400
The stay runs for more than 365 nights, on the availability call or the booking. Keep checkOut within 365 nights of checkIn, and ask for a longer stay to be booked with the property.
STAY_IN_PAST400
checkIn is before today where the property is, on the availability call or the booking. Start your date picker on the property’s today, and ask a guest who has already arrived to book with the desk.
STAY_LIMIT429
That email already has three stays reserved. Ask the guest to call to change an existing stay rather than book another.
RESERVATION_BUSY409
The floor is being booked at that moment and the lock could not be taken. Retry once after a short pause.
RATE_LIMITED429
Too many requests for this key. Back off for the seconds given in the Retry-After header.

Rate limits

Limits are keyed by your API key, not by the caller’s IP address, so a hundred buyers behind one office connection do not share a budget. A limited request returns 429 with a Retry-After header - honour it rather than retrying immediately.

Public API, everything else
Generous, tuned per deploymentKeyed by Your API key once it has been checked; the caller’s address before that, so a guessed key cannot spend your budget
POST /enquiries
30 requests per minuteKeyed by Your API key
POST /customers
120 requests per minuteKeyed by Your API key
POST /reservations
10 requests per minute, and three open requests per emailKeyed by The caller’s address, then the guest’s email
POST /bookings and POST /bookings/quote with a promoCode, from a publishable key
40 requests per 10 minutes, counted across bothKeyed by The caller’s address, so codes cannot be guessed by trying them one after another. Secret-key calls are not counted here; the per-key limit covers them
SSE seat stream
One connection per buyerKeyed by Your API key
POST /bar/orders
Three orders waiting per bookingKeyed by The booking, so one guest cannot flood the bar
SSE ordering and order streams
Eight open streamsKeyed by The caller’s address

Questions

No. It is a custom element. Drop the script tag and the element onto any page - static HTML, WordPress, Webflow, React, whatever you already run - and pass it three attributes. It renders in Shadow DOM, so it cannot collide with your styles.

In your Stripe account, connected per VenueSpine account. Checkout is created server-side, your Stripe secret never reaches the browser, and the buyer returns to a URL you control. VenueSpine does not sit between you and your money.

Fifteen minutes from the moment the booking is created. If the buyer abandons checkout the hold lapses and the seat returns to the pool, and every open seat map sees that happen over the stream.

Treat it as a hint, not a fact. It tells you the buyer came back to your page, not that the payment settled. Always confirm with GET /bookings/:id and act on the status field.

Because the browser’s EventSource API cannot set headers. It is safe here because a publishable key is read-only and designed to be visible. Never put a secret key in a URL.

Pounds, as a number with at most two decimals: a £22.00 seat is 22 and a £4.50 drink is 4.5, on events, tickets, bundles, seats, the catalogue, the bar menu, bookings and bar orders. Every amount is priced by the API from the venue’s own records; nothing you send carries money except an enquiry’s budgetMin and budgetMax, which are pounds too. Format for display on your side, and round to cents before you compare two amounts.

No. POST /bar/orders sends the request to the till; the bar accepts it into a tab at the guest’s table and takes payment the way it always does, card or cash at the table or the bar. Your app never touches money for bar orders, and a declined order costs nothing.

The order id is guessable and the read route accepts a publishable key, so the token is what proves the caller placed the order. Keep it with the order on the device that placed it. Lose it and the order is still on the bar’s side; your app simply cannot read it back.

The contract is additive: new parameters are optional and new routes are additive, so existing calls keep working. If something genuinely has to change we will tell you before it does.

Yes. The API covers listings, server-side filtering, pagination, event detail and the whole booking flow, so a venue’s public site can be built entirely on it, with VenueSpine owning the data.

For developers

Want a key? Tell us what you are building.

Publishable keys come with every account. Tell us what you are building and we will point you at the right endpoints.

Book a demo