# 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 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` | ## 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. ```json { "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. JavaScript ```javascript 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); } ``` Python ```python import gzip, json, urllib.request def fetch_feed(feed_url: str) -> dict: with urllib.request.urlopen(feed_url) as response: return json.loads(gzip.decompress(response.read())) ``` Java ```java HttpResponse response = HttpClient.newHttpClient() .send(HttpRequest.newBuilder(URI.create(feedUrl)).build(), HttpResponse.BodyHandlers.ofInputStream()); try (var reader = new InputStreamReader(new GZIPInputStream(response.body()))) { Feed feed = new ObjectMapper().readValue(reader, Feed.class); } ``` C# ```csharp using var response = await httpClient.GetStreamAsync(feedUrl); using var gzip = new GZipStream(response, CompressionMode.Decompress); var feed = await JsonSerializer.DeserializeAsync(gzip); ``` ## Feed format The decompressed file is a JSON object with metadata at the top level and a `slots` array. ### Top-level fields ```json { "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) | ### Slot fields Each entry in `slots` represents one available time for a specific party size: ```json { "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. ## 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. JavaScript ```javascript 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}]` ); } } } ``` Python ```python import gzip, json, os, urllib.request AUTH_URL = 'https://auth.bookabletech.com/oauth/token' API_URL = 'https://api.bookabletech.com' def get_token(client_id, client_secret): payload = json.dumps({ 'grant_type': 'client_credentials', 'client_id': client_id, 'client_secret': client_secret, 'audience': 'api.bookabletech.com', }).encode() req = urllib.request.Request( AUTH_URL, data=payload, headers={'Content-Type': 'application/json'} ) with urllib.request.urlopen(req) as r: return json.loads(r.read())['access_token'] def get_venues(token): page = 1 while True: req = urllib.request.Request( f'{API_URL}/venues?pageNumber={page}&pageSize=100', headers={'Authorization': f'Bearer {token}'}, ) with urllib.request.urlopen(req) as r: body = json.loads(r.read()) yield from body['data'] if page >= body['meta']['totalPages']: break page += 1 def fetch_feed(feed_url): with urllib.request.urlopen(feed_url) as r: return json.loads(gzip.decompress(r.read())) token = get_token(os.environ['CLIENT_ID'], os.environ['CLIENT_SECRET']) for venue in get_venues(token): print(f"\nVenue: {venue['name']}") for product in venue.get('products', []): if not product.get('feedUrl'): continue print(f" Product: {product['productName']}") feed = fetch_feed(product['feedUrl']) for slot in feed['slots']: print( f" {slot['date']} {slot['time']}" f" party:{slot['party_size']}" f" open:{slot['spots_open']}/{slot['spots_total']}" f" [{slot['type']}]" ) ``` Java ```java import com.fasterxml.jackson.databind.*; import java.io.*; import java.net.*; import java.net.http.*; import java.util.Map; import java.util.zip.GZIPInputStream; public class FeedExample { static final String AUTH_URL = "https://auth.bookabletech.com/oauth/token"; static final String API_URL = "https://api.bookabletech.com"; static final HttpClient HTTP = HttpClient.newHttpClient(); static final ObjectMapper JSON = new ObjectMapper(); public static void main(String[] args) throws Exception { String token = getToken(System.getenv("CLIENT_ID"), System.getenv("CLIENT_SECRET")); iterateVenues(token); } static String getToken(String clientId, String clientSecret) throws Exception { String body = JSON.writeValueAsString(Map.of( "grant_type", "client_credentials", "client_id", clientId, "client_secret", clientSecret, "audience", "api.bookabletech.com" )); var req = HttpRequest.newBuilder(URI.create(AUTH_URL)) .POST(HttpRequest.BodyPublishers.ofString(body)) .header("Content-Type", "application/json") .build(); return JSON.readTree(HTTP.send(req, HttpResponse.BodyHandlers.ofString()).body()) .get("access_token").asText(); } static void iterateVenues(String token) throws Exception { int page = 1, totalPages; do { var req = HttpRequest.newBuilder( URI.create(API_URL + "/venues?pageNumber=" + page + "&pageSize=100")) .header("Authorization", "Bearer " + token) .build(); JsonNode body = JSON.readTree(HTTP.send(req, HttpResponse.BodyHandlers.ofString()).body()); totalPages = body.get("meta").get("totalPages").asInt(); for (JsonNode venue : body.get("data")) { System.out.println("\nVenue: " + venue.get("name").asText()); for (JsonNode product : venue.withArray("products")) { JsonNode feedUrlNode = product.get("feedUrl"); if (feedUrlNode == null || feedUrlNode.isNull()) continue; System.out.println(" Product: " + product.get("productName").asText()); iterateFeed(feedUrlNode.asText()); } } } while (++page <= totalPages); } static void iterateFeed(String feedUrl) throws Exception { var req = HttpRequest.newBuilder(URI.create(feedUrl)).build(); var res = HTTP.send(req, HttpResponse.BodyHandlers.ofInputStream()); try (var reader = new InputStreamReader(new GZIPInputStream(res.body()))) { for (JsonNode slot : JSON.readTree(reader).withArray("slots")) { System.out.printf(" %s %s party:%d open:%d/%d [%s]%n", slot.get("date").asText(), slot.get("time").asText(), slot.get("party_size").asInt(), slot.get("spots_open").asInt(), slot.get("spots_total").asInt(), slot.get("type").asText()); } } } } ``` C# ```csharp using System.IO.Compression; using System.Net.Http.Json; using System.Text.Json; const string authUrl = "https://auth.bookabletech.com/oauth/token"; const string apiUrl = "https://api.bookabletech.com"; using var http = new HttpClient(); // Get token var tokenRes = await http.PostAsJsonAsync(authUrl, new { grant_type = "client_credentials", client_id = Environment.GetEnvironmentVariable("CLIENT_ID"), client_secret = Environment.GetEnvironmentVariable("CLIENT_SECRET"), audience = "api.bookabletech.com", }); using var tokenDoc = await JsonDocument.ParseAsync(await tokenRes.Content.ReadAsStreamAsync()); http.DefaultRequestHeaders.Authorization = new("Bearer", tokenDoc.RootElement.GetProperty("access_token").GetString()); // Page through venues int page = 1, totalPages; do { using var venuesDoc = await http.GetFromJsonAsync( $"{apiUrl}/venues?pageNumber={page}&pageSize=100"); totalPages = venuesDoc!.RootElement.GetProperty("meta").GetProperty("totalPages").GetInt32(); foreach (var venue in venuesDoc.RootElement.GetProperty("data").EnumerateArray()) { Console.WriteLine($"\nVenue: {venue.GetProperty("name").GetString()}"); foreach (var product in venue.GetProperty("products").EnumerateArray()) { if (!product.TryGetProperty("feedUrl", out var feedUrlProp) || feedUrlProp.ValueKind == JsonValueKind.Null) continue; Console.WriteLine($" Product: {product.GetProperty("productName").GetString()}"); // Feed download requires no authentication using var feedStream = await http.GetStreamAsync(feedUrlProp.GetString()); using var gzip = new GZipStream(feedStream, CompressionMode.Decompress); using var feedDoc = await JsonDocument.ParseAsync(gzip); foreach (var slot in feedDoc.RootElement.GetProperty("slots").EnumerateArray()) { Console.WriteLine( $" {slot.GetProperty("date").GetString()} {slot.GetProperty("time").GetString()}" + $" party:{slot.GetProperty("party_size").GetInt32()}" + $" open:{slot.GetProperty("spots_open").GetInt32()}/{slot.GetProperty("spots_total").GetInt32()}" + $" [{slot.GetProperty("type").GetString()}]"); } } } } while (++page <= totalPages); ``` ## 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 ```