7.9 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 UUIDid,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(newcustomerName),tab_deletedline_added/line_removed:lineId,productId,productName,unitPriceInCents,quantity(≥ 1)settled:amountInCents,paymentMethod(cash,payconiq,payment_pendingorother), optionalcommandIdanditems([{ productId?, productName, quantity, unitPriceInCents }])stock_set:productId,quantity(a count ≥ 0, ornullto stop tracking); notabId
Each id is applied once (tab state and stock in the same transaction).
acknowledgedEventIdslists 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
tabsis 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 }] }] }]. -
stockis every product of the venue:[{ productId, stock, countedAt }](stocknull when not tracked, may be negative). Line events change it unless they happened beforecountedAt; the latest count wins. -
settlementslists the venue's settlements aftersettlementsCursor(or of the last 30 days without one), oldest first, at most 500; the response'ssettlementsCursoris sent back next time. -
openTabsis 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. -
The dashboard manages the products.
productsin 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'scataloglists every product of the venue:[{ id, name, category, priceInCents, lowStockThreshold, archived }]. The terminal adds and updates its products from it and hides archived products and products the catalog doesn't list. -
Planned customers are opened as tabs by the server, once;
plannedTabNamesis always empty. -
Dashboard stock adjustments are applied by the server right away;
stockAdjustmentsis always empty.stockAdjustmentResultsfrom older app versions are still recorded and acknowledged. -
Every sync returns all pending commands for the device (only
close_tabcommands created before the server settled tabs itself);cursoris 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 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)
POST /venues/:venueId/stock/products - Add a product (admin): { 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).
PATCH /venues/:venueId/stock/products/:productId - Change a product (admin): 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.
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