API Reference
REST API
Base URL: /api · All endpoints return JSON. Prices stored in HKD cents (e.g. 3800 = HK$38.00).
Overview
Zest provides a full REST API covering dishes, orders, tables, bookings, settings, variants, and time slots. All endpoints return JSON.
Authentication
Protected endpoints require an API key. Send it via:
Authorization: Bearer sk-...x-api-key: sk-...?api_key=sk-...Roles
super_admin — Full access: manage users, all dish operations
admin — Manage dishes, manage own API keys
staff — Read-only: view dishes, manage own API keys
Error Format
{"error": {"code": "missing_api_key", "message": "..."}}| Status | Code | Meaning |
|---|---|---|
| 401 | missing_api_key | No API key provided |
| 401 | invalid_api_key | Key not found, malformed, or expired |
| 403 | insufficient_role | Role too low |
| 404 | not_found | Resource not found |
| 409 | conflict | Duplicate or conflict |
| 422 | validation_error | Missing or invalid body |
| 429 | rate_limited | Too many requests |
Auth Endpoints
/api/auth/registerRegister first user (super_admin).
Body:
{"username": "owner", "password": "password123"}Response:
{"api_key": "sk-...", "user": {"id": "...", "username": "owner", "role": "super_admin"}}Errors: 422, 409, 403
/api/auth/loginAuthenticate with username/password.
Body:
{"username": "owner", "password": "password123"}Response:
{"api_key": "sk-...", "user": {"id": "...", "username": "owner", "role": "..."}}Errors: 422, 401
/api/auth/keysList user's API keys.
Response:
Array of {id, key_prefix, name, last_used_at, created_at, expires_at}/api/auth/keysGenerate a new API key.
Body:
{"name": "office laptop", "expires_at": "2026-12-31T23:59:59Z"}Response:
{"api_key": "sk-...", "key": {"id": "...", "key_prefix": "sk-a1b2...", "name": "office laptop"}}Errors: 422
/api/auth/keys/:idAny (own) / super_admin (any)Revoke an API key.
Response:
{"deleted": true}Errors: 404, 403
Admin Endpoints
/api/admin/userssuper_adminList all users.
Response:
Array of {id, username, role, created_at}/api/admin/userssuper_adminCreate a new user with API key.
Body:
{"username": "staff1", "password": "pass123", "role": "staff"}Errors: 422, 409
/api/admin/users/:idsuper_adminUpdate user role, username, or password.
Errors: 404, 422, 409
/api/admin/users/:idsuper_adminDelete a user (CASCADE keys).
Errors: 404, 422
/api/admin/ai-fill-dishadmin+AI-generate dish names from hint.
Body:
{"hint": "salmon teriyaki"}Response:
{name_ja, name_en, name_zh, description_ja/en/zh, category}/api/admin/upload-dish-imageadmin+Upload dish image (max 10MB).
Response:
{"image_url": "/dishes/123456-abc.jpg"}Dish Endpoints
/api/dishesAnyList dishes. Ordered by sort_order.
Body:
?category=sashimi (opt) · ?time_slot_id=1 (opt)Response:
Array of {id, name_ja, name_en, name_zh, category, price_hkd, image_url, variant_groups, ...}/api/dishes/topAnyGet top-selling dishes by quantity sold.
Body:
?limit=4 (opt, max 50)Response:
Array of dishes with extra total_sold and revenue_hkd/api/dishes/:idAnyGet a single dish.
Errors: 404
/api/dishesadmin+Create a dish.
Body:
{"name_ja": "...", "name_en": "...", "name_zh": "...", "category": "sashimi", "price_hkd": 8800}Errors: 422
/api/dishes/:idadmin+Update a dish.
Errors: 404, 422
/api/dishes/:idadmin+Delete a dish.
Response:
{"deleted": true}Errors: 404
Categories
| Slug | 日本語 | English | 中文 |
|---|---|---|---|
| appetizer | 前菜 | Appetizer | 前菜 |
| sashimi | 刺身 | Sashimi | 刺身 |
| grilled | 焼物 | Grilled | 烤物 |
| fried | 揚物 | Fried | 炸物 |
| rice_bowl | 丼物 | Rice Bowl | 盖饭 |
| noodles | 麺類 | Noodles | 面类 |
| dessert | 甘味 | Dessert | 甜品 |
| drink | 飲物 | Drink | 饮品 |
Table Endpoints
/api/tablesAnyList active tables.
Body:
?qr_code=... (opt)Response:
Array of {id, label, qr_code, capacity, section, is_active, has_active_order}/api/tablesadmin+Create a table.
Body:
{"label": "Table 1", "capacity": 4, "section": "main"}Errors: 422, 409
/api/tables/:idadmin+Update a table.
Errors: 404
/api/tables/:idadmin+Delete a table.
Errors: 404
Order Endpoints
/api/ordersAnyList orders with items.
Body:
?status=... &table_id=... &offset=0 &limit=50Response:
Array of orders with items, table info, service charge, totals/api/orders/:idAnyGet a single order.
Errors: 404
/api/ordersPlace an order from QR scan (public).
Body:
{"table_qr_code": "...", "items": [{"dish_id": 1, "quantity": 2}], "party_size": 4}Errors: 422, 404, 409, 429
/api/orders/:id/itemsAppend a round.
Body:
{"table_qr_code": "...", "items": [{"dish_id": 3, "quantity": 1}]}Errors: 403, 404, 409, 429
/api/orders/:idstaff+Update order status or per-item status.
Body:
{"status": "preparing|served|paid|cancelled"}Errors: 404, 409, 422
/api/orders/:id/items/:itemIdstaff+Cancel a dish instance (soft-delete).
Errors: 400, 404, 409
/api/orders/streamstaff / table_qr_codeSSE endpoint for real-time order updates.
Response:
Events: connected, initial_order, order_created, order_updated, items_added, item_removed, keepaliveBooking Endpoints
/api/bookingsAnyList bookings.
Body:
?date=2026-05-10 (opt)Response:
Array of {id, customer_name, party_size, booking_date, booking_time, status, table_id, ...}/api/bookingsCreate a booking (public).
Body:
{"customer_name": "John", "booking_date": "2026-05-10", "booking_time": "19:00", "party_size": 4}Errors: 422
/api/bookings/:idstaff+Update a booking.
Body:
{"status": "confirmed|arrived|cancelled|no_show", "table_id": 1}Errors: 404, 422
/api/bookings/:idadmin+Delete a booking.
Errors: 404
Settings Endpoints
/api/settingsGet restaurant settings. Public.
Response:
{theme, dark_mode, service_charge_enabled, service_charge_percentage, basic_fee_enabled, basic_fee_per_person, restaurant_address, restaurant_phone}/api/settingsadmin+Update settings.
Body:
{"service_charge_percentage": 10}Errors: 400
Time Slot Endpoints
/api/time-slotsAnyList all time slots.
Response:
Array of {id, name, display_name_ja/en/zh, start_time, end_time, is_active, sort_order}/api/time-slots/currentAnyGet currently active time slot.
Errors: 404
/api/time-slotsadmin+Create a time slot.
Body:
{"name": "dinner", "start_time": "18:00", "end_time": "22:00"}Errors: 422
/api/time-slots/:idadmin+Update a time slot.
Errors: 404
/api/time-slots/:idadmin+Delete a time slot.
Errors: 404, 409
Variant Endpoints
/api/admin/variant-groupsAnyList variant groups with options.
Response:
Array of {id, name, sort_order, options: [{id, name, price_adjustment, is_default}]}/api/admin/variant-groupsadmin+Create a variant group.
Body:
{"name": "Temperature", "sort_order": 1}Errors: 422
/api/admin/variant-groups/:id/optionsadmin+Add option to variant group.
Body:
{"name": "Medium Rare", "price_adjustment": 0}/api/admin/dishes/:id/variantsAnyGet dish variant groups.
/api/admin/dishes/:id/variantsadmin+Assign variant group to dish.
Body:
{"variant_group_id": 1, "is_required": true}Errors: 422, 409
curl Examples
curl -X POST http://localhost:3898/api/auth/register \ -H "Content-Type: application/json" \ -d '{"username":"owner","password":"password123"}'curl -X POST http://localhost:3898/api/auth/login \ -H "Content-Type: application/json" \ -d '{"username":"owner","password":"password123"}'curl http://localhost:3898/api/dishes \ -H "Authorization: Bearer sk-..."curl -X POST http://localhost:3898/api/orders \ -H "Content-Type: application/json" \ -d '{"table_qr_code":"a1b2c3d4-...","items":[{"dish_id":1,"quantity":2}]}'