F

Flint Sync API

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.

Publicly reachable URL — no login required to call endpoints
🔑 API key required on every request
Order Screen API Docs

Authentication

Include this header on every request:

Authorization: Bearer {SYNC_API_KEY}

Your SYNC_API_KEY is set in Dashboard → Settings → Environment Variables.

Base URL

Live Endpoint

https://flint-backoffice.base44.app/api/functions/syncApi

All paths are passed as a ?path= query parameter. For example:

…/syncApi?path=config&till_id=abc123

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.

Endpoints

Tables & Stored Sales

Online Orders (Customer QR Ordering)

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.

How the Android Till receives online orders

  1. Customer scans QR code → opens /order/<venueId>/<tableNumber> in browser.
  2. Customer adds items and taps Place Order → browser calls submitOnlineOrder.
  3. Back-office creates a Transaction (status: pending, payment_method: other, reference: ONLINE-T{tableNumber}) and a RelayCommand (POST /api/v1/transactions) targeting the venue's online order till.
  4. The Android till's command poller picks up the relay command on its next poll cycle (≤1.5s).
  5. Till executes the command against its own local API (POST /api/v1/transactions) — this triggers the till's normal new-order flow (print kitchen ticket, show on display, etc.).
  6. Till posts result back via ?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.

Online Order Transaction shape (as delivered via RelayCommand body)

json
{
  "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"
}

Product Configurators

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.

Configurator selections in transactions

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.

json
{
  "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
}

Multi-Location Product Overrides

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.

Resolution logic — apply in this order

  1. Find the entry in location_details where location_id matches the till's location_id.
  2. If is_active === false for that entry → hide the product entirely at this location.
  3. For each field (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.
  4. If no matching location_details entry exists → use global product defaults as normal.
  5. If multiple_locations_enabled is false → ignore location_details entirely and use global defaults.
kotlin
// 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
    )
}

Error Responses

400

Bad Request

Missing or invalid parameters

401

Unauthorized

Missing or invalid API key

404

Not Found

Till ID or resource doesn't exist

500

Server Error

Unexpected internal error

json
{ "error": "error message here" }

Android (Kotlin) Example

kotlin
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)