Skip to content
Get API key

API reference

Bookings and availability API

A guest books on the venue’s public booking page. That creates a booking request, which is not a reservation until someone confirms it. Confirming creates the reservation and links the two.

States: pending_payment (waiting for a transfer proof) → awaiting_confirmation → confirmed or rejected. A pending_payment request that passes the outlet’s expiry window becomes expired.

Sansflow never confirms on its own: a request waits until staff or your integration decides. While it waits it holds the slot’s capacity, so nobody else can take it.

List bookings

GET/api-v1-bookings
Paginated
cursor

Returns booking requests, newest request first. Poll with status=awaiting_confirmation, or subscribe to the booking.requested webhook and skip polling.

Query parameters

NameTypeRequiredDescription
statusstringOptionalOne of pending_payment, awaiting_confirmation, confirmed, rejected, expired.
fromstringOptionalEarliest requested_at, inclusive. ISO 8601 with offset, or YYYY-MM-DD.A bare date means 00:00 UTC that day, so pass a full timestamp with +07:00 to cut at local midnight.
tostringOptionalLatest requested_at, inclusive. ISO 8601 with offset, or YYYY-MM-DD.A bare date means 00:00 UTC that day, so pass a full timestamp with +07:00 to cut at local midnight.
location_iduuidOptionalOnly this outlet. A key pinned to one outlet is always scoped to it; another outlet’s id is 403.
cursorstringOptionalThe next_cursor from the previous page. Opaque; send it back unchanged.
limitintegerOptionalRows per page.Default 50. At least 1; values above 100 are capped at 100.

Response fields (200)

NameTypeDescription
data[].iduuidThe booking request.
data[].business_iduuidThe business it belongs to.
data[].location_iduuidThe outlet it was made for.
data[].short_codestringThe code in the guest’s booking link.
data[].statusstringpending_payment, awaiting_confirmation, confirmed, rejected or expired.
data[].customer_namestringThe guest’s name as typed.
data[].customer_phonestringThe guest’s phone, as +62….
data[].party_sizeintegerGuests.
data[].requested_attimestampThe slot the guest asked for.
data[].notesstring | nullThe guest’s note.
data[].requested_tableobject | null{ id, name } when the guest picked a table.
data[].payment_proof_uploaded_attimestamp | nullWhen the guest uploaded a transfer proof.
data[].preorderobject | null{ order_note, item_count, total_due, deposit_required, deposit_amount } when the booking carries a pre-order. order_note is one line of at most 300 characters, or null.
data[].rejection_reasonstring | nullSet when rejected; the guest sees it.
data[].reservation_iduuid | nullThe reservation once confirmed.
data[].reservation_codestring | nullIts code, R-YYMMDD-NNN.
data[].created_attimestampWhen the guest submitted it.
data[].updated_attimestampLast change.

Errors

Every error answers { "error": { "code", "message", "request_id" } }. Branch on code; see Errors for retrying.

HTTPCodeWhen
400validation_failedA query parameter did not parse; the message names it. Also returned when from/to span more than 400 days.
401invalid_keyThe Authorization header is missing, malformed, or the key is revoked or inactive.
402payment_requiredThe business’s plan does not include API access (Sansflow Premium). The key itself is fine.
403insufficient_scopeThe key does not carry the scope this endpoint needs, or it is pinned to a different outlet.
429rate_limitedOver the key’s per-minute limit (60 for sf_test_, 300 for sf_live_). Wait the seconds in Retry-After.
500internal_errorSomething failed on our side. Quote the request_id when you report it.
Request
curl -G "https://ewpuclvmnygoddunkyrb.supabase.co/functions/v1/api-v1-bookings" \
  -H "Authorization: Bearer $SANSFLOW_API_KEY" \
  --data-urlencode "status=awaiting_confirmation"
Response200 OK
{
  "data": [
    {
      "id": "e7a3c5d9-2b4f-4c6e-8d18-3f9a1b5c7e46",
      "business_id": "7c9e1a3b-5d7f-4b9a-8c2e-4f6a8b0c2d91",
      "location_id": "3f6c2a1e-8b4d-4c2a-9e51-7a0d2b6c9f10",
      "short_code": "7K2QM4X9PD3R",
      "status": "awaiting_confirmation",
      "customer_name": "Budi",
      "customer_phone": "+6281300000000",
      "party_size": 4,
      "requested_at": "2026-11-08T12:00:00+00:00",
      "notes": "Window seat if possible",
      "requested_table": {
        "id": "0c2e4a6b-8d1f-4b3c-9e75-2a6c8e0b4d79",
        "name": "T12"
      },
      "payment_proof_uploaded_at": null,
      "preorder": null,
      "rejection_reason": null,
      "reservation_id": null,
      "reservation_code": null,
      "created_at": "2026-11-06T03:12:45+00:00",
      "updated_at": "2026-11-06T03:12:45+00:00"
    }
  ],
  "has_more": false,
  "next_cursor": null,
  "request_id": "5d0c7e2a-91f4-4b3e-a8c6-0f2e4b6d8a1c"
}

Example values. Run replays this response here; nothing is sent.

Retrieve a booking

GET/api-v1-bookings/{booking_id}

One booking request. Once confirmed it also carries reservation, so you can follow the visit through seated, completed or no_show.

Path parameters

NameTypeRequiredDescription
booking_iduuidRequiredThe booking request’s id.

Response fields (200)

NameTypeDescription
iduuidThe booking request.
business_iduuidThe business it belongs to.
location_iduuidThe outlet it was made for.
short_codestringThe code in the guest’s booking link.
statusstringpending_payment, awaiting_confirmation, confirmed, rejected or expired.
reservationobjectPresent once confirmed: the reservation, with table_id, booking_id and booking_short_code besides the list fields.

Errors

Every error answers { "error": { "code", "message", "request_id" } }. Branch on code; see Errors for retrying.

HTTPCodeWhen
400invalid_requestThe id in the path is not a UUID.
401invalid_keyThe Authorization header is missing, malformed, or the key is revoked or inactive.
402payment_requiredThe business’s plan does not include API access (Sansflow Premium). The key itself is fine.
403insufficient_scopeThe key does not carry the scope this endpoint needs, or it is pinned to a different outlet.
429rate_limitedOver the key’s per-minute limit (60 for sf_test_, 300 for sf_live_). Wait the seconds in Retry-After.
404booking_not_foundNo such booking for this business or, for an outlet-pinned key, for that outlet.
404not_foundUnknown path under this endpoint.
500internal_errorSomething failed on our side. Quote the request_id when you report it.
Request
curl "https://ewpuclvmnygoddunkyrb.supabase.co/functions/v1/api-v1-bookings/e7a3c5d9-2b4f-4c6e-8d18-3f9a1b5c7e46" \
  -H "Authorization: Bearer $SANSFLOW_API_KEY"
Response200 OK
{
  "data": {
    "id": "e7a3c5d9-2b4f-4c6e-8d18-3f9a1b5c7e46",
    "business_id": "7c9e1a3b-5d7f-4b9a-8c2e-4f6a8b0c2d91",
    "location_id": "3f6c2a1e-8b4d-4c2a-9e51-7a0d2b6c9f10",
    "short_code": "7K2QM4X9PD3R",
    "status": "confirmed",
    "customer_name": "Budi",
    "customer_phone": "+6281300000000",
    "party_size": 4,
    "requested_at": "2026-11-08T12:00:00+00:00",
    "notes": "Window seat if possible",
    "requested_table": {
      "id": "0c2e4a6b-8d1f-4b3c-9e75-2a6c8e0b4d79",
      "name": "T12"
    },
    "payment_proof_uploaded_at": null,
    "preorder": null,
    "rejection_reason": null,
    "reservation_id": "d2f6b8a4-1c3e-4b5d-9a87-6e0f2c4b8d35",
    "reservation_code": "R-261108-003",
    "created_at": "2026-11-06T03:12:45+00:00",
    "updated_at": "2026-11-06T03:20:10+00:00",
    "reservation": {
      "id": "d2f6b8a4-1c3e-4b5d-9a87-6e0f2c4b8d35",
      "business_id": "7c9e1a3b-5d7f-4b9a-8c2e-4f6a8b0c2d91",
      "code": "R-261108-003",
      "status": "booked",
      "customer_id": "c4e8a2f1-6b9d-4e3a-8f72-1d5c9b0a3e24",
      "customer_name": "Budi",
      "customer_phone": "+6281300000000",
      "customer_email": null,
      "party_size": 4,
      "reserved_at": "2026-11-08T12:00:00+00:00",
      "duration_minutes": 90,
      "notes": null,
      "location_id": "3f6c2a1e-8b4d-4c2a-9e51-7a0d2b6c9f10",
      "table_id": "0c2e4a6b-8d1f-4b3c-9e75-2a6c8e0b4d79",
      "table": "T12",
      "booking_id": "e7a3c5d9-2b4f-4c6e-8d18-3f9a1b5c7e46",
      "booking_short_code": "7K2QM4X9PD3R",
      "created_at": "2026-11-06T03:20:10+00:00",
      "updated_at": "2026-11-06T03:20:10+00:00"
    }
  },
  "request_id": "5d0c7e2a-91f4-4b3e-a8c6-0f2e4b6d8a1c"
}

Example values. Run replays this response here; nothing is sent.

Retrieve availability

GET/api-v1-availability

The slot grid for one day with free-table counts, the same numbers the public booking page shows, plus the outlet’s booking rules.

free_tables is the active tables at the outlet minus every hold (booked or seated reservations and unresolved booking requests) overlapping the slot plus the default duration, capped by max_concurrent_bookings when limit_by_max_concurrent is on. With no tables defined it is max_concurrent_bookings minus the holds.

Slots inside the minimum notice are left out. When the day cannot be booked, bookable is false, slots is empty and reason is one of booking_disabled, day_not_bookable, date_in_past or beyond_max_advance. With booking_disabled, settings is null.

Query parameters

NameTypeRequiredDescription
datestringOptionalThe day, YYYY-MM-DD.Default: today in the business’s timezone.
location_iduuidOptionalThe outlet.Default: the key’s pinned outlet, otherwise the business’s default outlet.

Response fields (200)

NameTypeDescription
timezonestringThe business’s timezone; the day is cut in it.
bookablebooleanWhether the day takes bookings.
reasonstring | nullWhy not, when bookable is false.
slots[]arrayslot_at (UTC), free_tables, is_full.
settingsobject | nullThe outlet’s booking rules: days_of_week (0 = Sunday), hours, slot interval, default duration, notice, advance window, party size range, concurrency cap, table selection, deposit and expiry.

Errors

Every error answers { "error": { "code", "message", "request_id" } }. Branch on code; see Errors for retrying.

HTTPCodeWhen
400validation_failedA query parameter did not parse; the message names it. Also returned when from/to span more than 400 days.
401invalid_keyThe Authorization header is missing, malformed, or the key is revoked or inactive.
402payment_requiredThe business’s plan does not include API access (Sansflow Premium). The key itself is fine.
403insufficient_scopeThe key does not carry the scope this endpoint needs, or it is pinned to a different outlet.
429rate_limitedOver the key’s per-minute limit (60 for sf_test_, 300 for sf_live_). Wait the seconds in Retry-After.
404location_not_foundlocation_id is unknown or inactive for this business.
500internal_errorSomething failed on our side. Quote the request_id when you report it.
Request
curl -G "https://ewpuclvmnygoddunkyrb.supabase.co/functions/v1/api-v1-availability" \
  -H "Authorization: Bearer $SANSFLOW_API_KEY" \
  --data-urlencode "date=2026-11-08" \
  --data-urlencode "location_id=3f6c2a1e-8b4d-4c2a-9e51-7a0d2b6c9f10"
Response200 OK
{
  "data": {
    "date": "2026-11-08",
    "timezone": "Asia/Jakarta",
    "location_id": "3f6c2a1e-8b4d-4c2a-9e51-7a0d2b6c9f10",
    "bookable": true,
    "reason": null,
    "slots": [
      {
        "slot_at": "2026-11-08T11:00:00+00:00",
        "free_tables": 6,
        "is_full": false
      },
      {
        "slot_at": "2026-11-08T12:00:00+00:00",
        "free_tables": 0,
        "is_full": true
      }
    ],
    "settings": {
      "days_of_week": [
        0,
        1,
        2,
        3,
        4,
        5,
        6
      ],
      "open_time": "18:00:00",
      "close_time": "22:00:00",
      "slot_interval_minutes": 60,
      "default_duration_minutes": 90,
      "min_notice_hours": 2,
      "max_advance_days": 30,
      "min_party_size": 1,
      "max_party_size": 10,
      "max_concurrent_bookings": 8,
      "limit_by_max_concurrent": false,
      "table_selection_enabled": false,
      "deposit_required": false,
      "deposit_amount": 0,
      "pending_expiry_hours": 3
    }
  },
  "request_id": "5d0c7e2a-91f4-4b3e-a8c6-0f2e4b6d8a1c"
}

Example values. Run replays this response here; nothing is sent.

Confirm a booking

POST/api-v1-bookings/{booking_id}/confirm
Idempotent
Idempotency-Key

Confirms a pending booking request and creates its reservation, in one transaction, with the slot’s capacity re-checked.

In order: an already confirmed booking returns 200 with the same reservation and replay: true. A booking with a pre-order and no transfer proof is refused with proof_required. The slot’s free capacity is recomputed without this request’s own hold; if nothing is left the answer is capacity_full (unless allow_overbooking is true). An explicit table_id must be an active table at the outlet and free for the window. A table the guest only preferred that is no longer free is dropped, and the booking is confirmed without a table.

Two confirms for the same slot are serialised, so a slot cannot be over-booked through the API. Staff confirming from the Reservations page are not capacity-checked; treat capacity_full as “ask the venue”.

Two webhooks follow: reservation.created, then booking.confirmed.

Path parameters

NameTypeRequiredDescription
booking_iduuidRequiredThe booking request to confirm.

Headers

NameTypeRequiredDescription
Idempotency-KeystringRequiredA unique key per decision, usually a UUID.1-255 characters. Replays for 24 hours.

Request body

NameTypeRequiredDescription
table_iduuid | nullOptionalSeat the party at this table.
duration_minutesinteger | nullOptionalHow long to hold the table.15-600. Default: the outlet’s default duration.
allow_overbookingbooleanOptionalSkip the capacity check. A deliberate human override, echoed back as overbooked.Default false.

Response fields (200)

NameTypeDescription
dataobjectThe booking (as Retrieve a booking) with reservation.
data.replaybooleantrue when the booking was already confirmed and nothing changed.
data.overbookedbooleantrue when allow_overbooking was set.

Errors

Every error answers { "error": { "code", "message", "request_id" } }. Branch on code; see Errors for retrying.

HTTPCodeWhen
400invalid_requestThe id in the path is not a UUID.
400idempotency_key_requiredA POST without an Idempotency-Key header (1-255 characters).
400invalid_jsonThe POST body is not valid JSON.
422validation_failedThe body failed the schema (unknown fields too); error.details lists the fields.
401invalid_keyThe Authorization header is missing, malformed, or the key is revoked or inactive.
402payment_requiredThe business’s plan does not include API access (Sansflow Premium). The key itself is fine.
403insufficient_scopeThe key does not carry the scope this endpoint needs, or it is pinned to a different outlet.
429rate_limitedOver the key’s per-minute limit (60 for sf_test_, 300 for sf_live_). Wait the seconds in Retry-After.
409idempotency_key_conflictThe Idempotency-Key was already used with a different request.
404booking_not_foundNo such booking for this business or, for an outlet-pinned key, for that outlet.
404table_not_foundtable_id is not an active table at the booking’s outlet.
409booking_not_confirmableThe booking is rejected or expired; only pending bookings can be confirmed.
409proof_requiredThe booking has a pre-order and no transfer proof yet. It stays pending.
409capacity_fullThe slot has no free capacity left. The booking stays pending.
409table_takenThe named table already has a reservation overlapping that time.
404not_foundA path other than {booking_id}/confirm or {booking_id}/reject.
500internal_errorSomething failed on our side. Quote the request_id when you report it.
Request
curl -X POST "https://ewpuclvmnygoddunkyrb.supabase.co/functions/v1/api-v1-bookings/e7a3c5d9-2b4f-4c6e-8d18-3f9a1b5c7e46/confirm" \
  -H "Authorization: Bearer $SANSFLOW_API_KEY" \
  -H "Idempotency-Key: 8f1b3a2e-6c4d-4e9a-b2f7-1d3c5e7a9b0f" \
  -H "Content-Type: application/json" \
  -d '{
    "table_id": "0c2e4a6b-8d1f-4b3c-9e75-2a6c8e0b4d79"
  }'
Response200 OK
{
  "data": {
    "id": "e7a3c5d9-2b4f-4c6e-8d18-3f9a1b5c7e46",
    "business_id": "7c9e1a3b-5d7f-4b9a-8c2e-4f6a8b0c2d91",
    "location_id": "3f6c2a1e-8b4d-4c2a-9e51-7a0d2b6c9f10",
    "short_code": "7K2QM4X9PD3R",
    "status": "confirmed",
    "customer_name": "Budi",
    "customer_phone": "+6281300000000",
    "party_size": 4,
    "requested_at": "2026-11-08T12:00:00+00:00",
    "notes": "Window seat if possible",
    "requested_table": {
      "id": "0c2e4a6b-8d1f-4b3c-9e75-2a6c8e0b4d79",
      "name": "T12"
    },
    "payment_proof_uploaded_at": null,
    "preorder": null,
    "rejection_reason": null,
    "reservation_id": "d2f6b8a4-1c3e-4b5d-9a87-6e0f2c4b8d35",
    "reservation_code": "R-261108-003",
    "created_at": "2026-11-06T03:12:45+00:00",
    "updated_at": "2026-11-06T03:20:10+00:00",
    "reservation": {
      "id": "d2f6b8a4-1c3e-4b5d-9a87-6e0f2c4b8d35",
      "business_id": "7c9e1a3b-5d7f-4b9a-8c2e-4f6a8b0c2d91",
      "code": "R-261108-003",
      "status": "booked",
      "customer_id": "c4e8a2f1-6b9d-4e3a-8f72-1d5c9b0a3e24",
      "customer_name": "Budi",
      "customer_phone": "+6281300000000",
      "customer_email": null,
      "party_size": 4,
      "reserved_at": "2026-11-08T12:00:00+00:00",
      "duration_minutes": 90,
      "notes": null,
      "location_id": "3f6c2a1e-8b4d-4c2a-9e51-7a0d2b6c9f10",
      "table_id": "0c2e4a6b-8d1f-4b3c-9e75-2a6c8e0b4d79",
      "table": "T12",
      "booking_id": "e7a3c5d9-2b4f-4c6e-8d18-3f9a1b5c7e46",
      "booking_short_code": "7K2QM4X9PD3R",
      "created_at": "2026-11-06T03:20:10+00:00",
      "updated_at": "2026-11-06T03:20:10+00:00"
    },
    "replay": false,
    "overbooked": false
  },
  "request_id": "5d0c7e2a-91f4-4b3e-a8c6-0f2e4b6d8a1c"
}

Example values. Run replays this response here; nothing is sent.

Reject a booking

POST/api-v1-bookings/{booking_id}/reject
Idempotent
Idempotency-Key

Rejects a pending booking request with a reason the guest sees on their booking status page and that the booking.rejected webhook carries.

Rejecting an already rejected booking returns 200 with replay: true.

Path parameters

NameTypeRequiredDescription
booking_iduuidRequiredThe booking request to reject.

Headers

NameTypeRequiredDescription
Idempotency-KeystringRequiredA unique key per decision, usually a UUID.1-255 characters. Replays for 24 hours.

Request body

NameTypeRequiredDescription
reasonstringRequiredWhy, in words the guest can be shown.3-500 characters after trimming.

Errors

Every error answers { "error": { "code", "message", "request_id" } }. Branch on code; see Errors for retrying.

HTTPCodeWhen
400invalid_requestThe id in the path is not a UUID.
400idempotency_key_requiredA POST without an Idempotency-Key header (1-255 characters).
400invalid_jsonThe POST body is not valid JSON.
422validation_failedThe body failed the schema; error.details lists the fields.
401invalid_keyThe Authorization header is missing, malformed, or the key is revoked or inactive.
402payment_requiredThe business’s plan does not include API access (Sansflow Premium). The key itself is fine.
403insufficient_scopeThe key does not carry the scope this endpoint needs, or it is pinned to a different outlet.
429rate_limitedOver the key’s per-minute limit (60 for sf_test_, 300 for sf_live_). Wait the seconds in Retry-After.
409idempotency_key_conflictThe Idempotency-Key was already used with a different request.
404booking_not_foundNo such booking for this business or, for an outlet-pinned key, for that outlet.
409booking_not_rejectableThe booking is confirmed or expired; only pending bookings can be rejected.
500internal_errorSomething failed on our side. Quote the request_id when you report it.
Request
curl -X POST "https://ewpuclvmnygoddunkyrb.supabase.co/functions/v1/api-v1-bookings/e7a3c5d9-2b4f-4c6e-8d18-3f9a1b5c7e46/reject" \
  -H "Authorization: Bearer $SANSFLOW_API_KEY" \
  -H "Idempotency-Key: 4c0d9e71-2a3b-4f5c-8d6e-7f8091a2b3c4" \
  -H "Content-Type: application/json" \
  -d '{
    "reason": "Penuh untuk jam tersebut"
  }'
Response200 OK
{
  "data": {
    "id": "e7a3c5d9-2b4f-4c6e-8d18-3f9a1b5c7e46",
    "business_id": "7c9e1a3b-5d7f-4b9a-8c2e-4f6a8b0c2d91",
    "location_id": "3f6c2a1e-8b4d-4c2a-9e51-7a0d2b6c9f10",
    "short_code": "7K2QM4X9PD3R",
    "status": "rejected",
    "customer_name": "Budi",
    "customer_phone": "+6281300000000",
    "party_size": 4,
    "requested_at": "2026-11-08T12:00:00+00:00",
    "notes": "Window seat if possible",
    "requested_table": {
      "id": "0c2e4a6b-8d1f-4b3c-9e75-2a6c8e0b4d79",
      "name": "T12"
    },
    "payment_proof_uploaded_at": null,
    "preorder": null,
    "rejection_reason": "Penuh untuk jam tersebut",
    "reservation_id": null,
    "reservation_code": null,
    "created_at": "2026-11-06T03:12:45+00:00",
    "updated_at": "2026-11-06T03:21:40+00:00",
    "replay": false
  },
  "request_id": "5d0c7e2a-91f4-4b3e-a8c6-0f2e4b6d8a1c"
}

Example values. Run replays this response here; nothing is sent.

Base URL, keys and conventions: API reference overview.Events for this resource: Webhooks reference.

Stuck? Quote the request_id from the response and write toSansflow support.Checked against API v1 on .