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:

Header
Authorization: Bearer sk-...
Alternative Header
x-api-key: sk-...
Query Param
?api_key=sk-...

Roles

super_adminFull access: manage users, all dish operations

adminManage dishes, manage own API keys

staffRead-only: view dishes, manage own API keys

Error Format

Error Response
{"error": {"code": "missing_api_key", "message": "..."}}
StatusCodeMeaning
401missing_api_keyNo API key provided
401invalid_api_keyKey not found, malformed, or expired
403insufficient_roleRole too low
404not_foundResource not found
409conflictDuplicate or conflict
422validation_errorMissing or invalid body
429rate_limitedToo many requests

Auth Endpoints

POST/api/auth/register

Register first user (super_admin).

Body:

{"username": "owner", "password": "password123"}

Response:

{"api_key": "sk-...", "user": {"id": "...", "username": "owner", "role": "super_admin"}}

Errors: 422, 409, 403

POST/api/auth/login

Authenticate with username/password.

Body:

{"username": "owner", "password": "password123"}

Response:

{"api_key": "sk-...", "user": {"id": "...", "username": "owner", "role": "..."}}

Errors: 422, 401

GET/api/auth/keys

List user's API keys.

Response:

Array of {id, key_prefix, name, last_used_at, created_at, expires_at}
POST/api/auth/keys

Generate 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

DELETE/api/auth/keys/:idAny (own) / super_admin (any)

Revoke an API key.

Response:

{"deleted": true}

Errors: 404, 403

Admin Endpoints

GET/api/admin/userssuper_admin

List all users.

Response:

Array of {id, username, role, created_at}
POST/api/admin/userssuper_admin

Create a new user with API key.

Body:

{"username": "staff1", "password": "pass123", "role": "staff"}

Errors: 422, 409

PUT/api/admin/users/:idsuper_admin

Update user role, username, or password.

Errors: 404, 422, 409

DELETE/api/admin/users/:idsuper_admin

Delete a user (CASCADE keys).

Errors: 404, 422

POST/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}
POST/api/admin/upload-dish-imageadmin+

Upload dish image (max 10MB).

Response:

{"image_url": "/dishes/123456-abc.jpg"}

Dish Endpoints

GET/api/dishesAny

List 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, ...}
GET/api/dishes/topAny

Get top-selling dishes by quantity sold.

Body:

?limit=4 (opt, max 50)

Response:

Array of dishes with extra total_sold and revenue_hkd
GET/api/dishes/:idAny

Get a single dish.

Errors: 404

POST/api/dishesadmin+

Create a dish.

Body:

{"name_ja": "...", "name_en": "...", "name_zh": "...", "category": "sashimi", "price_hkd": 8800}

Errors: 422

PUT/api/dishes/:idadmin+

Update a dish.

Errors: 404, 422

DELETE/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

GET/api/tablesAny

List active tables.

Body:

?qr_code=... (opt)

Response:

Array of {id, label, qr_code, capacity, section, is_active, has_active_order}
POST/api/tablesadmin+

Create a table.

Body:

{"label": "Table 1", "capacity": 4, "section": "main"}

Errors: 422, 409

PUT/api/tables/:idadmin+

Update a table.

Errors: 404

DELETE/api/tables/:idadmin+

Delete a table.

Errors: 404

Order Endpoints

GET/api/ordersAny

List orders with items.

Body:

?status=... &table_id=... &offset=0 &limit=50

Response:

Array of orders with items, table info, service charge, totals
GET/api/orders/:idAny

Get a single order.

Errors: 404

POST/api/orders

Place an order from QR scan (public).

Body:

{"table_qr_code": "...", "items": [{"dish_id": 1, "quantity": 2}], "party_size": 4}

Errors: 422, 404, 409, 429

POST/api/orders/:id/items

Append a round.

Body:

{"table_qr_code": "...", "items": [{"dish_id": 3, "quantity": 1}]}

Errors: 403, 404, 409, 429

PUT/api/orders/:idstaff+

Update order status or per-item status.

Body:

{"status": "preparing|served|paid|cancelled"}

Errors: 404, 409, 422

DELETE/api/orders/:id/items/:itemIdstaff+

Cancel a dish instance (soft-delete).

Errors: 400, 404, 409

GET/api/orders/streamstaff / table_qr_code

SSE endpoint for real-time order updates.

Response:

Events: connected, initial_order, order_created, order_updated, items_added, item_removed, keepalive

Booking Endpoints

GET/api/bookingsAny

List bookings.

Body:

?date=2026-05-10 (opt)

Response:

Array of {id, customer_name, party_size, booking_date, booking_time, status, table_id, ...}
POST/api/bookings

Create a booking (public).

Body:

{"customer_name": "John", "booking_date": "2026-05-10", "booking_time": "19:00", "party_size": 4}

Errors: 422

PUT/api/bookings/:idstaff+

Update a booking.

Body:

{"status": "confirmed|arrived|cancelled|no_show", "table_id": 1}

Errors: 404, 422

DELETE/api/bookings/:idadmin+

Delete a booking.

Errors: 404

Settings Endpoints

GET/api/settings

Get restaurant settings. Public.

Response:

{theme, dark_mode, service_charge_enabled, service_charge_percentage, basic_fee_enabled, basic_fee_per_person, restaurant_address, restaurant_phone}
PUT/api/settingsadmin+

Update settings.

Body:

{"service_charge_percentage": 10}

Errors: 400

Time Slot Endpoints

GET/api/time-slotsAny

List all time slots.

Response:

Array of {id, name, display_name_ja/en/zh, start_time, end_time, is_active, sort_order}
GET/api/time-slots/currentAny

Get currently active time slot.

Errors: 404

POST/api/time-slotsadmin+

Create a time slot.

Body:

{"name": "dinner", "start_time": "18:00", "end_time": "22:00"}

Errors: 422

PUT/api/time-slots/:idadmin+

Update a time slot.

Errors: 404

DELETE/api/time-slots/:idadmin+

Delete a time slot.

Errors: 404, 409

Variant Endpoints

GET/api/admin/variant-groupsAny

List variant groups with options.

Response:

Array of {id, name, sort_order, options: [{id, name, price_adjustment, is_default}]}
POST/api/admin/variant-groupsadmin+

Create a variant group.

Body:

{"name": "Temperature", "sort_order": 1}

Errors: 422

POST/api/admin/variant-groups/:id/optionsadmin+

Add option to variant group.

Body:

{"name": "Medium Rare", "price_adjustment": 0}
GET/api/admin/dishes/:id/variantsAny

Get dish variant groups.

POST/api/admin/dishes/:id/variantsadmin+

Assign variant group to dish.

Body:

{"variant_group_id": 1, "is_required": true}

Errors: 422, 409

curl Examples

Register (first user only)
curl -X POST http://localhost:3898/api/auth/register \  -H "Content-Type: application/json" \  -d '{"username":"owner","password":"password123"}'
Login
curl -X POST http://localhost:3898/api/auth/login \  -H "Content-Type: application/json" \  -d '{"username":"owner","password":"password123"}'
List all dishes
curl http://localhost:3898/api/dishes \  -H "Authorization: Bearer sk-..."
Place an order (public)
curl -X POST http://localhost:3898/api/orders \  -H "Content-Type: application/json" \  -d '{"table_qr_code":"a1b2c3d4-...","items":[{"dish_id":1,"quantity":2}]}'