6.6 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 - List devices in venue
Devices
POST /devices/register - Register a new device
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:
-
openTabsmust be the device's complete list of open tabs. Tabs it no longer reports (e.g. deleted on the terminal) are removed from the dashboard. Settling a tab on the terminal keeps the tab open under the same name with a zero balance, so the name stays listed. -
Every sync returns all pending commands for the device;
cursoris still returned but no longer filters. Devices dedupe by command id and re-send the ack for commands they already processed. -
The first ack for a command wins; later acks are accepted as no-ops.
-
plannedTabNamesonly contains planned customers the device has not opened a tab for yet. Once a sync reports an open tab with that name (ignoring case and extra spaces), the customer is marked delivered and no longer sent. The customer (and their email, used for reminders) is kept. -
events(optional) reports what happened on tabs since the last sync, for sales statistics. Each event has a terminal-generated UUIDid, atype,tabId,customerNameandoccurredAt(ISO-8601, UTC):line_added/line_removed:productName,unitPriceInCents,quantity(always positive)settled:amountInCents,paymentMethod(cash,payconiq,payment_pendingorother), optionalcommandId(the remote close that caused it) anditems(the settled lines)
The response lists
acknowledgedEventIds(stored, including events already stored by an earlier sync) andrejectedEvents([{ id, reason }], invalid and never stored). The terminal removes both from its outbox. An invalid event never fails the sync. At most 1000 events per sync. -
products(optional) is the terminal's complete product catalog:[{ id, name, category?, priceInCents, stock }], wherestockis an integer (may be negative) ornullwhen the terminal doesn't track stock for that product. Products left out are removed from the dashboard. When the field is omitted (older app versions) the stored products are left alone. An invalid product is skipped and logged, never failing the sync. At most 2000 products. -
stockAdjustmentsin the response lists every pending stock change requested from the dashboard for the venue, oldest first:[{ id, productId, type, quantity, note, requestedBy, createdAt }].type: "add"changes the stock byquantity(negative takes stock out);type: "set"makes the stockquantity(a count, ≥ 0, and starts tracking an untracked product). They are re-sent on every sync until the terminal reports an outcome, so the terminal must apply each id only once. -
stockAdjustmentResults(optional) reports outcomes:[{ id, status: "applied" | "rejected", stockAfter?, reason? }](stockAfteris required forapplied). The response listsacknowledgedStockAdjustmentResultIdsandrejectedStockAdjustmentResults([{ id, reason }]); the terminal drops both from its outbox. The terminal's outcome is recorded even if the adjustment was cancelled on the dashboard in the meantime.
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 - Create a command
GET /venues/:venueId/commands - List command history
POST /venues/:venueId/commands/devices/:deviceId/revoke - Revoke device (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