Files
kooltab-backoffice/docs/api.md
T
brammie15 4dae99786f
CI / build-and-test (push) Canceled after 0s
fix: image processing
2026-10-21 16:29:32 +02:00

9.9 KiB
Raw Blame History

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/images - Upload a picture taken on the terminal (device auth, multipart field file): { imageId }. Processed like a dashboard upload (see Images); the terminal links it with a product_updated event 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
    • product_created / product_updated / product_archived: productId (the terminal makes the id of a new product) and product with the fields that changed: any of name, category, priceInCents, lowStockThreshold, archived, imageId. product_created needs name and priceInCents; no tabId. Every field keeps the time it last changed, and an event only changes a field it changed later (by occurredAt), so an offline rename and a dashboard price change both survive. An imageId the server doesn't have is ignored; an update of a product the venue doesn't have is acknowledged and dropped

    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 are changed on the dashboard and, through the product events above, on terminals. products in the request (at most 2000) is only used on that same first sync, to take over the terminal's catalog with its stock; after that it is ignored. The response's catalog lists every product of the venue: [{ id, name, category, priceInCents, lowStockThreshold, archived, imageId }]. The terminal adds and updates its products from it (replaying its unsent product events on top), hides archived products and products the catalog doesn't list, and downloads pictures it doesn't have yet from GET /images/:imageId.

  • 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 venue's products (archived ones too, with archivedAt) 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 managers): { 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 managers) POST /venues/:venueId/stock/products - Add a product (admins and managers): { name, category?, priceInCents, stock?, lowStockThreshold? }. stock is the starting count (recorded as an applied set adjustment); null or left out: not tracked. product_name_exists when an active product has the same name (ignoring case). Terminals may still create products with the same name; the products page marks those as duplicates. PATCH /venues/:venueId/stock/products/:productId - Change a product (admins and managers): any of { name, category, priceInCents, lowStockThreshold, archived }. archived: true deletes it for the terminals (kept for tabs and history), archived: false brings it back. The stock itself changes through adjustments. POST /venues/:venueId/stock/products/:productId/image - Set a product's picture (admins and managers; multipart field file, see Images): the product with its new imageId DELETE /venues/:venueId/stock/products/:productId/image - Remove a product's picture (admins and managers)

Images

GET /images/:imageId - A product picture (public, image/webp, cached for a year: an id never changes content). Uploads (JPEG, PNG, WebP, GIF or AVIF, at most 10 MB) are turned upright and stored as a 512×512 WebP without metadata. A product on a plain background (all four edges one colour, or transparent) is trimmed and centred with a 40 px margin on that background. Any other photo is cropped around its centre when roughly square, or fitted whole on a transparent background when not (bottles, cans). The id is the SHA-256 of the stored file. invalid_image when the upload can't be read.

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