Skip to content
Last updated

Availability Feed

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.


When to use the feed vs. GET /availability

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 caseRight tool
Search results with available datesFeed
Calendar / date-picker UIFeed
"Next available slot" across a set of venuesFeed
Displaying available party sizes for a dateFeed
Real-time confirmation immediately before bookingGET /venues/{compositeId}/availability

Finding the feed URL

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.


Downloading the feed

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);
}

Feed format

The decompressed file is a JSON object with metadata at the top level and a slots array.

Top-level fields

{
  "product_id": "1|CO|512b201dd5d190d2978ca231|5d2d312913b85008ae110b75",
  "product_name": "Venue Hire",
  "venue_name": "The Grand Hall",
  "slots": [...]
}
FieldDescription
product_idThe product's compositeId — same value as products[].compositeId in the venue response
product_nameHuman-readable product name
venue_nameHuman-readable venue name
slotsArray of available time slots (see below)

Slot fields

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"
}
FieldTypeDescription
datestringDate in YYYY-MM-DD, in the venue's local timezone
timestringTime in HH:MM:SS, in the venue's local timezone
party_sizeintegerThe party size this slot applies to. The feed contains one entry per party size supported by the product
duration_minutesintegerExpected booking duration
spots_totalintegerTotal capacity for this slot
spots_openintegerRemaining capacity at the time the feed was generated
typestring"book" — instant confirmation; "request" — pending operator approval. Same semantics as GET /availability

Slots with spots_open: 0 are excluded from the feed entirely.


Freshness

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.


Example: venues → products → slots

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}]`
      );
    }
  }
}

Booking from feed data

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