FillTables Partner API

FillTables · Developer reference · v1

FillTables Partner API

Book straight into a FillTables venue from a booking platform, an events app or the venue website, and hear back the moment a booking is confirmed, seated or cancelled.

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

MethodPathPurpose
GET/partner/v1/venueName, currency, booking hours, slot length, largest table
GET/partner/v1/availabilityTimes the floor can seat a party on a day
POST/partner/v1/bookingsCreate a booking
GET/partner/v1/bookings/{id}Read one of your bookings
POST/partner/v1/bookings/{id}/cancelCancel 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"
  }'
FieldRequiredRules
customerNameyes1 to 80 characters
partySizeyesinteger 1 to 200
atyesepoch ms or ISO-8601 with offset. Or send date (YYYY-MM-DD) + time (HH:MM) in venue-local time
phonenoE.164 (+61…), or an Australian local number, which is normalised. Refused if malformed
emailnoA valid address. Refused if malformed
notesnoup to 300 characters, shown to staff
addressnoup to 200 characters: an off-site location for an event held elsewhere
externalRefnoyour 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>" }.

StatusCodeMeaning
400INVALIDA field failed validation; error says which
401INVALID_KEYMissing, unknown or revoked key
404NOT_FOUNDNot one of your bookings, or an unknown path
409CONFLICT, BAD_TRANSITIONThe booking cannot move to that state (already seated)
429RATE_LIMITEDOver 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.

EventWhen
reservation.createdA booking landed, from any channel
reservation.confirmedStaff confirmed it, or the floor confirmed it instantly
reservation.seatedThe party arrived and was seated
reservation.cancelledCancelled by staff, the guest, or a partner
reservation.no_showMarked a no-show
reservation.updatedTime, party, table, contact or address edited
pingThe 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

  1. The owner mints a key for you and adds your webhook URL, subscribed to reservation.confirmed and reservation.cancelled.
  2. Your app calls availability and shows the guest the free times.
  3. On booking, POST with your externalRef; store the returned booking.id and show the guest "confirmed" or "pending the venue" from confirmed.
  4. Webhooks keep your record in step: confirmed when staff accept a pending request, cancelled if the venue or the guest cancels.
  5. If the guest cancels with you, POST /cancel.