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.
- GET List bookings
- GET Retrieve a booking
- GET Retrieve availability
- POST Confirm a booking
- POST Reject a booking
List bookings
/api-v1-bookingsReturns booking requests, newest request first. Poll with status=awaiting_confirmation, or subscribe to the booking.requested webhook and skip polling.
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
status | string | Optional | One of pending_payment, awaiting_confirmation, confirmed, rejected, expired. |
from | string | Optional | Earliest 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. |
to | string | Optional | Latest 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_id | uuid | Optional | Only this outlet. A key pinned to one outlet is always scoped to it; another outlet’s id is 403. |
cursor | string | Optional | The next_cursor from the previous page. Opaque; send it back unchanged. |
limit | integer | Optional | Rows per page.Default 50. At least 1; values above 100 are capped at 100. |
Response fields (200)
| Name | Type | Description |
|---|---|---|
data[].id | uuid | The booking request. |
data[].business_id | uuid | The business it belongs to. |
data[].location_id | uuid | The outlet it was made for. |
data[].short_code | string | The code in the guest’s booking link. |
data[].status | string | pending_payment, awaiting_confirmation, confirmed, rejected or expired. |
data[].customer_name | string | The guest’s name as typed. |
data[].customer_phone | string | The guest’s phone, as +62…. |
data[].party_size | integer | Guests. |
data[].requested_at | timestamp | The slot the guest asked for. |
data[].notes | string | null | The guest’s note. |
data[].requested_table | object | null | { id, name } when the guest picked a table. |
data[].payment_proof_uploaded_at | timestamp | null | When the guest uploaded a transfer proof. |
data[].preorder | object | 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_reason | string | null | Set when rejected; the guest sees it. |
data[].reservation_id | uuid | null | The reservation once confirmed. |
data[].reservation_code | string | null | Its code, R-YYMMDD-NNN. |
data[].created_at | timestamp | When the guest submitted it. |
data[].updated_at | timestamp | Last change. |
Errors
Every error answers { "error": { "code", "message", "request_id" } }. Branch on code; see Errors for retrying.
| HTTP | Code | When |
|---|---|---|
| 400 | validation_failed | A query parameter did not parse; the message names it. Also returned when from/to span more than 400 days. |
| 401 | invalid_key | The Authorization header is missing, malformed, or the key is revoked or inactive. |
| 402 | payment_required | The business’s plan does not include API access (Sansflow Premium). The key itself is fine. |
| 403 | insufficient_scope | The key does not carry the scope this endpoint needs, or it is pinned to a different outlet. |
| 429 | rate_limited | Over the key’s per-minute limit (60 for sf_test_, 300 for sf_live_). Wait the seconds in Retry-After. |
| 500 | internal_error | Something failed on our side. Quote the request_id when you report it. |
curl -G "https://ewpuclvmnygoddunkyrb.supabase.co/functions/v1/api-v1-bookings" \
-H "Authorization: Bearer $SANSFLOW_API_KEY" \
--data-urlencode "status=awaiting_confirmation"const url = new URL("https://ewpuclvmnygoddunkyrb.supabase.co/functions/v1/api-v1-bookings");
url.search = new URLSearchParams({
"status": "awaiting_confirmation"
}).toString();
const res = await fetch(
url,
{
headers: {
Authorization: `Bearer ${process.env.SANSFLOW_API_KEY}`,
},
},
);
const json = await res.json();
if (!res.ok) throw new Error(`${json.error.code} (${json.error.request_id})`);
console.log(json);import os
import requests
res = requests.get(
"https://ewpuclvmnygoddunkyrb.supabase.co/functions/v1/api-v1-bookings",
headers={
"Authorization": f"Bearer {os.environ['SANSFLOW_API_KEY']}",
},
params={
"status": "awaiting_confirmation",
},
timeout=30,
)
data = res.json()
if not res.ok:
raise RuntimeError(f"{data['error']['code']} ({data['error']['request_id']})")
print(data)GET /api-v1-bookings
{
"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
/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
| Name | Type | Required | Description |
|---|---|---|---|
booking_id | uuid | Required | The booking request’s id. |
Response fields (200)
| Name | Type | Description |
|---|---|---|
id | uuid | The booking request. |
business_id | uuid | The business it belongs to. |
location_id | uuid | The outlet it was made for. |
short_code | string | The code in the guest’s booking link. |
status | string | pending_payment, awaiting_confirmation, confirmed, rejected or expired. |
reservation | object | Present 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.
| HTTP | Code | When |
|---|---|---|
| 400 | invalid_request | The id in the path is not a UUID. |
| 401 | invalid_key | The Authorization header is missing, malformed, or the key is revoked or inactive. |
| 402 | payment_required | The business’s plan does not include API access (Sansflow Premium). The key itself is fine. |
| 403 | insufficient_scope | The key does not carry the scope this endpoint needs, or it is pinned to a different outlet. |
| 429 | rate_limited | Over the key’s per-minute limit (60 for sf_test_, 300 for sf_live_). Wait the seconds in Retry-After. |
| 404 | booking_not_found | No such booking for this business or, for an outlet-pinned key, for that outlet. |
| 404 | not_found | Unknown path under this endpoint. |
| 500 | internal_error | Something failed on our side. Quote the request_id when you report it. |
curl "https://ewpuclvmnygoddunkyrb.supabase.co/functions/v1/api-v1-bookings/e7a3c5d9-2b4f-4c6e-8d18-3f9a1b5c7e46" \
-H "Authorization: Bearer $SANSFLOW_API_KEY"const res = await fetch(
"https://ewpuclvmnygoddunkyrb.supabase.co/functions/v1/api-v1-bookings/e7a3c5d9-2b4f-4c6e-8d18-3f9a1b5c7e46",
{
headers: {
Authorization: `Bearer ${process.env.SANSFLOW_API_KEY}`,
},
},
);
const json = await res.json();
if (!res.ok) throw new Error(`${json.error.code} (${json.error.request_id})`);
console.log(json);import os
import requests
res = requests.get(
"https://ewpuclvmnygoddunkyrb.supabase.co/functions/v1/api-v1-bookings/e7a3c5d9-2b4f-4c6e-8d18-3f9a1b5c7e46",
headers={
"Authorization": f"Bearer {os.environ['SANSFLOW_API_KEY']}",
},
timeout=30,
)
data = res.json()
if not res.ok:
raise RuntimeError(f"{data['error']['code']} ({data['error']['request_id']})")
print(data)GET /api-v1-bookings/{booking_id}
{
"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
/api-v1-availabilityThe 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
| Name | Type | Required | Description |
|---|---|---|---|
date | string | Optional | The day, YYYY-MM-DD.Default: today in the business’s timezone. |
location_id | uuid | Optional | The outlet.Default: the key’s pinned outlet, otherwise the business’s default outlet. |
Response fields (200)
| Name | Type | Description |
|---|---|---|
timezone | string | The business’s timezone; the day is cut in it. |
bookable | boolean | Whether the day takes bookings. |
reason | string | null | Why not, when bookable is false. |
slots[] | array | slot_at (UTC), free_tables, is_full. |
settings | object | null | The 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.
| HTTP | Code | When |
|---|---|---|
| 400 | validation_failed | A query parameter did not parse; the message names it. Also returned when from/to span more than 400 days. |
| 401 | invalid_key | The Authorization header is missing, malformed, or the key is revoked or inactive. |
| 402 | payment_required | The business’s plan does not include API access (Sansflow Premium). The key itself is fine. |
| 403 | insufficient_scope | The key does not carry the scope this endpoint needs, or it is pinned to a different outlet. |
| 429 | rate_limited | Over the key’s per-minute limit (60 for sf_test_, 300 for sf_live_). Wait the seconds in Retry-After. |
| 404 | location_not_found | location_id is unknown or inactive for this business. |
| 500 | internal_error | Something failed on our side. Quote the request_id when you report it. |
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"const url = new URL("https://ewpuclvmnygoddunkyrb.supabase.co/functions/v1/api-v1-availability");
url.search = new URLSearchParams({
"date": "2026-11-08",
"location_id": "3f6c2a1e-8b4d-4c2a-9e51-7a0d2b6c9f10"
}).toString();
const res = await fetch(
url,
{
headers: {
Authorization: `Bearer ${process.env.SANSFLOW_API_KEY}`,
},
},
);
const json = await res.json();
if (!res.ok) throw new Error(`${json.error.code} (${json.error.request_id})`);
console.log(json);import os
import requests
res = requests.get(
"https://ewpuclvmnygoddunkyrb.supabase.co/functions/v1/api-v1-availability",
headers={
"Authorization": f"Bearer {os.environ['SANSFLOW_API_KEY']}",
},
params={
"date": "2026-11-08",
"location_id": "3f6c2a1e-8b4d-4c2a-9e51-7a0d2b6c9f10",
},
timeout=30,
)
data = res.json()
if not res.ok:
raise RuntimeError(f"{data['error']['code']} ({data['error']['request_id']})")
print(data)GET /api-v1-availability
{
"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
/api-v1-bookings/{booking_id}/confirmConfirms 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
| Name | Type | Required | Description |
|---|---|---|---|
booking_id | uuid | Required | The booking request to confirm. |
Headers
| Name | Type | Required | Description |
|---|---|---|---|
Idempotency-Key | string | Required | A unique key per decision, usually a UUID.1-255 characters. Replays for 24 hours. |
Request body
| Name | Type | Required | Description |
|---|---|---|---|
table_id | uuid | null | Optional | Seat the party at this table. |
duration_minutes | integer | null | Optional | How long to hold the table.15-600. Default: the outlet’s default duration. |
allow_overbooking | boolean | Optional | Skip the capacity check. A deliberate human override, echoed back as overbooked.Default false. |
Response fields (200)
| Name | Type | Description |
|---|---|---|
data | object | The booking (as Retrieve a booking) with reservation. |
data.replay | boolean | true when the booking was already confirmed and nothing changed. |
data.overbooked | boolean | true when allow_overbooking was set. |
Errors
Every error answers { "error": { "code", "message", "request_id" } }. Branch on code; see Errors for retrying.
| HTTP | Code | When |
|---|---|---|
| 400 | invalid_request | The id in the path is not a UUID. |
| 400 | idempotency_key_required | A POST without an Idempotency-Key header (1-255 characters). |
| 400 | invalid_json | The POST body is not valid JSON. |
| 422 | validation_failed | The body failed the schema (unknown fields too); error.details lists the fields. |
| 401 | invalid_key | The Authorization header is missing, malformed, or the key is revoked or inactive. |
| 402 | payment_required | The business’s plan does not include API access (Sansflow Premium). The key itself is fine. |
| 403 | insufficient_scope | The key does not carry the scope this endpoint needs, or it is pinned to a different outlet. |
| 429 | rate_limited | Over the key’s per-minute limit (60 for sf_test_, 300 for sf_live_). Wait the seconds in Retry-After. |
| 409 | idempotency_key_conflict | The Idempotency-Key was already used with a different request. |
| 404 | booking_not_found | No such booking for this business or, for an outlet-pinned key, for that outlet. |
| 404 | table_not_found | table_id is not an active table at the booking’s outlet. |
| 409 | booking_not_confirmable | The booking is rejected or expired; only pending bookings can be confirmed. |
| 409 | proof_required | The booking has a pre-order and no transfer proof yet. It stays pending. |
| 409 | capacity_full | The slot has no free capacity left. The booking stays pending. |
| 409 | table_taken | The named table already has a reservation overlapping that time. |
| 404 | not_found | A path other than {booking_id}/confirm or {booking_id}/reject. |
| 500 | internal_error | Something failed on our side. Quote the request_id when you report it. |
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"
}'const res = await fetch(
"https://ewpuclvmnygoddunkyrb.supabase.co/functions/v1/api-v1-bookings/e7a3c5d9-2b4f-4c6e-8d18-3f9a1b5c7e46/confirm",
{
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SANSFLOW_API_KEY}`,
"Idempotency-Key": "8f1b3a2e-6c4d-4e9a-b2f7-1d3c5e7a9b0f",
"Content-Type": "application/json",
},
body: JSON.stringify({
"table_id": "0c2e4a6b-8d1f-4b3c-9e75-2a6c8e0b4d79"
}),
},
);
const json = await res.json();
if (!res.ok) throw new Error(`${json.error.code} (${json.error.request_id})`);
console.log(json);import os
import requests
res = requests.post(
"https://ewpuclvmnygoddunkyrb.supabase.co/functions/v1/api-v1-bookings/e7a3c5d9-2b4f-4c6e-8d18-3f9a1b5c7e46/confirm",
headers={
"Authorization": f"Bearer {os.environ['SANSFLOW_API_KEY']}",
"Idempotency-Key": "8f1b3a2e-6c4d-4e9a-b2f7-1d3c5e7a9b0f",
},
json={
"table_id": "0c2e4a6b-8d1f-4b3c-9e75-2a6c8e0b4d79"
},
timeout=30,
)
data = res.json()
if not res.ok:
raise RuntimeError(f"{data['error']['code']} ({data['error']['request_id']})")
print(data)POST /api-v1-bookings/{booking_id}/confirm
{
"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
/api-v1-bookings/{booking_id}/rejectRejects 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
| Name | Type | Required | Description |
|---|---|---|---|
booking_id | uuid | Required | The booking request to reject. |
Headers
| Name | Type | Required | Description |
|---|---|---|---|
Idempotency-Key | string | Required | A unique key per decision, usually a UUID.1-255 characters. Replays for 24 hours. |
Request body
| Name | Type | Required | Description |
|---|---|---|---|
reason | string | Required | Why, 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.
| HTTP | Code | When |
|---|---|---|
| 400 | invalid_request | The id in the path is not a UUID. |
| 400 | idempotency_key_required | A POST without an Idempotency-Key header (1-255 characters). |
| 400 | invalid_json | The POST body is not valid JSON. |
| 422 | validation_failed | The body failed the schema; error.details lists the fields. |
| 401 | invalid_key | The Authorization header is missing, malformed, or the key is revoked or inactive. |
| 402 | payment_required | The business’s plan does not include API access (Sansflow Premium). The key itself is fine. |
| 403 | insufficient_scope | The key does not carry the scope this endpoint needs, or it is pinned to a different outlet. |
| 429 | rate_limited | Over the key’s per-minute limit (60 for sf_test_, 300 for sf_live_). Wait the seconds in Retry-After. |
| 409 | idempotency_key_conflict | The Idempotency-Key was already used with a different request. |
| 404 | booking_not_found | No such booking for this business or, for an outlet-pinned key, for that outlet. |
| 409 | booking_not_rejectable | The booking is confirmed or expired; only pending bookings can be rejected. |
| 500 | internal_error | Something failed on our side. Quote the request_id when you report it. |
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"
}'const res = await fetch(
"https://ewpuclvmnygoddunkyrb.supabase.co/functions/v1/api-v1-bookings/e7a3c5d9-2b4f-4c6e-8d18-3f9a1b5c7e46/reject",
{
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SANSFLOW_API_KEY}`,
"Idempotency-Key": "4c0d9e71-2a3b-4f5c-8d6e-7f8091a2b3c4",
"Content-Type": "application/json",
},
body: JSON.stringify({
"reason": "Penuh untuk jam tersebut"
}),
},
);
const json = await res.json();
if (!res.ok) throw new Error(`${json.error.code} (${json.error.request_id})`);
console.log(json);import os
import requests
res = requests.post(
"https://ewpuclvmnygoddunkyrb.supabase.co/functions/v1/api-v1-bookings/e7a3c5d9-2b4f-4c6e-8d18-3f9a1b5c7e46/reject",
headers={
"Authorization": f"Bearer {os.environ['SANSFLOW_API_KEY']}",
"Idempotency-Key": "4c0d9e71-2a3b-4f5c-8d6e-7f8091a2b3c4",
},
json={
"reason": "Penuh untuk jam tersebut"
},
timeout=30,
)
data = res.json()
if not res.ok:
raise RuntimeError(f"{data['error']['code']} ({data['error']['request_id']})")
print(data)POST /api-v1-bookings/{booking_id}/reject
{
"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 .