Files
kooltab-backoffice/docs/api.md
T
brammie15 0ca9f4d16a
CI / build-and-test (push) Canceled after 0s
feat: make server own tabs
2026-10-20 16:18:36 +02:00

7.2 KiB

API Documentation

Base URL: http://localhost:3001/api/v1

Swagger UI: http://localhost:3001/api/docs

Authentication

Dashboard Users

  • Login to receive JWT access token (24h) + refresh token (30 days)
  • Include access token in Authorization: Bearer <token> header

Devices

  • Register to receive device bearer token (90 days)
  • Include device token in Authorization: Bearer <token> header
  • Device ID must match URL parameter

Endpoints

Health

GET /health - Server health check (503 when the database is unreachable)

Auth

POST /auth/login - Login with email/password POST /auth/refresh - Refresh access token POST /auth/logout - Logout and revoke refresh token

Venues

GET /venues - List venues GET /venues/:venueId - Get venue details GET /venues/:venueId/devices - The venue's paired terminals { devices: [...] }

Devices

POST /devices/register - Pair a terminal; every pairing adds one POST /devices/:deviceId/sync - Sync device state (device auth) POST /devices/:deviceId/commands/:commandId/ack - Acknowledge command (device auth) GET /devices/generation-code - Generate registration code (admin)

Sync semantics. The server holds the venue's open tabs and stock; a venue can have several terminals. Each terminal works offline, queues what it does as events, and on every sync replaces its tabs and stock with the server's and replays its unsent events on top. The full rules are in the app repository: docs/server-tabs-source-of-truth.md and docs/server-stock-source-of-truth.md.

  • Every write to a venue's tabs, stock and settlements (syncs, dashboard settles, stock adjustments) runs in one transaction that locks the venue row, so simultaneous terminals apply their changes one after the other.

  • events (at most 1000): terminal-generated UUID id, type, occurredAt (ISO-8601, UTC; capped at the server time when more than 5 minutes ahead) and per type:

    • tab_opened (tabId, customerName, openedAt), tab_renamed (new customerName), tab_deleted
    • line_added / line_removed: lineId, productId, productName, unitPriceInCents, quantity (≥ 1)
    • settled: amountInCents, paymentMethod (cash, payconiq, payment_pending or other), optional commandId and items ([{ productId?, productName, quantity, unitPriceInCents }])
    • stock_set: productId, quantity (a count ≥ 0, or null to stop tracking); no tabId

    Each id is applied once (tab state and stock in the same transaction). acknowledgedEventIds lists the applied ones, including repeats; rejectedEvents ([{ id, reason }]) were invalid and never applied. The terminal drops both from its outbox. An invalid event never fails the sync.

  • The response's tabs is the venue's complete open tab state after this request's events: [{ id, customerName, openedAt, version, lines: [{ id, productId, productName, unitPriceInCents, quantity, units: [{ id: "<event id>:<index>", addedAt, removedAt }] }] }].

  • stock is every product of the venue: [{ productId, stock, countedAt }] (stock null when not tracked, may be negative). Line events change it unless they happened before countedAt; the latest count wins.

  • settlements lists the venue's settlements after settlementsCursor (or of the last 30 days without one), oldest first, at most 500; the response's settlementsCursor is sent back next time.

  • openTabs is only used once per venue, on the first sync after the server started holding the tabs: the terminal's own tabs (and catalog stock) are taken over and the events of that request are stored without applying them again.

  • products (optional, at most 2000) upserts names, categories and prices. Its stock is ignored (counts arrive as stock_set), a new product starts untracked, and products are never removed because one terminal doesn't report them. An invalid product is skipped and logged.

  • Planned customers are opened as tabs by the server, once; plannedTabNames is always empty.

  • Dashboard stock adjustments are applied by the server right away; stockAdjustments is always empty. stockAdjustmentResults from older app versions are still recorded and acknowledged.

  • Every sync returns all pending commands for the device (only close_tab commands created before the server settled tabs itself); cursor is still returned but no longer filters. The first ack for a command wins.

Tabs

GET /venues/:venueId/tabs - List open tabs

Planned Tabs (customer list)

GET /venues/:venueId/planned-tabs - List planned customers POST /venues/:venueId/planned-tabs - Add a customer (admin) POST /venues/:venueId/planned-tabs/import - Bulk import { entries: [{ customerName, email? }], mode: 'merge' | 'replace' } (admin). Names match case-insensitively and keep their existing spelling; replace also removes customers missing from the import. PUT /venues/:venueId/planned-tabs/:id - Rename a customer / set email (admin) DELETE /venues/:venueId/planned-tabs/:id - Remove a customer (admin)

Reminders

GET /venues/:venueId/reminders/candidates - Customers with an unpaid balance, with their reminder count, last reminder and whether they are eligible (have an email and weren't reminded in the last 7 days) POST /venues/:venueId/reminders/send - Send payment reminders (admin). Body { "tabIds": [...] } picks the recipients, including customers still in their cooldown; without it every eligible customer is reminded GET /venues/:venueId/reminders?customerName= - Reminder history, optionally for one customer POST /venues/:venueId/reminders/test/:plannedTabId - Send a test email (admin)

Stock

GET /venues/:venueId/stock - The terminal's products with stock, low stock level and pending adjustments GET /venues/:venueId/stock/adjustments?productId=&limit=&offset= - Stock adjustment history, newest first POST /venues/:venueId/stock/adjustments - Request stock changes (admins and viewers): { adjustments: [{ productId, type: 'add' | 'set', quantity, note? }] }. add takes a non-zero quantity (negative for breakage) and needs a tracked product; set takes 0 or more. POST /venues/:venueId/stock/adjustments/:id/cancel - Cancel an adjustment the terminal hasn't received yet (admins and viewers) PUT /venues/:venueId/stock/products/:productId - Set { lowStockThreshold: number | null } (admin, dashboard only)

Commands

POST /venues/:venueId/commands - Settle a tab: { tabId, type: "close_tab", expectedVersion, paymentMethod }. The server settles everything on the tab at once (also with every terminal offline) and records the command as completed. version_mismatch when the tab changed since it was shown, tab_empty when there is nothing on it. GET /venues/:venueId/commands - List command history POST /venues/:venueId/commands/devices/:deviceId/revoke - Unpair a terminal (admin)

All /venues/:venueId/... routes return 403 venue_forbidden unless the user belongs to that venue.

Error Format

{
  "error": {
    "code": "error_code",
    "message": "Human-readable message"
  }
}

HTTP Status Codes

  • 200 Success
  • 201 Created
  • 400 Bad Request
  • 401 Unauthorized
  • 403 Forbidden
  • 404 Not Found
  • 409 Conflict
  • 429 Too Many Requests