The Flint Sync API allows Android/native clients and third-party systems to fetch product catalogs, submit transactions, and retrieve configuration. All endpoints require a Bearer token.
Include this header on every request:
Your SYNC_API_KEY is set in Dashboard → Settings → Environment Variables.
Live Endpoint
All paths are passed as a ?path= query parameter. For example:
This URL is publicly reachable — your Android app can call it directly without any login or web session. The SYNC_API_KEY in the Authorization header is the only security layer.
Customers scan a QR code at their table and place orders directly from their phone. Orders are submitted to a dedicated Online Order Till (configured in Settings → Tills → mark till as is_online_order_till: true). The Android till polling that till's relay commands will receive the order automatically — no extra integration needed.
/order/<venueId>/<tableNumber> in browser.submitOnlineOrder.Transaction (status: pending, payment_method: other, reference: ONLINE-T{tableNumber}) and a RelayCommand (POST /api/v1/transactions) targeting the venue's online order till.POST /api/v1/transactions) — this triggers the till's normal new-order flow (print kitchen ticket, show on display, etc.).?path=commands/result.The till does not need any special handling — it receives online orders exactly like any other relay command. The only requirement is that is_online_order_till is set to true on that till in Settings.
{
"uuid": "txn_abc123",
"items": [
{ "product_name": "Hendricks Gin", "quantity": 2, "unit_price": 4.50, "tax_rate": 20, "line_total": 9.00 }
],
"subtotal": 9.00,
"tax_total": 0,
"total": 9.00,
"payment_method": "other",
"status": "pending",
"notes": "Table 4 — Pay at counter",
"reference": "ONLINE-T4"
}Configurators define guided selection flows attached to products (e.g. cook temperature, meal deal choices). Each step can contain product-based options (linked to a real product) or text-only options (e.g. "Rare", "Medium", "Well Done") — no product required.
When submitting a transaction that includes configurator choices, add a configurator_selections array to the relevant item. Use custom_description for text-only options, or product_id + product_name for product-based options.
{
"items": [
{
"product_id": "prod_steak",
"product_name": "8oz Sirloin Steak",
"quantity": 1,
"unit_price": 22.00,
"tax_rate": 20,
"line_total": 22.00,
"configurator_selections": [
// Text-only option (no product_id needed)
{ "step_name": "Temperature", "custom_description": "Medium Rare", "price": 0 },
// Product-based option
{ "step_name": "Sauce", "product_id": "prod_peppercorn", "product_name": "Peppercorn Sauce", "price": 2.50 }
]
}
],
"subtotal": 24.50,
"tax_total": 4.08,
"total": 24.50
}When venue.multiple_locations_enabled is true (returned in GET ?path=config), products may carry per-location overrides in their location_details array. The till must apply the correct override for its assigned location_id.
location_details where location_id matches the till's location_id.is_active === false for that entry → hide the product entirely at this location.price, tax_rate, cost_price, stock_count, printer_station_ids, selling_variations): if the location value is non-null / non-empty, use it. Otherwise fall back to the global product value.location_details entry exists → use global product defaults as normal.multiple_locations_enabled is false → ignore location_details entirely and use global defaults.// Kotlin helper — resolve effective product values for this till's location
fun resolveProduct(product: Product, locationId: String?, multiLocationEnabled: Boolean): ResolvedProduct {
if (!multiLocationEnabled || locationId == null) {
return ResolvedProduct.fromGlobal(product)
}
val ld = product.locationDetails.find { it.locationId == locationId }
?: return ResolvedProduct.fromGlobal(product) // no override → use global
if (ld.isActive == false) return ResolvedProduct.hidden() // hidden at this location
return ResolvedProduct(
price = ld.price ?: product.standardUnit?.price,
taxRate = ld.taxRate ?: product.standardUnit?.taxRate ?: 20.0,
costPrice = ld.costPrice ?: product.standardUnit?.costPrice,
stockCount = ld.stockCount ?: product.standardUnit?.stockCount,
printerStationIds = ld.printerStationIds.ifEmpty { product.printerStationIds },
sellingVariations = ld.sellingVariations.ifEmpty { product.sellingVariations },
isActive = true
)
}Bad Request
Missing or invalid parameters
Unauthorized
Missing or invalid API key
Not Found
Till ID or resource doesn't exist
Server Error
Unexpected internal error
{ "error": "error message here" }val apiKey = "your_sync_api_key"
val baseUrl = "https://flint-backoffice.base44.app/api/functions/syncApi"
val tillId = "abc123"
// 1. Boot — fetch till/venue/printer config
val config = get("$baseUrl?path=config&till_id=$tillId")
// config.data.till.assigned_printer_id → ID of the receipt printer
// config.data.printers → array of printers with connection_address + printer_model
// config.data.printer_stations → logical stations (Kitchen, Bar, Receipt)
// config.data.till.printer_routing → maps station IDs to printer IDs for this till
// 2. Sync full product catalogue
val products = get("$baseUrl?path=products&venue_id=${config.data.venue.id}")
// 3. Sync staff users (for PIN login)
val users = get("$baseUrl?path=users&venue_id=${config.data.venue.id}")
// users.data[].pos_pin → use for staff PIN authentication
// ─── Normal sale ──────────────────────────────────────────────────────────
val saleBody = """
{
"client_txn_id": "${UUID.randomUUID()}",
"till_id": "$tillId",
"transaction_type": "sale",
"items": [
{ "product_name": "Hendricks Gin", "quantity": 2,
"unit_price": 4.50, "tax_rate": 20, "line_total": 9.00 }
],
"subtotal": 9.00, "tax_total": 1.50, "total": 9.00,
"payment_method": "card", "status": "approved", "cashier_name": "John"
}""".toRequestBody("application/json".toMediaType())
post("$baseUrl?path=transactions", saleBody)
// ─── No Sale (cash drawer opened without a sale) ───────────────────────────
val noSaleBody = """
{
"client_txn_id": "${UUID.randomUUID()}",
"till_id": "$tillId",
"transaction_type": "no_sale",
"items": [], "subtotal": 0, "tax_total": 0, "total": 0,
"payment_method": "none", "status": "approved", "cashier_name": "John"
}""".toRequestBody("application/json".toMediaType())
post("$baseUrl?path=transactions", noSaleBody)
// ─── Cancelled order (started but cancelled before payment) ─────────────────
val cancelBody = """
{
"client_txn_id": "${UUID.randomUUID()}",
"till_id": "$tillId",
"transaction_type": "cancelled",
"items": [
{ "product_name": "Peroni", "quantity": 1, "unit_price": 5.00, "tax_rate": 20, "line_total": 5.00 }
],
"subtotal": 5.00, "tax_total": 0.83, "total": 5.00,
"payment_method": "none", "status": "approved", "cashier_name": "John"
}""".toRequestBody("application/json".toMediaType())
post("$baseUrl?path=transactions", cancelBody)
// ─── Bulk offline sync (all types supported) ────────────────────────────────
val bulkBody = """
{
"transactions": [
{ "client_txn_id": "uuid-1", "till_id": "$tillId", "transaction_type": "sale",
"items": [...], "total": 9.00, "payment_method": "cash", "status": "approved" },
{ "client_txn_id": "uuid-2", "till_id": "$tillId", "transaction_type": "no_sale",
"items": [], "total": 0, "payment_method": "none", "status": "approved" },
{ "client_txn_id": "uuid-3", "till_id": "$tillId", "transaction_type": "cancelled",
"items": [...], "total": 5.00, "payment_method": "none", "status": "approved" }
]
}""".toRequestBody("application/json".toMediaType())
val bulkResult = post("$baseUrl?path=transactions/bulk", bulkBody)
// bulkResult.count = newly created
// bulkResult.skipped = already existed (safe duplicates ignored)