API reference
Menu API
The menu as one outlet sells it: prices and availability resolved for that outlet. Only products enabled for online ordering with a fixed price are listed.
The response carries an ETag. Send it back as If-None-Match and an unchanged menu answers 304 Not Modified with no body. A 304 counts as an answered call.
Retrieve the menu
/api-v1-menu- Scope
menu.read- Plan
- Sansflow Premium
Categories, products with variants and modifier group ids, modifier groups with options, and combo slots for one outlet.
Unlike the other reads, the body is the menu itself, not wrapped in data, and it has no request_id field; the x-request-id header still carries it.
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
location_id | uuid | Required | The outlet whose menu you want. |
Headers
| Name | Type | Required | Description |
|---|---|---|---|
If-None-Match | string | Optional | The ETag from a previous response. |
Response fields (200)
| Name | Type | Description |
|---|---|---|
categories[] | array | id, name, sort_order. |
products[] | array | id, name, description, category (the category’s name), image_url, price (the outlet’s price), is_available, variants[] { id, name, price }, modifier_groups (ids). |
modifier_groups[] | array | id, name, required, min_select, max_select, options[] { id, name, price }. |
combos[] | array | combo_product_id, slot_name, component_product_id, quantity. |
generated_at | timestamp | When this copy was built. Not part of the ETag. |
Errors
Every error answers { "error": { "code", "message", "request_id" } }. Branch on code; see Errors for retrying.
| HTTP | Code | When |
|---|---|---|
| 400 | invalid_request | location_id is missing. |
| 400 | validation_failed | location_id 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 | 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-menu" \
-H "Authorization: Bearer $SANSFLOW_API_KEY" \
--data-urlencode "location_id=3f6c2a1e-8b4d-4c2a-9e51-7a0d2b6c9f10"const url = new URL("https://ewpuclvmnygoddunkyrb.supabase.co/functions/v1/api-v1-menu");
url.search = new URLSearchParams({
"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-menu",
headers={
"Authorization": f"Bearer {os.environ['SANSFLOW_API_KEY']}",
},
params={
"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)Headers: ETag: "sha256:…", Cache-Control: private, max-age=0, must-revalidate
GET /api-v1-menu
{
"location_id": "3f6c2a1e-8b4d-4c2a-9e51-7a0d2b6c9f10",
"categories": [
{
"id": "8a0c2e4f-6b1d-4f7a-9c38-2e4a6c8e0f92",
"name": "Kopi",
"sort_order": 0
}
],
"products": [
{
"id": "f1b3d5a7-9c2e-4a6b-8d14-7e3c5a9b1d68",
"name": "Kopi Susu",
"description": null,
"category": "Kopi",
"image_url": null,
"price": 25000,
"is_available": true,
"variants": [
{
"id": "6e8a0c2d-4f1b-4d5e-8a96-0c2e4a6b8d81",
"name": "Iced",
"price": 27000
}
],
"modifier_groups": [
"8a0c2e4f-6b1d-4f7a-9c38-2e4a6c8e0f92"
]
}
],
"modifier_groups": [
{
"id": "8a0c2e4f-6b1d-4f7a-9c38-2e4a6c8e0f92",
"name": "Extra",
"required": false,
"min_select": 0,
"max_select": 2,
"options": [
{
"id": "9b1d3f5a-7c2e-4a8b-8d49-3f5b7d9f1a03",
"name": "Extra shot",
"price": 3000
}
]
}
],
"combos": [
{
"combo_product_id": "2d4f6b8c-0e1a-4c3d-9f57-8b9d1e3f5a70",
"slot_name": "Drink",
"component_product_id": "f1b3d5a7-9c2e-4a6b-8d14-7e3c5a9b1d68",
"quantity": 1
}
],
"generated_at": "2026-11-08T03:00:00Z"
}Example values. Run replays this response here; nothing is sent.
Base URL, keys and conventions: API reference overview.
Stuck? Quote the request_id from the response and write toSansflow support.Checked against API v1 on .