Skip to content
Last updated

Availability Feed

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.


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 static files; 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'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.


Feed structure: index + daily shards

The feed for a product consists of two file types:

  1. 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.
  2. 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.

Index format (index.json)

{
  "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
}
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
shardsOne entry per day with availability. Sorted by date ascending
config_hash, activity_datesInternal 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_daysEarliest bookable date = today + this many days. Missing if there is no minimum
advance_bookings_max_daysLatest bookable date = today + this many days. Missing if there is no maximum
activefalse = the venue is closed and shards is empty. If missing, treat as true
shut_atClosing date. null if the venue is open or the date is unknown
shards[].dateDate in YYYY-MM-DD, in the venue's local timezone
shards[].urlDirect download URL for that day's shard file

Advance-booking windows

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.

Operator rules for your channel

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 as type: "request" with request_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.

Closed venues (tombstones)

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.

Daily shard format ({date}.json.gz)

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.

FieldTypeDescription
merchant_idstringThe venue id (same as the venue id in GET /venues)
service_idstringThe product compositeId — same as the shard's product_id
datestringYYYY-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
timestringHH:MM:SS, venue local time. After midnight on a trading day, it belongs to the next calendar morning
date_timestring, omitted when absentThe 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_sizeintegerThe party size this slot applies to. The feed contains one entry per party size supported by the product
duration_minutesintegerExpected booking duration
min_durationinteger, nullableMinimum selectable duration (minutes). Equal to duration_minutes for products with no duration-range concept
max_durationinteger, nullableMaximum selectable duration (minutes). Equal to duration_minutes for products with no duration-range concept
duration_intervalinteger, nullableStep size (minutes) between min_duration and max_duration. null when the product has no such concept
spots_totalintegerTotal capacity for this slot
spots_openintegerRemaining capacity at the time the feed was generated
typestring"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_reasonstring, omitted when absentWhy 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"
depositobject, omitted when absentThe 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.amountnumberDeposit amount for this slot
deposit.currency_codestringISO 4217 currency code for deposit.amount
deposit.amount_perstringWhether amount applies once per booking (booking), is multiplied by total party size (guest), or is multiplied by adult/child count separately (adult/child)
room_idstring, omitted when absentThe room this slot is for, when the product has several (see Per-area rooms). Treat as opaque
room_namestring, omitted when absentDisplay name of that room
space_idinteger, omitted when absentPer-area Zonal rooms only: the space to book. Send it as spaceId on POST /venues/{compositeId}/booking
duration_requirementstring, omitted when absentDO_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.

Per-area rooms (Zonal)

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 area
  • room_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.


Downloading the feed

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

Freshness

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.


Example: venues → products → index → slots

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

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.

From the feed slot, send:

  • date_time as dateTime (or date + time unchanged when date_time is missing)
  • space_id as spaceId, for a per-area room
Feed (discovery)  →  GET /venues/{compositeId}/availability (real-time check)  →  POST /venues/{compositeId}/booking

Migrating from the single-file format

Prior 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:

  1. Re-read feedUrl from GET /venues — it now points to index.json. Do not gunzip it; the index is plain JSON.
  2. Fetch the shard URLs listed in shards[] for the dates you need. Each shard is gzip-compressed, as the old single file was.
  3. The slot object shape is unchanged. The top level of each shard adds a date field, and product_name / venue_name now appear only in the index, not in shard files.