Concepts
Pagination
List endpoints page with an opaque cursor. Send back the cursor you were given until there are no more pages; rows never repeat or go missing, even while new sales come in.
The list envelope
Every list endpoint (transactions, customers, reservations, bookings) answers the same shape:
{
"data": [ … ],
"has_more": true,
"next_cursor": "MjAyNi0wOS0xOFQxMjo0MTowMy…",
"request_id": "…"
}| Parameter | Meaning |
|---|---|
limit | Rows per page, 1 to 100. Default 50. Values above 100 are capped at 100. |
cursor | The next_cursor from the previous page. Leave it out for the first page. |
Paging through a list
Keep the same filters (from, to, location_id, status…) on every page and add cursor. Stop when has_more is false; next_cursor is then null.
# first page
curl "https://ewpuclvmnygoddunkyrb.supabase.co/functions/v1/api-v1-transactions?from=2026-09-01&to=2026-09-30&limit=100" \
-H "Authorization: Bearer $SANSFLOW_API_KEY"
# next page: send next_cursor back as cursor, same filters
curl "https://ewpuclvmnygoddunkyrb.supabase.co/functions/v1/api-v1-transactions?from=2026-09-01&to=2026-09-30&limit=100&cursor=MjAyNi0wOS0xOFQxMjo0MTowMy…" \
-H "Authorization: Bearer $SANSFLOW_API_KEY"async function* transactions(params) {
let cursor = null;
do {
const q = new URLSearchParams({ ...params, limit: "100", ...(cursor ? { cursor } : {}) });
const res = await fetch(`https://ewpuclvmnygoddunkyrb.supabase.co/functions/v1/api-v1-transactions?${q}`, {
headers: { Authorization: `Bearer ${process.env.SANSFLOW_API_KEY}` },
});
const page = await res.json();
if (!res.ok) throw new Error(page.error.code);
yield* page.data;
cursor = page.has_more ? page.next_cursor : null;
} while (cursor);
}
for await (const tx of transactions({ from: "2026-09-01", to: "2026-09-30" })) {
console.log(tx.number, tx.totals.net);
}import os, requests
def transactions(**params):
cursor = None
while True:
q = {**params, "limit": 100, **({"cursor": cursor} if cursor else {})}
res = requests.get(
"https://ewpuclvmnygoddunkyrb.supabase.co/functions/v1/api-v1-transactions",
params=q,
headers={"Authorization": f"Bearer {os.environ['SANSFLOW_API_KEY']}"},
timeout=10,
)
page = res.json()
res.raise_for_status()
yield from page["data"]
if not page["has_more"]:
break
cursor = page["next_cursor"]
for tx in transactions(**{"from": "2026-09-01", "to": "2026-09-30"}):
print(tx["number"], tx["totals"]["net"])The cursor is opaque: don't build or change one. It is URL-safe, so you can put it in a query string as it is. A cursor that was not issued by the API answers 400 validation_failed.
Ordering
Lists are newest first on their sort column, with the row id as a tie-breaker:
| Endpoint | Sorted by | from / to filter |
|---|---|---|
/api-v1-transactions | created_at | created_at, at most 400 days apart |
/api-v1-customers | created_at | (search instead) |
/api-v1-reservations | reserved_at | reserved_at, the time the guest arrives |
/api-v1-bookings | created_at | requested_at |
Because paging is by key rather than by offset, a sale that lands while you iterate does not shift the pages: you never see a row twice or skip one. To pick up new rows later, start again without a cursor, or listen for webhooks.
Stuck? Quote the request_id from the response and write toSansflow support.Checked against API v1 on .