# 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 ` header ### Devices - Register to receive device bearer token (90 days) - Include device token in `Authorization: Bearer ` 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: - `openTabs` must 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; `cursor` is 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. ### 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 and email, not reminded in the last 7 days `POST /venues/:venueId/reminders/send` - Send payment reminders `GET /venues/:venueId/reminders` - Reminder history `POST /venues/:venueId/reminders/test/:plannedTabId` - Send a test email (admin) ### 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 ```json { "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