Each bookable product exposes a pre-generated availability feed: a downloadable file containing up to 90 days of time slots, refreshed every 4 hours. Use it to build discovery interfaces, search widgets, and availability calendars without making live API calls per venue or date.
GET /venues/{compositeId}/availability is a real-time endpoint. It calls the underlying TMS to return current availability for a specific product, date, and party size. It is designed to be called once, immediately before creating a booking — not for bulk data loading.
The feed serves the opposite use case. If you need to display availability across many products, dates, or party sizes — a search results page, a calendar view, a "next available" filter — use the feed. It is pre-generated and served as a static file; there are no API rate limits, no per-request latency, and no TMS calls involved.
| Use case | Right tool |
|---|---|
| Search results with available dates | Feed |
| Calendar / date-picker UI | Feed |
| "Next available slot" across a set of venues | Feed |
| Displaying available party sizes for a date | Feed |
| Real-time confirmation immediately before booking | GET /venues/{compositeId}/availability |
The feed URL is available on each product in the GET /venues and GET /venues/{venueId} responses, in the products[].feedUrl field.
{
"data": [{
"name": "Dishoom Covent Garden",
"products": [{
"compositeId": "29|CO|275cc44dd2e2496fba44857c9257443a|5c4af02d6354a83e3a0ea3b4",
"productName": "Dinner",
"feedUrl": "https://feeds.bookabletech.com/29/275cc44dd2e2496fba44857c9257443a/5c4af02d6354a83e3a0ea3b4.json.gz"
}]
}]
}feedUrl is null when no feed has been generated yet for a product. Always null-check before attempting a download.
The file is served as a gzip-compressed JSON document. No authentication is required — download it directly from the URL.
import { gunzipSync } from 'zlib';
async function fetchFeed(feedUrl) {
const response = await fetch(feedUrl);
const buffer = Buffer.from(await response.arrayBuffer());
const json = gunzipSync(buffer).toString('utf-8');
return JSON.parse(json);
}The decompressed file is a JSON object with metadata at the top level and a slots array.
{
"product_id": "1|CO|512b201dd5d190d2978ca231|5d2d312913b85008ae110b75",
"product_name": "Venue Hire",
"venue_name": "The Grand Hall",
"slots": [...]
}| Field | Description |
|---|---|
product_id | The product's compositeId — same value as products[].compositeId in the venue response |
product_name | Human-readable product name |
venue_name | Human-readable venue name |
slots | Array of available time slots (see below) |
Each entry in slots represents one available time for a specific party size:
{
"date": "2026-06-15",
"time": "18:00:00",
"party_size": 4,
"duration_minutes": 90,
"spots_total": 116,
"spots_open": 32,
"type": "book"
}| Field | Type | Description |
|---|---|---|
date | string | Date in YYYY-MM-DD, in the venue's local timezone |
time | string | Time in HH:MM:SS, in the venue's local timezone |
party_size | integer | The party size this slot applies to. The feed contains one entry per party size supported by the product |
duration_minutes | integer | Expected booking duration |
spots_total | integer | Total capacity for this slot |
spots_open | integer | Remaining capacity at the time the feed was generated |
type | string | "book" — instant confirmation; "request" — pending operator approval. Same semantics as GET /availability |
Slots with spots_open: 0 are excluded from the feed entirely.
The feed covers a rolling 90-day window from the current date and is regenerated every 4 hours. It reflects the state of bookings and operator rules as of the last generation cycle — it is not a live snapshot.
This is intentional: the feed is optimised for discovery and display. The exact state at the moment of booking is confirmed by the real-time GET /availability call.
The following example authenticates, pages through all venues, and for each product with a feed prints every available slot.
import { gunzipSync } from 'zlib';
const AUTH_URL = 'https://auth.bookabletech.com/oauth/token';
const API_URL = 'https://api.bookabletech.com';
async function getToken(clientId, clientSecret) {
const res = await fetch(AUTH_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
grant_type: 'client_credentials',
client_id: clientId,
client_secret: clientSecret,
audience: 'api.bookabletech.com',
}),
});
const { access_token } = await res.json();
return access_token;
}
async function fetchFeed(feedUrl) {
const res = await fetch(feedUrl);
const buf = Buffer.from(await res.arrayBuffer());
return JSON.parse(gunzipSync(buf).toString('utf-8'));
}
async function* getVenues(token) {
let page = 1;
while (true) {
const res = await fetch(`${API_URL}/venues?pageNumber=${page}&pageSize=100`, {
headers: { Authorization: `Bearer ${token}` },
});
const { data, meta } = await res.json();
yield* data;
if (page >= meta.totalPages) break;
page++;
}
}
const token = await getToken(process.env.CLIENT_ID, process.env.CLIENT_SECRET);
for await (const venue of getVenues(token)) {
console.log(`\nVenue: ${venue.name}`);
for (const product of venue.products ?? []) {
if (!product.feedUrl) continue;
console.log(` Product: ${product.productName}`);
const feed = await fetchFeed(product.feedUrl);
for (const slot of feed.slots) {
console.log(
` ${slot.date} ${slot.time}` +
` party:${slot.party_size}` +
` open:${slot.spots_open}/${slot.spots_total}` +
` [${slot.type}]`
);
}
}
}When a user selects a slot from your feed-powered UI and proceeds to book, always call GET /venues/{compositeId}/availability in real-time to confirm the slot is still open before submitting the booking request. The type value passed to POST /venues/{compositeId}/booking must come from that live response.
Feed (discovery) → GET /venues/{compositeId}/availability (real-time check) → POST /venues/{compositeId}/booking