Base URL and keys
Each venue runs its own FillTables instance, so the base URL comes from the venue owner. The venue owner also mints your key under Settings → Advanced → Partner API and hands it to you once. Examples below use https://api.example.com.
Authorization: Bearer ft_…
The key is bound to one venue, so no request ever names a venue. Keep it server-side. The owner can revoke it at any time, after which every call answers 401.
Endpoints
| Method | Path | Purpose |
|---|---|---|
| GET | /partner/v1/venue | Name, currency, booking hours, slot length, largest table |
| GET | /partner/v1/availability | Times the floor can seat a party on a day |
| POST | /partner/v1/bookings | Create a booking |
| GET | /partner/v1/bookings/{id} | Read one of your bookings |
| POST | /partner/v1/bookings/{id}/cancel | Cancel one of your bookings |
| GET | /partner/v1/bookings?from=&to= | Your bookings in a window |
Your bookings means bookings created with a key carrying your partner name. Bookings made on the venue website, by staff, or by another partner are never visible to you.
GET /partner/v1/venue
{
"venueId": "venue-1",
"name": "Glider Garden",
"currencyCode": "AUD",
"source": "partner:eventbrite",
"reservations": {
"enabled": true, "autoConfirm": true,
"openHour": 11, "closeHour": 21,
"slotMins": 30, "durationMins": 90,
"maxTableSeats": 8
}
}
source is the tag every booking you create carries. maxTableSeats is the largest single table: a party above it never gets an instant slot and always lands with staff as a request. null means the venue has not drawn a floor plan yet.
GET /partner/v1/availability
GET /partner/v1/availability?date=2026-10-03&partySize=4
{
"venueId": "venue-1", "date": "2026-10-03", "partySize": 4, "maxTableSeats": 8,
"slots": [
{ "at": 1790722800000, "atIso": "2026-10-03T08:00:00.000Z", "time": "18:00" },
{ "at": 1790724600000, "atIso": "2026-10-03T08:30:00.000Z", "time": "18:30" }
]
}
date is the venue's local calendar day and time is venue-local; at and atIso are absolute. An empty slots array means nothing fits that day for that party, either because the day is full or because the party is larger than any table. You may still POST a booking for a time that is not listed; it becomes a request for staff rather than an instant confirmation.
POST /partner/v1/bookings
curl -X POST "https://api.example.com/partner/v1/bookings" \
-H "Authorization: Bearer ft_…" -H "Content-Type: application/json" \
-d '{
"customerName": "Priya Shah",
"partySize": 4,
"at": "2026-10-03T19:00:00+10:00",
"phone": "+61412345678",
"email": "priya@example.au",
"notes": "Window seat if possible",
"externalRef": "EB-1001"
}'
| Field | Required | Rules |
|---|---|---|
customerName | yes | 1 to 80 characters |
partySize | yes | integer 1 to 200 |
at | yes | epoch ms or ISO-8601 with offset. Or send date (YYYY-MM-DD) + time (HH:MM) in venue-local time |
phone | no | E.164 (+61…), or an Australian local number, which is normalised. Refused if malformed |
email | no | A valid address. Refused if malformed |
notes | no | up to 300 characters, shown to staff |
address | no | up to 200 characters: an off-site location for an event held elsewhere |
externalRef | no | your own booking id, up to 80 characters. Enables idempotency |
The time must be no more than 3 hours in the past and no more than a year ahead.
// 201 Created
{ "booking": { …the booking object… }, "confirmed": true }
Confirmed vs request. FillTables auto-assigns a real table and confirms on the spot when the floor can seat the party at that time (confirmed: true, status: "CONFIRMED"). When nothing fits, the booking still lands as status: "BOOKED" with confirmed: false, and the venue's staff confirm or decline it. Subscribe to reservation.confirmed and reservation.cancelled to learn the outcome.
Idempotency. When externalRef is set and a booking with that reference already exists for your partner, the request returns 200 with the existing booking and "duplicate": true. No second table is taken. Always send externalRef and retry on network errors without fear.
The venue receives the same email it gets for a website booking. FillTables does not email the guest for partner bookings; that confirmation is yours to send.
GET /partner/v1/bookings/{id}
200 { "booking": { … } }, or 404 if the id is not one of your bookings.
POST /partner/v1/bookings/{id}/cancel
200 { "booking": { …, "status": "CANCELLED" } }. Cancelling an already-cancelled booking returns 200 again. Cancelling a party that has already been seated answers 409.
GET /partner/v1/bookings?from=&to=
from and to are epoch milliseconds; the default window is yesterday to 90 days ahead, and a window may not exceed a year. Returns { "venueId", "from", "to", "bookings": [ … ] }.
The booking object
The same object is returned by every endpoint above and delivered inside every webhook.
{
"id": "0192d3a0-…",
"venueId": "venue-1",
"status": "CONFIRMED",
"partySize": 4,
"customerName": "Priya Shah",
"at": 1790762400000,
"atIso": "2026-10-03T09:00:00.000Z",
"durationMins": 90,
"tableId": "T4",
"phone": "+61412345678",
"email": "priya@example.au",
"notes": "Window seat if possible",
"address": "12 Party Lane, Sydney",
"source": "partner:eventbrite",
"externalRef": "EB-1001",
"createdAt": 1789820000000
}
status is one of BOOKED (awaiting staff), CONFIRMED, SEATED, CANCELLED, NO_SHOW. Optional fields (tableId, phone, email, notes, address, externalRef) are omitted when empty rather than sent as null.
Errors and limits
Every error is JSON: { "error": "<plain sentence>", "code": "<CODE>" }.
| Status | Code | Meaning |
|---|---|---|
| 400 | INVALID | A field failed validation; error says which |
| 401 | INVALID_KEY | Missing, unknown or revoked key |
| 404 | NOT_FOUND | Not one of your bookings, or an unknown path |
| 409 | CONFLICT, BAD_TRANSITION | The booking cannot move to that state (already seated) |
| 429 | RATE_LIMITED | Over the limit; back off and retry |
Bookings are refused with 400 when the venue has switched online bookings off. The limit is 300 requests a minute per key; failed authentications are limited separately per IP address. On 429, wait and retry with backoff rather than spreading the same load across more keys.
Webhooks: events
The venue owner adds your HTTPS URL under Settings → Webhooks, ticks the events you want, and is shown a signing secret once to pass to you. FillTables then POSTs to your URL.
| Event | When |
|---|---|
reservation.created | A booking landed, from any channel |
reservation.confirmed | Staff confirmed it, or the floor confirmed it instantly |
reservation.seated | The party arrived and was seated |
reservation.cancelled | Cancelled by staff, the guest, or a partner |
reservation.no_show | Marked a no-show |
reservation.updated | Time, party, table, contact or address edited |
ping | The owner pressed Test |
Events are sent for every booking at the venue, not only yours. Filter on data.source if you only care about your own (partner:<your-name>).
Webhooks: delivery
POST <your url>
Content-Type: application/json
User-Agent: FillTables-Webhooks/1
X-FillTables-Event: reservation.confirmed
X-FillTables-Delivery: <delivery id>
X-FillTables-Signature: t=1789820000,v1=5f2b…
{
"id": "<delivery id>",
"event": "reservation.confirmed",
"createdAt": 1789820000123,
"venueId": "venue-1",
"data": { …the booking object… }
}
For the Slack format the body is { "text": "…one readable line…" } with no booking object; ask the owner to choose JSON for an integration.
Answer with any 2xx within 5 seconds. Redirects are not followed. On 5xx, 429 or 408 FillTables retries twice more (after 1 s and 10 s); on any other 4xx it does not. The same delivery id is reused on every retry of one event, so de-duplicate on it. Deliveries can arrive out of order under retry; createdAt in the envelope is the event time.
Verify the signature
v1 is HMAC-SHA256 over "<t>.<raw body>" with the signing secret, hex encoded. Reject a delivery whose t is more than 5 minutes from your clock; that blocks replays. Use the raw request body bytes, before any JSON parsing or re-serialisation.
Node
import { createHmac, timingSafeEqual } from 'node:crypto'
export function verify(secret, header, rawBody, nowMs = Date.now()) {
const parts = Object.fromEntries(header.split(',').map((kv) => kv.split('=')))
const t = Number(parts.t)
if (!Number.isFinite(t) || Math.abs(nowMs / 1000 - t) > 300) return false
const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex')
const a = Buffer.from(expected, 'hex'), b = Buffer.from(String(parts.v1 ?? ''), 'hex')
return a.length === b.length && timingSafeEqual(a, b)
}
Python
import hmac, hashlib, time
def verify(secret: str, header: str, raw_body: bytes, now: float | None = None) -> bool:
parts = dict(kv.split('=', 1) for kv in header.split(','))
t = int(parts.get('t', '0'))
if abs((now or time.time()) - t) > 300:
return False
expected = hmac.new(secret.encode(), f'{t}.'.encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts.get('v1', ''))
Putting it together
- The owner mints a key for you and adds your webhook URL, subscribed to
reservation.confirmedandreservation.cancelled. - Your app calls
availabilityand shows the guest the free times. - On booking, POST with your
externalRef; store the returnedbooking.idand show the guest "confirmed" or "pending the venue" fromconfirmed. - Webhooks keep your record in step:
confirmedwhen staff accept a pending request,cancelledif the venue or the guest cancels. - If the guest cancels with you, POST
/cancel.