REST endpoints for partner integrations — POS systems, chat widgets, AI phone agents, anything that needs to read availability or write bookings on behalf of a restaurant.
https://tablefox.co.uk/api/v1/* — semver, breaking changes only on a major bump.{ "error": "...", "errors": ["...", "..."] } shape with the right HTTP status (400/401/403/404/409/500).
Each restaurant has its own API key, issued by TableFox. To integrate on behalf of a restaurant, ask them to generate a key from their TableFox dashboard, or email hello@tablefox.co.uk. Master keys (one key, many restaurants) are available for accredited partners — get in touch and we'll provision one.
Check availability for a party of 4 on 5 April 2026:
# cURL curl 'https://tablefox.co.uk/api/v1/availability?date=2026-04-05&covers=4' \ -H 'X-API-Key: your-key-here'
Create a booking:
# cURL curl 'https://tablefox.co.uk/api/v1/bookings' \ -X POST \ -H 'X-API-Key: your-key-here' \ -H 'Content-Type: application/json' \ -d '{"date":"2026-04-05","time":"19:00","covers":4,"first_name":"Alex","phone":"+447450000000"}'
Once you've got that working, download the OpenAPI spec and import it into Postman, Insomnia or your client generator of choice for the rest.
Three ways to send your key — pick whichever your HTTP client prefers. Click any endpoint below to see request fields, responses and error codes.
GET /bookings/lookup, POST /bookings/modify, POST /bookings/cancel) are tagged self-service in the sidebar — they're the customer-facing variants used by chat widgets and AI phone agents. They require the booking's phone number to match the request and respect the restaurant's edit/cancel cutoff window. If you're building an integration on behalf of the restaurant (POS, EPOS, CRM, etc.) you'll usually want the equivalent untagged endpoints instead: POST /bookings/:id/modify and POST /bookings/:id/status with CANCELLED.
All integration endpoints require authentication via one of:
| X-API-Key header | X-API-Key: your-key-here |
| Query parameter | ?api_key=your-key-here |
| Bearer token | Authorization: Bearer your-key-here |
Per-restaurant key: Identifies the restaurant automatically. No restaurant_id needed.
Master key: Requires restaurant_id parameter to identify which restaurant.
Check available time slots for a date and party size.
| Param | Type | Required | Description |
|---|---|---|---|
date | string | Yes | Date in YYYY-MM-DD format |
covers | integer | Yes | Number of guests. Capped at the restaurant's maximum online party size setting (same limit as the booking widget); above it the endpoint returns 400 with a max_covers field. |
service | string | No | Service code filter (e.g. LUNCH, DINNER) |
restaurant_id | integer | Master key only | Restaurant ID |
{ "success": true, "restaurant": "Viva Brazil", "date": "2026-04-05", "covers": 4, "slots": [{ "time": "12:00", "service": "LUNCH", "available": true }, ...], "count": 15 }
List a restaurant's call-transfer destinations for the AI phone agent's "Call Transfer" action. Returns a bare JSON array; the agent matches the caller's request against each destination's trigger. Each row carries an available flag computed from its hours (weekly schedule + date overrides, evaluated in Europe/London). Open destinations include a phone_number; closed destinations have phone_number: null plus opens_label / opens_iso so the agent can tell the caller when to try again (and still offer a callback). An empty array means no transfers are configured at all.
| Param | Type | Required | Description |
|---|---|---|---|
restaurant_id | integer | Master key only | Restaurant ID |
[
{ "phone_number": "+441234567890", "label": "Takeaway", "trigger": "caller wants a takeaway or collection order", "priority": 1, "available": true },
{ "phone_number": null, "label": "Events", "trigger": "large group or private event enquiry", "priority": 2, "available": false, "opens_label": "tomorrow at 10:00am", "opens_iso": "2026-07-17T10:00:00+01:00" }
]
Resolve a dialed AI phone number to its restaurant. Used by the AI phone agent at call setup: the dialed number comes in, the restaurant's identity and prompt FAQs come back. Numbers are assigned per restaurant by the platform team. Master key auth only — this is the reverse lookup (no restaurant_id is known yet), so per-restaurant keys are rejected.
| Param | Type | Required | Description |
|---|---|---|---|
number | string | Yes | The dialed number in E.164 format, e.g. +447700900000 |
{ "restaurant_id": 12, "name": "The Fox & Hound", "faqs": "Q: Do you have parking? A: ...", "voice": "",
"hours": "Monday: Lunch 12:00 to 15:00, Dinner 17:00 to 22:00\nTuesday: Closed\n..." }
Returns 404 when no restaurant has that number assigned, 400 for a malformed number. voice is the restaurant's TTS voice id and tts_provider its speech-engine preference (both empty = agent defaults). spoken_name is an optional phonetic spelling used only when the agent says the restaurant name aloud (fixes mispronounced names like "Knutsford" → "Nutsford"). hours is a plain-text opening-hours summary — the weekly service schedule plus any date exceptions in the next 14 days — so the agent can answer hours questions from real data (empty when no schedule is configured).
Returning-caller lookup used by the AI phone agent at call setup: does this caller-ID number have an email address on file from a previous booking at this restaurant? Only a masked address is ever returned — the agent offers "the email ending …" instead of making the caller spell one out, and POST /api/v1/bookings with email_on_file: true resolves the real address server-side, so the full email never crosses the voice channel.
| Param | Type | Required | Description |
|---|---|---|---|
restaurant_id | integer | Master key only | Restaurant ID |
phone | string | Yes | Caller-ID number in E.164 format, e.g. +447700900000 |
{ "known": true, "first_name": "Chris", "email_masked": "c***@apexia.uk" }
Returns { "known": false } when the number has no previous booking with an email at this restaurant, 400 for a malformed number. The lookup uses the most recent matching booking.
Per-call usage and cost record, posted by the in-house voice agent when a call ends. Feeds the AI 30d usage column above and monthly per-restaurant cost reporting. Costs are estimates computed by the voice agent from its configured rates until reconciled against provider bills.
{ "restaurant_id": 12, "call_sid": "CA...", "caller_phone": "+447...", "started_at": "2026-07-20T17:31:02Z",
"duration_seconds": 142, "turns": 9, "input_tokens": 1200, "output_tokens": 950,
"cache_read_tokens": 18400, "cache_write_tokens": 5200,
"llm_cost": 0.031, "telephony_cost": 0.142, "currency": "GBP", "outcome": "booking_confirmed",
"transcript": "[0:02] Agent: Hello, thank you for calling...\n[0:07] Caller: ..." }
transcript is optional: timestamped plain-text conversation lines, viewable per call in the platform admin. tts_provider and voice_id optionally record which speech engine and voice spoke on the call.
{ "success": true }
Create a new booking. Sends confirmation email/SMS if auto-accepted. Sends payment link if restaurant requires deposit/card hold. When the restaurant has new-booking email alerts switched on (Settings → Notifications, off by default), a staff alert email is also sent for customer-driven sources (ONLINE, AI_PHONE) — never for INTERNAL, WALKIN or PHONE.
Once the booking is created, this endpoint always returns success: true. If a confirmation email/SMS or payment link fails afterwards, the booking still exists and is still returned — the failure is recorded in the API log as [POST-PROCESSING FAILED] rather than reported as an error. Do not retry on a non-response: retrying a booking that succeeded creates a duplicate.
| Field | Type | Required | Description |
|---|---|---|---|
date | string | Yes | YYYY-MM-DD |
time | string | Yes | HH:MM (24-hour) |
covers | integer | Yes | Number of guests. Capped at the restaurant's maximum online party size setting (same limit as the booking widget); above it the endpoint returns 400 with a max_covers field. |
first_name | string | Yes | Customer first name |
last_name | string | No | Customer last name |
phone | string | Conditional | Phone or email required |
email | string | Conditional | Required if restaurant has payment mode enabled |
email_on_file | boolean | No | Returning-caller shortcut used by the AI phone agent: when true (and email is not sent), the email from this phone number's most recent booking at the restaurant is reused server-side. Ignored when email is provided or nothing is on file. |
notes | string | No | Special requests |
tags | string/array | No | Comma-separated or array: BIRTHDAY, ANNIVERSARY, ALLERGY, HIGH_CHAIR, VIP |
marketing_opt_in_email | boolean | No | Set true ONLY when the customer explicitly consented to email marketing. Stored on the customer record and synced into marketing contacts. |
marketing_opt_in_sms | boolean | No | Set true ONLY when the customer explicitly consented to SMS marketing. |
source | string | No | ONLINE, INTERNAL, PHONE, WALKIN, AI_PHONE. Default: PHONE |
restaurant_id | integer | Master key only | Restaurant ID |
{ "success": true, "booking": { "booking_id": 123, "booking_reference": "RB-20260405-A1B2C3", "status": "CONFIRMED", "date": "2026-04-05", "time": "19:00", "covers": 4, "customer_name": "John Smith" }, "payment": null, "message": "Booking confirmed successfully" }
Payment response (when deposit/hold required):
{ "success": true, "booking": { ... "status": "PENDING_PAYMENT" }, "payment": { "required": true, "mode": "DEPOSIT", "amount_formatted": "£20.00", "message": "A deposit of £20.00 is required. A payment link has been sent." } }
List bookings for a date with optional filters. Cancelled bookings are excluded unless you ask for them via status=CANCELLED.
| Param | Type | Required | Description |
|---|---|---|---|
date | string | No | YYYY-MM-DD (defaults to today) |
status | string | No | Filter: PENDING, CONFIRMED, SEATED, COMPLETED, NO_SHOW, CANCELLED |
service | string | No | Service code (LUNCH, DINNER, …) |
q | string | No | Search name / phone / email / reference (min 2 chars) |
search_all_dates | boolean | No | Set 1 with q to search across all dates |
page | integer | No | Page number (default 1) |
per_page | integer | No | 1–50 (default 30) |
restaurant_id | integer | Master key only | Restaurant ID |
{ "success": true, "date": "2026-04-05", "bookings": [{ "booking_id": 123, "reference": "RB-...", "name": "John Smith", "phone": "+44...", "covers": 4, "time": "19:00", "status": "CONFIRMED", "service": "DINNER", "tables": ["T5"], "notes": "Window seat", "extras_count": 0, ... }], "pagination": { "page": 1, "per_page": 30, "total": 17, "total_pages": 1 } }
Full detail for a single booking — covers, tables, tags, voucher redemptions, extras, customer stats (VIP / blocked / total visits) and timestamps.
| Param | Type | Required | Description |
|---|---|---|---|
id | integer | Yes | Booking ID |
restaurant_id | integer | Master key only | Restaurant ID (query param) |
{ "success": true, "booking": { "booking_id": 123, "reference": "RB-...", "status": "CONFIRMED", "date": "2026-04-05", "time": "19:00", "covers": 4, "customer": { "customer_id": 42, "name": "John Smith", "phone": "+44...", "email": "...", "is_vip": false, "total_bookings": 5, "total_no_shows": 0 }, "tables": [{ "table_id": 7, "name": "T5", "zone": "Window" }], "tags": ["BIRTHDAY"], "notes": "...", "internal_notes": "...", "vouchers": [...], "extras": [...] } }
Look up a customer's upcoming bookings by phone number. Only returns bookings matching the phone — no data leakage.
| Param | Type | Required | Description |
|---|---|---|---|
phone | string | Yes | Customer phone number |
restaurant_id | integer | Master key only | Restaurant ID |
{ "success": true, "bookings": [{ "booking_id": 123, "booking_reference": "RB-...", "date": "2026-04-05", "time": "19:00", "covers": 4, "status": "CONFIRMED", "name": "John Smith", "editable": true, "editable_reason": null, "cancellable": true, "cancellable_reason": null }], "count": 1, "editing_allowed": true, "editing_cutoff_hours": 24 }
editable requires both customer editing to be enabled AND the booking to be outside the cutoff window. cancellable only requires the booking to be outside the cutoff window. The same editing_cutoff_hours applies to both.
Modify an existing booking. Requires phone match for security. Respects restaurant edit settings and cutoff.
| Field | Type | Required | Description |
|---|---|---|---|
booking_id | integer | Yes | Booking ID from lookup |
phone | string | Yes | Must match booking's customer phone |
new_date | string | No | New date YYYY-MM-DD (omit to keep same) |
new_time | string | No | New time HH:MM (omit to keep same) |
new_covers | integer | No | New party size (omit to keep same). Capped at the restaurant's maximum online party size setting; above it the endpoint returns 400 with a max_covers field. |
new_email | string | No | Corrected email address. If the booking is awaiting payment, a fresh payment link is automatically emailed to the new address (response gains a payment.resent object). |
notes | string | No | Updated special requests (omit to keep same) |
tags | string/array | No | Updated tags: BIRTHDAY, ANNIVERSARY, ALLERGY, HIGH_CHAIR (omit to keep same) |
restaurant_id | integer | Master key only | Restaurant ID |
{ "success": true, "message": "Booking updated successfully", "booking": { "booking_id": 123, "booking_reference": "RB-...", "date": "2026-04-06", "time": "20:00", "covers": 4, "status": "CONFIRMED" } }
403 — Editing not allowed or within cutoff period404 — Booking not found or phone doesn't match400 — Time conflict with another bookingChange a booking's date, time, covers, notes, tags, or assigned tables. Acts on behalf of the restaurant — no phone match, no cutoff window. Use this from POS, EPOS, CRM or any other restaurant-side integration. (The self-service /api/v1/bookings/modify is the customer-facing equivalent with phone-match + cutoff.)
| Field | Type | Required | Description |
|---|---|---|---|
date | string | No | New YYYY-MM-DD |
time | string | No | New HH:MM |
covers | integer | No | New party size |
notes | string | No | Replace customer-facing notes |
internal_notes | string | No | Replace staff-only notes |
tags | array | No | Replace tag list |
table_ids | array | No | Replace table assignment (use GET /api/v1/tables/available for options) |
restaurant_id | integer | Master key only | Restaurant ID |
404 — Booking not found409 — OVERLAP_WARNING when the new time/tables conflict with another bookingCancel an existing booking. Requires phone match for security. Respects the restaurant's customer edit/cancel cutoff (see editing_cutoff_hours from /bookings/lookup) — cancellations inside that window are rejected with 403.
| Field | Type | Required | Description |
|---|---|---|---|
booking_id | integer | Yes | Booking ID from lookup |
phone | string | Yes | Must match booking's customer phone |
restaurant_id | integer | Master key only | Restaurant ID |
{ "success": true, "message": "Booking cancelled successfully", "booking_reference": "RB-..." }
403 — Within the cutoff window; customer must speak to the restaurant directly404 — Booking not found or phone mismatchChange a booking's status — e.g. SEATED when the host seats the guest, COMPLETED when the bill is closed, NO_SHOW after a grace window.
| Field | Type | Required | Description |
|---|---|---|---|
status | string | Yes | One of: PENDING, CONFIRMED, SEATED, COMPLETED, NO_SHOW, CANCELLED |
restaurant_id | integer | Master key only | Restaurant ID |
{ "success": true, "booking": { "booking_id": 123, "reference": "RB-...", "status": "SEATED" }, "message": "Status updated to SEATED" }
Same as POST /api/v1/vouchers/redeem, but the redemption is automatically linked to the booking at :id — convenient when the POS already knows which check the voucher applies to.
| Field | Type | Required | Description |
|---|---|---|---|
code | string | Yes | Voucher code |
amount_pence | integer | Yes | Amount to redeem, in pence |
note | string | No | Free-text note |
restaurant_id | integer | Master key only | Restaurant ID |
Search customers by name, email or phone. Useful when a POS wants to surface "is this a returning guest?" before opening a tab.
| Param | Type | Required | Description |
|---|---|---|---|
q | string | Yes | Search term (min 2 chars). Matches name / email / phone. |
page | integer | No | Page number (default 1) |
per_page | integer | No | 1–50 (default 20) |
restaurant_id | integer | Master key only | Restaurant ID |
{ "success": true, "customers": [{ "customer_id": 42, "name": "John Smith", "email": "...", "phone": "+44...", "total_bookings": 5, "total_no_shows": 0, "last_visit": "2026-03-12", "is_vip": false, "blocked": false }], "pagination": { ... } }
Full customer record plus recent booking history (last 50 bookings most-recent first). Use this to show "this is their Nth visit" pop-ups in the POS.
| Param | Type | Required | Description |
|---|---|---|---|
id | integer | Yes | Customer ID |
restaurant_id | integer | Master key only | Restaurant ID (query param) |
{ "success": true, "customer": { "customer_id": 42, "first_name": "John", "last_name": "Smith", "email": "...", "phone": "+44...", "total_bookings": 5, "total_no_shows": 0, "is_vip": false, "blocked": false }, "bookings": [{ "booking_id": 123, "reference": "RB-...", "date": "2026-03-12", "time": "19:00", "covers": 4, "status": "COMPLETED" }] }
Returns every active table with an is_occupied flag for the requested time window and an is_suggested flag for the best-fit suggestion. Used to power POS floor plans.
| Param | Type | Required | Description |
|---|---|---|---|
covers | integer | No | Party size (default 2) |
date | string | No | YYYY-MM-DD (default today) |
time | string | No | HH:MM (default now) |
restaurant_id | integer | Master key only | Restaurant ID |
{ "success": true, "tables": [{ "table_id": 7, "name": "T5", "zone": "Window", "min_covers": 2, "max_covers": 4, "is_occupied": false, "is_suggested": true, "fits_party": true }], "suggested_ids": [7] }
Look up a voucher by exact code. Returns value, remaining balance, expiry, and a convenience can_redeem flag (active & not expired & positive balance). Always returns 200 with found: false when the code doesn't exist — no 404 — so POS can show a friendly "voucher not found" message.
| Param | Type | Required | Description |
|---|---|---|---|
code | string | Yes | Voucher code (case-insensitive) |
restaurant_id | integer | Master key only | Restaurant ID |
{ "success": true, "found": true, "voucher": { "voucher_id": 88, "code": "TF-ABC123", "status": "ACTIVE", "value_pence": 10000, "balance_pence": 6000, "expires_at": "2027-04-05 23:59:59", "is_expired": false, "recipient_name": "Alex", "product_name": "Gift £100", "can_redeem": true } }
Redeem (partially or fully) against a voucher's remaining balance. Standalone — pass booking_reference if you want the redemption linked to a specific booking.
| Field | Type | Required | Description |
|---|---|---|---|
code | string | Yes | Voucher code |
amount_pence | integer | Yes | Amount to redeem, in pence. Can't exceed remaining balance. |
booking_reference | string | No | Booking to link the redemption to (e.g. RB-20260405-A1B2C3) |
note | string | No | Free-text note attached to the redemption |
restaurant_id | integer | Master key only | Restaurant ID |
{ "success": true, "redemption_id": 555, "new_balance_pence": 3000, "fully_redeemed": false, "voucher": { "voucher_id": 88, "code": "TF-ABC123", "balance_pence": 3000, "status": "ACTIVE" }, "message": "Redeemed £30.00" }
Get restaurant info and services for a given date.
| Param | Type | Required | Description |
|---|---|---|---|
date | string | No | Date in YYYY-MM-DD (defaults to today) |
restaurant_id | integer | Master key only | Restaurant ID |
{ "success": true, "restaurant": { "name": "Viva Brazil", "phone": "0330...", "email": "...", "max_online_covers": 8 }, "date": "2026-04-05", "services": [{ "name": "Lunch", "code": "LUNCH", "start_time": "12:00:00", "end_time": "16:00:00" }, ...] }
Submit a callback request (typically from an AI phone agent). The request lands in the restaurant's in-app inbox (phone icon in the top nav) and fires a push notification to staff. An email is also sent to the restaurant address when the restaurant has turned on Email me too in Notification settings (off by default for new restaurants).
| Param | Type | Required | Description |
|---|---|---|---|
phone | string | Recommended | Caller's phone number |
first_name | string | No | Caller's first name |
last_name | string | No | Caller's last name |
reason | string | No | Short reason from the AI (e.g. "wants to book", "menu enquiry") |
notes | string | No | Free-form notes / transcript from the caller |
restaurant_id | integer | Master key only | Restaurant ID |
At least one of phone, first_name, or notes is required.
{ "success": true, "message": "Callback request received", "callback_request_id": 42 }
Point any of these at /developers/openapi.json to get an interactive sandbox or generated client SDK:
Questions, missing fields, or want to integrate something we haven't published yet? Email hello@tablefox.co.uk — we actively support POS, EPOS, ticketing and CRM integrations.