## Overview Your `client_id` and `client_secret` are available in the [Bookable Portal](https://portal.bookabletech.com) under your account settings. To securely access the Bookable API, obtain an OAuth 2.0 access token using the **Client Credentials** flow: 1. POST your credentials to the token endpoint. 2. Cache the returned token along with the `expires_in` value. 3. Attach the token to every API request as `Authorization: Bearer `. 4. Re-fetch the token when it is close to expiry (recommended: 60 seconds before). ## Token endpoint | Environment | URL | | --- | --- | | Production | `https://auth.bookabletech.com/oauth/token` | | Sandbox | `https://auth-sandbox.bookabletech.com/oauth/token` | The examples below include token caching — avoid requesting a new token on every API call. cURL ```bash curl -X POST https://auth.bookabletech.com/oauth/token \ -H "Content-Type: application/json" \ -d '{ "grant_type": "client_credentials", "client_id": "YOUR_CLIENT_ID", "client_secret": "YOUR_CLIENT_SECRET", "audience": "api.bookabletech.com" }' ``` Copy the `access_token` from the response and use it in your API calls: ```bash curl https://api.bookabletech.com/venues \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` JavaScript ```javascript // Node.js — token manager with caching (fetch available natively in Node 18+) class TokenManager { #accessToken = null; #expiresAt = 0; async getAccessToken() { if (this.#accessToken && Date.now() < this.#expiresAt - 60_000) { return this.#accessToken; } const response = await fetch('https://auth.bookabletech.com/oauth/token', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ grant_type: 'client_credentials', client_id: process.env.BOOKABLE_CLIENT_ID, client_secret: process.env.BOOKABLE_CLIENT_SECRET, audience: 'api.bookabletech.com', }), }); if (!response.ok) { throw new Error(`Auth failed: ${response.status} ${await response.text()}`); } const data = await response.json(); this.#accessToken = data.access_token; this.#expiresAt = Date.now() + data.expires_in * 1000; return this.#accessToken; } } // Usage const tokenManager = new TokenManager(); const token = await tokenManager.getAccessToken(); const apiResponse = await fetch('https://api.bookabletech.com/venues', { headers: { Authorization: `Bearer ${token}` }, }); ``` Python ```python import time import requests import os class TokenManager: def __init__(self): self._access_token = None self._expires_at = 0 def get_access_token(self) -> str: # Return cached token if still valid (with 60s buffer) if self._access_token and time.time() < self._expires_at - 60: return self._access_token response = requests.post( 'https://auth.bookabletech.com/oauth/token', json={ 'grant_type': 'client_credentials', 'client_id': os.environ['BOOKABLE_CLIENT_ID'], 'client_secret': os.environ['BOOKABLE_CLIENT_SECRET'], 'audience': 'api.bookabletech.com', } ) response.raise_for_status() data = response.json() self._access_token = data['access_token'] self._expires_at = time.time() + data['expires_in'] return self._access_token # Usage token_manager = TokenManager() token = token_manager.get_access_token() api_response = requests.get( 'https://api.bookabletech.com/venues', headers={'Authorization': f'Bearer {token}'} ) ``` Java ```java import java.net.http.*; import java.net.URI; import java.time.Instant; import java.util.Map; import com.fasterxml.jackson.databind.ObjectMapper; public class TokenManager { private static final HttpClient client = HttpClient.newHttpClient(); private static final ObjectMapper mapper = new ObjectMapper(); private static String accessToken; private static Instant expirationTime; public static String getAccessToken() throws Exception { if (accessToken != null && Instant.now().isBefore(expirationTime.minusSeconds(60))) { return accessToken; } String body = """ { "grant_type": "client_credentials", "client_id": "YOUR_CLIENT_ID", "client_secret": "YOUR_CLIENT_SECRET", "audience": "api.bookabletech.com" } """; HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://auth.bookabletech.com/oauth/token")) .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(body)) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); var tokenData = mapper.readValue(response.body(), Map.class); accessToken = (String) tokenData.get("access_token"); int expiresIn = (Integer) tokenData.get("expires_in"); expirationTime = Instant.now().plusSeconds(expiresIn); return accessToken; } } ``` C# ```csharp using System.Net.Http.Json; using System.Text.Json; public class TokenManager { private static string? _accessToken; private static DateTime _expirationTime; private static readonly HttpClient Client = new(); public static async Task GetAccessTokenAsync() { if (_accessToken != null && DateTime.UtcNow < _expirationTime.AddSeconds(-60)) return _accessToken!; var payload = new { grant_type = "client_credentials", client_id = Environment.GetEnvironmentVariable("BOOKABLE_CLIENT_ID"), client_secret = Environment.GetEnvironmentVariable("BOOKABLE_CLIENT_SECRET"), audience = "api.bookabletech.com" }; var response = await Client.PostAsJsonAsync("https://auth.bookabletech.com/oauth/token", payload); response.EnsureSuccessStatusCode(); var content = await response.Content.ReadAsStringAsync(); using var doc = JsonDocument.Parse(content); _accessToken = doc.RootElement.GetProperty("access_token").GetString(); var expiresIn = doc.RootElement.GetProperty("expires_in").GetInt32(); _expirationTime = DateTime.UtcNow.AddSeconds(expiresIn); return _accessToken!; } } ``` Ruby ```ruby require 'net/http' require 'json' require 'time' class TokenManager @@access_token = nil @@expiration_time = Time.now def self.get_access_token if @@access_token.nil? || Time.now >= (@@expiration_time - 60) uri = URI("https://auth.bookabletech.com/oauth/token") request = Net::HTTP::Post.new(uri, 'Content-Type' => 'application/json') request.body = { grant_type: 'client_credentials', client_id: ENV['BOOKABLE_CLIENT_ID'], client_secret: ENV['BOOKABLE_CLIENT_SECRET'], audience: 'api.bookabletech.com' }.to_json response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http| http.request(request) end data = JSON.parse(response.body) @@access_token = data["access_token"] @@expiration_time = Time.now + data["expires_in"].to_i end @@access_token end end ``` ## Response reference ```json { "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...", "scope": "venue:read venue-booking:create", "expires_in": 3600, "token_type": "Bearer" } ``` | Field | Description | | --- | --- | | `access_token` | JWT to include in every API request as `Authorization: Bearer ` | | `scope` | Permissions granted: `venue:read` (search venues), `venue-booking:create` (create bookings) | | `expires_in` | Token lifetime in seconds (typically 3600). Always read this — do not hardcode it. | | `token_type` | Always `Bearer` | 🔔 Important Do not hardcode the expiration time — always read and store the `expires_in` value from the response. ## Next steps Quickstart Make your first booking end-to-end in 5 steps Core Concepts Understand venues, composite IDs, and booking types API Reference Full endpoint documentation Webhooks Receive real-time booking status updates