Each bookable product exposes a pre-generated availability feed: a lightweight index file listing up to 180 days of availability, with one downloadable daily shard file per day that has open slots. The feed is checked once daily; a day whose availability hasn't changed can be carried forward unchanged rather than rewritten (see Freshness below). Use it to build discovery interfaces, search widgets, and availability calendars without making live API calls per venue or date.
Because availability is sharded by date, you download only the days you need — a calendar widget showing next week fetches the index plus seven small files, not a 180-day payload.
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 static files; 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's index 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/index.json"
}]
}]
}feedUrl is null when no feed has been generated yet for a product. Always null-check before attempting a download.
Do not construct index or shard URLs yourself — read feedUrl from the venue response, and read shard URLs from the index. The URL structure is internal and may change.
The feed for a product consists of two file types:
- Index file (
index.json) — plain, uncompressed JSON. Lists every day in the 180-day window that has at least one open slot, with a direct download URL for each day's shard file. - Daily shard files (
{date}.json.gz) — gzip-compressed JSON. One file per day, containing every open slot for that date.
Days with no availability have no shard file and are omitted from the index. An index with an empty shards array means the product has no availability in the entire window.
Dates the product can't be booked for yet (or anymore) are also missing — see Advance-booking windows.
{
"product_id": "29|CO|275cc44dd2e2496fba44857c9257443a|5c4af02d6354a83e3a0ea3b4",
"product_name": "Dinner",
"venue_name": "Dishoom Covent Garden",
"shards": [
{ "date": "2026-07-09", "url": "https://feeds.bookabletech.com/29/275cc44dd2e2496fba44857c9257443a/5c4af02d6354a83e3a0ea3b4/2026-07-09.json.gz" },
{ "date": "2026-07-10", "url": "https://feeds.bookabletech.com/29/275cc44dd2e2496fba44857c9257443a/5c4af02d6354a83e3a0ea3b4/2026-07-10.json.gz" }
],
"config_hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"activity_dates": ["2026-07-09", "2026-07-12"],
"advance_bookings_min_days": 7,
"advance_bookings_max_days": 365,
"active": true,
"shut_at": null
}| 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 |
shards | One entry per day with availability. Sorted by date ascending |
config_hash, activity_dates | Internal bookkeeping for incremental refresh (detecting when a date's shard needs regenerating) — not meaningful to feed consumers. Both nullable/absent on older indexes. Safe to ignore |
advance_bookings_min_days | Earliest bookable date = today + this many days. Missing if there is no minimum |
advance_bookings_max_days | Latest bookable date = today + this many days. Missing if there is no maximum |
active | false = the venue is closed and shards is empty. If missing, treat as true |
shut_at | Closing date. null if the venue is open or the date is unknown |
shards[].date | Date in YYYY-MM-DD, in the venue's local timezone |
shards[].url | Direct download URL for that day's shard file |
Some products can only be booked a certain number of days ahead — for example "at least 7 days ahead" or "at most 1 year ahead". Dates outside this range are not in the feed, just like dates with no availability.
Use advance_bookings_min_days and advance_bookings_max_days to tell the guest why a date is missing ("book at least 7 days ahead") instead of showing it as sold out. Days are counted from today, in the venue's timezone.
The range moves every day, so a date can disappear from the index. Re-read index.json every day.
Booking a date outside the range fails with AVAIL-N-007 (too soon) or AVAIL-N-008 (too far ahead) — see the error catalog.
An operator can set booking rules that apply only to some partners (see GET /booking-rules). When a rule applies to you on a product, that product's feedUrl points to an index generated for your channel, and its shards already reflect the rule:
booking_window(book): slots inside the minimum notice, or past the horizon, are left out. The window is in hours, so a date can lose only its earliest slots. Because a shard is used for up to a day, a slot is also left out if it would fall inside the minimum notice before the next refresh.route_to_enquiry: matching slots are published astype: "request"withrequest_reason: "operator_rule". Your enquiry policy never removes these — the operator wants them as enquiries.
You don't need to do anything to get this: keep reading feedUrl from the venue response rather than building the URL yourself.
When a venue closes, its feed is emptied. Its index.json stays, with active: false, shut_at (closing date, or null if unknown) and an empty shards array.
{
"product_id": "29|CO|275cc44dd2e2496fba44857c9257443a|5c4af02d6354a83e3a0ea3b4",
"product_name": "Dinner",
"venue_name": "Dishoom Covent Garden",
"shards": [],
"config_hash": null,
"activity_dates": null,
"active": false,
"shut_at": "2026-09-30T12:00:00Z"
}When you see active: false, stop offering the venue. Bookings for it fail with VENUE-N-009 (see the error catalog). If you have a webhook, you also get a venue.updated event.
If the venue re-opens, the feed comes back.
The decompressed shard is a JSON object with the product ID, the date, and a slots array.
{
"product_id": "29|CO|275cc44dd2e2496fba44857c9257443a|5c4af02d6354a83e3a0ea3b4",
"date": "2026-07-09",
"slots": [
{
"merchant_id": "29|CO|275cc44dd2e2496fba44857c9257443a",
"service_id": "29|CO|275cc44dd2e2496fba44857c9257443a|5c4af02d6354a83e3a0ea3b4",
"date": "2026-07-09",
"time": "18:00:00",
"date_time": "2026-07-09T18:00:00+01:00",
"party_size": 4,
"duration_minutes": 90,
"min_duration": 60,
"max_duration": 120,
"duration_interval": 15,
"spots_total": 116,
"spots_open": 32,
"type": "book",
"deposit": {
"amount": 5.00,
"currency_code": "GBP",
"amount_per": "guest"
}
},
{
"merchant_id": "29|CO|275cc44dd2e2496fba44857c9257443a",
"service_id": "29|CO|275cc44dd2e2496fba44857c9257443a|5c4af02d6354a83e3a0ea3b4",
"date": "2026-07-09",
"time": "20:00:00",
"date_time": "2026-07-09T20:00:00+01:00",
"party_size": 8,
"duration_minutes": 90,
"min_duration": 60,
"max_duration": 120,
"duration_interval": 15,
"spots_total": 116,
"spots_open": 8,
"type": "request",
"request_reason": "exceeds_auto_confirm"
},
{
"merchant_id": "29|CO|275cc44dd2e2496fba44857c9257443a",
"service_id": "29|CO|275cc44dd2e2496fba44857c9257443a|5c4af02d6354a83e3a0ea3b4",
"date": "2026-07-09",
"time": "00:30:00",
"date_time": "2026-07-10T00:30:00+01:00",
"party_size": 4,
"duration_minutes": 90,
"min_duration": 90,
"max_duration": 90,
"spots_total": 40,
"spots_open": 12,
"type": "book"
}
]
}The last slot is in the evening's after-midnight tail: it sits in the 9 July shard, but date_time shows it starts at 00:30 on 10 July.
| Field | Type | Description |
|---|---|---|
merchant_id | string | The venue id (same as the venue id in GET /venues) |
service_id | string | The product compositeId — same as the shard's product_id |
date | string | YYYY-MM-DD — the venue's trading day, not always the calendar date. A Collins evening that runs past midnight lists its after-midnight starts under that evening's date |
time | string | HH:MM:SS, venue local time. After midnight on a trading day, it belongs to the next calendar morning |
date_time | string, omitted when absent | The real moment the slot starts, venue-local with its UTC offset (2026-07-10T00:30:00+01:00). Use it to show the slot, and send it unchanged as dateTime when booking. Sent on Collins slots. When it is missing — other systems, or a Collins shard written before 2026-10-16 and not regenerated yet — use date + time: they are the real moment, except for after-midnight times on an old Collins shard |
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 |
min_duration | integer, nullable | Minimum selectable duration (minutes). Equal to duration_minutes for products with no duration-range concept |
max_duration | integer, nullable | Maximum selectable duration (minutes). Equal to duration_minutes for products with no duration-range concept |
duration_interval | integer, nullable | Step size (minutes) between min_duration and max_duration. null when the product has no such concept |
spots_total | integer | Total capacity for this slot |
spots_open | integer | Remaining capacity at the time the feed was generated |
type | string | "book" — the system has confirmed this slot can be auto-confirmed; "request" — requires manual operator approval. Same semantics as GET /availability. When creating the booking, request is always a valid choice, even for a slot that returned book |
request_reason | string, omitted when absent | Why type is "request". Known values: fully_booked, exceeds_auto_confirm, request_only, operator_rule (an operator route_to_enquiry rule — see Operator rules for your channel). New values may be introduced in future API versions — handle unknown values gracefully. Always omitted when type is "book" |
deposit | object, omitted when absent | The product's conditional deposit rule matched against this slot's date, time, party size, and duration — same rule schedule as paymentRules / ConditionalDepositRule elsewhere in the API. Omitted entirely when the product has no deposit rules, or none match this slot |
deposit.amount | number | Deposit amount for this slot |
deposit.currency_code | string | ISO 4217 currency code for deposit.amount |
deposit.amount_per | string | Whether amount applies once per booking (booking), is multiplied by total party size (guest), or is multiplied by adult/child count separately (adult/child) |
room_id | string, omitted when absent | The room this slot is for, when the product has several (see Per-area rooms). Treat as opaque |
room_name | string, omitted when absent | Display name of that room |
space_id | integer, omitted when absent | Per-area Zonal rooms only: the space to book. Send it as spaceId on POST /venues/{compositeId}/booking |
duration_requirement | string, omitted when absent | DO_NOT_SHOW_DURATION or MUST_SHOW_DURATION — whether to show the duration to the guest |
Slots with spots_open: 0 are excluded from the feed entirely.
A Zonal product with two or more areas gets one slot per area for each time and party size, instead of one slot for the whole product. Each of these slots carries:
room_id— opaque, unique per arearoom_name— the area's name (e.g. "Terrace")space_id— the space to book
Each area's party sizes follow its own minimum and maximum covers. To book the slot in that area, send space_id as spaceId. A space that isn't offered by the product fails with 422 BOOK-N-017.
{
"date": "2026-07-09",
"time": "19:00:00",
"party_size": 6,
"duration_minutes": 120,
"spots_total": 10,
"spots_open": 4,
"type": "book",
"room_id": "29|ZO|8f2c…|41d7…|1182",
"room_name": "Terrace",
"space_id": 1182
}merchant_id, service_id and the duration fields are left out of this example for brevity.
A generated slot whose local date/time falls in a DST spring-forward gap (a local time that never occurs, e.g. 01:30 on the night clocks jump from 01:00 to 02:00) is dropped from the feed rather than causing generation to fail.
No authentication is required for either file type — download directly from the URLs. The index is plain JSON; shard files are gzip-compressed JSON.
import { gunzipSync } from 'zlib';
async function fetchIndex(feedUrl) {
const response = await fetch(feedUrl);
return response.json();
}
async function fetchShard(shardUrl) {
const response = await fetch(shardUrl);
const buffer = Buffer.from(await response.arrayBuffer());
return JSON.parse(gunzipSync(buffer).toString('utf-8'));
}The feed covers a rolling 180-day window from the current date and is checked once daily. Shard URLs for a given date are stable across regenerations. A date's shard is only rewritten when it could have changed — a new/cancelled booking, or a change to any slot-affecting operator/venue configuration; otherwise the previous day's content is carried forward unchanged. To bound how stale an unchanged date can get, every date is still force-regenerated on a weekly full-sweep day (Monday, by default) regardless of activity. The feed reflects the state of bookings and operator rules as of its last actual regeneration — for a quiet date, that can be a few days old, not just the last 24 hours — it is not a live snapshot.
Always fetch the index first, on every refresh cycle. Do not cache shard URLs across days: as the window rolls forward, past dates disappear from the index and new dates appear, and a day that sells out is dropped from the index entirely.
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 downloads the index and prints every available slot from each daily shard.
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 fetchIndex(feedUrl) {
const res = await fetch(feedUrl);
return res.json();
}
async function fetchShard(shardUrl) {
const res = await fetch(shardUrl);
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 index = await fetchIndex(product.feedUrl);
for (const { date, url } of index.shards) {
const shard = await fetchShard(url);
for (const slot of shard.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.
From the feed slot, send:
date_timeasdateTime(ordate+timeunchanged whendate_timeis missing)space_idasspaceId, for a per-area room
Feed (discovery) → GET /venues/{compositeId}/availability (real-time check) → POST /venues/{compositeId}/bookingPrior to BookingApi v7.3.0, feedUrl pointed to a single gzip-compressed file ({productId}.json.gz) containing all 90 days of slots. That format is no longer generated. To migrate:
- Re-read
feedUrlfromGET /venues— it now points toindex.json. Do not gunzip it; the index is plain JSON. - Fetch the shard URLs listed in
shards[]for the dates you need. Each shard is gzip-compressed, as the old single file was. - The slot object shape is unchanged. The top level of each shard adds a
datefield, andproduct_name/venue_namenow appear only in the index, not in shard files.