Complete examples
Each of these scripts is complete and runs as it stands. Both read every page, retry the errors that are worth retrying and stop with a message on any other.
Total a day's sales (Node.js)
Adds up one business date from the raw orders and refunds, then compares the result with the daily summary. Needs the read:reports permission.
day.mjs
// Totals one business date from the raw orders and refunds, and checks the
// result against the daily summary. Node.js 18 or later, no dependencies.
//
// SEAMLESS_API_KEY=k_live_... node day.mjs 2026-11-01
const ENDPOINT = "https://api.seamlesspos.com/graphql";
const RETRY = new Set(["RATE_LIMITED", "UNAVAILABLE", "INTERNAL_SERVER_ERROR"]);
// Sends one query. Retries the errors worth retrying, waiting longer each time.
async function seamless(query, variables = {}) {
for (let attempt = 0; ; attempt++) {
const res = await fetch(ENDPOINT, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SEAMLESS_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ query, variables }),
});
const body = await res.json();
if (!body.errors) return body.data;
const { message, extensions } = body.errors[0];
if (!RETRY.has(extensions?.code) || attempt === 4) {
throw new Error(`${extensions?.code}: ${message}`);
}
await new Promise(resolve => setTimeout(resolve, 1000 * 2 ** attempt));
}
}
// Reads every page of `orders` or `refunds`.
async function allPages(field, selection, from, to) {
const query = `query Page($from: DateTime!, $to: DateTime!, $after: String) {
site { ${field}(from: $from, to: $to, first: 200, after: $after) { nodes { ${selection} } nextCursor } }
}`;
const records = [];
let after = null;
do {
const data = await seamless(query, { from, to, after });
records.push(...data.site[field].nodes);
after = data.site[field].nextCursor;
} while (after);
return records;
}
const date = process.argv[2];
if (!/^\d{4}-\d{2}-\d{2}$/.test(date ?? "")) throw new Error("Pass a date, like 2026-11-01");
// A business date does not line up with midnight UTC, so read a day either
// side and keep the records whose businessDate is the one wanted.
const day = 24 * 60 * 60 * 1000;
const from = new Date(Date.parse(`${date}T00:00:00Z`) - day).toISOString();
const to = new Date(Date.parse(`${date}T00:00:00Z`) + 2 * day).toISOString();
const orders = (await allPages("orders", "businessDate total voided", from, to))
.filter(o => o.businessDate === date && !o.voided);
const refunds = (await allPages("refunds", "businessDate amount", from, to))
.filter(r => r.businessDate === date);
const sales = orders.reduce((sum, o) => sum + (o.total ?? 0), 0);
const refunded = refunds.reduce((sum, r) => sum + (r.amount ?? 0), 0);
const { site } = await seamless(
`query Summary($date: Date!) { site { currency dailySummary(from: $date, to: $date) { gross transactions } } }`,
{ date },
);
const summary = site.dailySummary[0];
const money = pence => `${(pence / 100).toFixed(2)} ${site.currency}`;
console.log(`${date}: ${orders.length} orders, ${refunds.length} refunds`);
console.log(`Sales ${money(sales)} less refunds ${money(refunded)} = ${money(sales - refunded)}`);
console.log(`Daily summary gross: ${money(summary?.gross ?? 0)}`);
Output
2026-11-01: 143 orders, 2 refunds
Sales 1867.40 GBP less refunds 24.90 GBP = 1842.50 GBP
Daily summary gross: 1842.50 GBP
Export the product library (Python)
Writes every product and price to a CSV file. Needs the read:products permission.
products.py
"""Writes the whole product library to products.csv. Python 3.8 or later.
pip install requests
SEAMLESS_API_KEY=k_live_... python products.py
"""
import csv
import os
import time
import requests
ENDPOINT = "https://api.seamlesspos.com/graphql"
RETRY = {"RATE_LIMITED", "UNAVAILABLE", "INTERNAL_SERVER_ERROR"}
def seamless(query, variables=None):
"""Sends one query. Retries the errors worth retrying, waiting longer each time."""
for attempt in range(5):
res = requests.post(
ENDPOINT,
headers={"Authorization": f"Bearer {os.environ['SEAMLESS_API_KEY']}"},
json={"query": query, "variables": variables or {}},
timeout=30,
)
body = res.json()
if not body.get("errors"):
return body["data"]
error = body["errors"][0]
code = error.get("extensions", {}).get("code")
if code not in RETRY or attempt == 4:
raise RuntimeError(f"{code}: {error['message']}")
time.sleep(2 ** attempt)
QUERY = """
query Products($after: String) {
site {
products(first: 200, after: $after) {
nodes {
id
name
category { name }
prices { amount orderMode variant taxRatePercent }
}
nextCursor
}
}
}
"""
products, after = [], None
while True:
page = seamless(QUERY, {"after": after})["site"]["products"]
products.extend(page["nodes"])
after = page["nextCursor"]
if not after:
break
with open("products.csv", "w", newline="") as f:
out = csv.writer(f)
out.writerow(["id", "name", "category", "order mode", "variant", "price", "tax rate %"])
for product in products:
for price in product["prices"]:
out.writerow([
product["id"],
product["name"],
(product["category"] or {}).get("name"),
price["orderMode"],
price["variant"],
None if price["amount"] is None else f"{price['amount'] / 100:.2f}",
price["taxRatePercent"],
])
print(f"Wrote {len(products)} products to products.csv")
Output
Wrote 408 products to products.csv
products.csv
id,name,category,order mode,variant,price,tax rate %
66168c952dacadbfc5e41576,Lemonade,Soft Drinks,Sit In,,3.50,20
66168c952dacadbfc5e41576,Lemonade,Soft Drinks,Take Out,,3.20,20
66336eae98f4389e8af4dcc2,Steak,Hot Food,Sit In,,25.00,20