API reference
Every route the API serves, who may call it, and how sign-in, permissions, errors, lists and limits work.
For the Full Stack package
Base URL and response format
Every route is served under the /api prefix. Locally the base URL is http://localhost:8000/api. On a server it is your API's address followed by /api, the same value both frontends read from NEXT_PUBLIC_API_BASE_URL (Environment variables).
The API serves two audiences that never share a token: customers, who use the website's routes, and staff, who use the dashboard's. Request bodies are JSON, except file uploads. Every answer comes in the same envelope:
{
"success": true,
"data": { },
"message": ""
}data holds the result. message is a short sentence some writes fill in, in the request's language, and empty otherwise. Money is decimal dollars in JSON, such as 14.99, and every total is computed by the API: nothing a client sends about money is trusted.
The health check needs no token:
curl http://localhost:8000/api/health{"success":true,"data":{"status":"ok","database":"up"},"message":""}When the database cannot be read it still answers 200, with "status":"degraded" and "database":"down". The Docker image's health check reads that field.
Errors
A failure keeps the envelope, with success: false and the HTTP status:
{
"success": false,
"data": null,
"message": "errors.kitchen_closed",
"errors": { "email": ["…"] },
"code": "…"
}- Errors a customer sees are keys such as
errors.kitchen_closed,errors.outside_areaorerrors.rate_limited, which the website translates itself. Other messages are sentences in the request's language. errorsappears on a validation failure, with the problems per field.codecarries a machine-readable reason where one exists, such aschangeswith achangeslist when a price moved before an order was placed.- A guarded route with no token answers
401; a token without the permission answers403.
Lists and paging
List routes take page and limit (page_count is accepted for limit). limit is capped at 100. Unless the caller sorts, rows come newest-updated first, with the id as a tiebreaker, so paging is stable.
{ "items": [ ], "total": 64, "page": 1, "limit": 12, "totalPages": 6 }The dish and order lists put the rows under items. The categories, reviews, customers, team, roles and settings lists put them under data, with the same paging fields. Lists read in a sequence, such as the homepage sections and the kitchens' queue, keep that sequence.
Language
Send Accept-Language: ar for Arabic messages; anything else answers in English. Menu records carry both languages as { "en": "…", "ar": "…" } whatever the header says, and the client picks one.
Signing in
Sign in as staff
Terminalcurl -X POST http://localhost:8000/api/auth/login \ -H "Content-Type: application/json" \ -d '{"email":"owner@foodstudio.example","password":"FoodDemo2026!"}'data.access_tokenis the staff token, withtoken_typeBearer,expires_in(7dby default,JWT_EXPIRATION) and the member with their roles.GET /api/auth/mereturns the signed-in member with their permissions.Call a protected route
Terminalcurl http://localhost:8000/api/orders -H "Authorization: Bearer YOUR_TOKEN"Sign in as a customer
POST /api/auth/customer/loginwith the same body answers{ token, user };POST /api/auth/customer/registertakesfirst_name,last_name,email,password(8 characters or more) and optionallyphone, and answers the same. The demo customer issam@foodstudio.examplewithFoodDemo2026!.
- A customer token is refused on every staff route, and a staff token on every customer route.
- An unknown email and a wrong password both answer
401with the same message. - Ten failed sign-ins from one address within 15 minutes, per form, answer
429until the oldest failure is 15 minutes old (RATE_LIMIT_LOGIN). Successful sign-ins are not counted. - There is no sign-out route: a client signs out by forgetting its token.
| Method | Path | Who can call it | What it does |
|---|---|---|---|
POST | /api/auth/login | Anyone | Staff sign-in |
GET | /api/auth/me | Any signed-in staff member | The signed-in member, their roles and permissions |
POST | /api/auth/customer/register | Anyone | Create a customer account and sign in. A repeated client_token returns the first attempt's account |
POST | /api/auth/customer/login | Anyone | Customer sign-in |
GET | /api/auth/customer/me | Customer | The profile, with reward_points |
PATCH | /api/auth/customer/me | Customer | Edit first_name, last_name, email, phone |
PATCH | /api/auth/customer/me/password | Customer | Change the password: current_password, new_password |
POST | /api/auth/customer/forgot-password | Anyone | Email a reset link. Always answers { sent: true }, for any address |
POST | /api/auth/customer/reset-password | Anyone | Finish a reset: token, password. A token works once, within an hour |
Permissions
Staff routes check a permission, named <module>.<action>. A member holds every permission of every role they have.
| Module | Permissions |
|---|---|
| Team | admins.view, admins.create, admins.edit, admins.delete, admins.assign_roles |
| Roles | roles.view, roles.create, roles.edit, roles.delete, roles.assign_permissions |
| Settings, kitchens and promo codes | settings.view, settings.edit |
| Customers | users.view, users.create, users.update, users.delete, users.restore, users.verify |
| Categories | categories.view, categories.create, categories.edit, categories.delete, categories.restore |
| Menu, offers and homepage | products.view, products.create, products.edit, products.delete, products.restore |
| Orders and the overview | orders.view, orders.edit |
| AI | ai_studio.use, ai_chat.use, ai_chat.view_models |
| Seeded role | Grants |
|---|---|
| Owner | Every permission |
| Manager | Every permission except admins.* and roles.* |
| Kitchen | orders.view and orders.edit, narrowed to its own kitchen's orders, with no prices and no customer email, and only the moves from confirmed to preparing and from preparing to ready |
The menu
| Method | Path | Who can call it | What it does |
|---|---|---|---|
GET | /api/products | Anyone; staff also see hidden rows | The published menu. q, category (slug), dietary (comma separated), available, sort (popularity, price_asc, price_desc, name), page, limit (12 by default). scope=admin with products.view lists every dish, drafts included |
GET | /api/products/featured | Anyone | The featured dishes |
GET | /api/products/slug/:slug | Anyone; staff also see hidden rows | One dish by its slug, with related dishes |
GET | /api/products/:id | Anyone; staff also see hidden rows | One dish by id |
GET | /api/products/:id/related | Anyone | Dishes from the same category, then the best sellers |
GET | /api/categories | Anyone; staff also see hidden rows | The categories, in their menu order |
GET | /api/categories/roots | Anyone | The top-level categories |
GET | /api/categories/slug/:slug | Anyone; staff also see hidden rows | One category by its slug |
GET | /api/categories/:id | Anyone; staff also see hidden rows | One category by id |
GET | /api/offers | Anyone; staff also see hidden rows | The combos, with their choices, surcharges and savings |
GET | /api/offers/:code | Anyone; staff also see hidden rows | One combo, such as classic-combo |
GET | /api/homepage-sections | Anyone; staff also see hidden rows | The home page's sections, in page order |
GET | /api/homepage-sections/:key/products | Anyone | The dishes a section shows, in order |
GET | /api/reviews/product/:productId | Anyone | A dish's reviews |
GET | /api/reviews/product/:productId/summary | Anyone | The average, the total and the count per star |
A dish carries name, description and ingredients in both languages, price, its category, images, option_groups with their choices and each choice's price_delta, allergens, dietary_tags, preparation_minutes, and the switches is_active (published), is_available (orderable now), is_featured and is_best_seller.
Kitchens and the postcode check
| Method | Path | Who can call it | What it does |
|---|---|---|---|
GET | /api/kitchens | Anyone; staff also see hidden rows | The kitchens, each with its hours, timezone, preparation time, delivery postcodes and whether it is open now |
GET | /api/kitchens/:code | Anyone; staff also see hidden rows | One kitchen, such as central |
POST | /api/kitchens/check | Anyone | Which kitchen can serve a postcode |
{ "postcode": "10001", "mode": "delivery", "kitchen_code": "central" }It answers { result, kitchen, alternatives }, where result is match, outside_area or closed. Pickup is not limited by the postcode. Whether a kitchen is open follows the service clock setting.
Pricing, promo codes and the cart
| Method | Path | Who can call it | What it does |
|---|---|---|---|
POST | /api/checkout/quote | Anyone; a customer token is read when sent | Price a cart against a kitchen without writing anything |
POST | /api/promo/validate | Anyone | What a code would take off a subtotal: { code, subtotal } |
GET | /api/cart | Customer | The signed-in customer's server cart, with its quote |
POST | /api/cart/items | Customer | Add a selection |
PATCH | /api/cart/items/:id | Customer | Change a line's quantity, options or note |
DELETE | /api/cart/items/:id | Customer | Remove a line |
DELETE | /api/cart | Customer | Empty the cart |
PUT | /api/cart/meta | Customer | Change the kitchen and delivery or pickup |
POST | /api/cart/merge | Customer | Fold a guest cart in on sign-in, never replacing a line |
{
"lines": [
{ "product_id": 9, "quantity": 1, "options": [{ "group": "size", "choice": "double" }], "instructions": "no onions" },
{ "offer_id": 1, "quantity": 1, "offer_choices": { "main": 9, "side": 5, "drink": 3 } }
],
"kitchen_code": "central",
"fulfillment": "delivery",
"address": { "street": "12 Main St", "city": "Demo City", "postcode": "10001" },
"promo_code": "TASTE10"
}- A line names a
product_idor anoffer_id, with aquantityfrom 1 to 10 andinstructionsof at most 200 characters. - The quote reports the subtotal, the discounts, the fee, the total, the points the order would earn, whether delivery can go ahead (
address_incomplete,outside_area,closed,below_minimum) and achangeslist for any line whose price moved or that cannot be ordered. - A promo takes its percentage of the menu subtotal, never of the delivery fee.
redeem_points: truetakes one redemption off for a signed-in customer with enough points.
Placing and tracking an order
| Method | Path | Who can call it | What it does |
|---|---|---|---|
POST | /api/orders | Anyone; a customer token is read when sent | Place an order. Needs an Idempotency-Key header |
GET | /api/orders/my | Customer | The customer's orders, newest first |
GET | /api/orders/number/:orderNumber | Customer | One of the customer's own orders, such as FS-1042 |
GET | /api/orders/track | Anyone | Guest tracking: ?number=FS-1042&email=… |
GET | /api/orders/track/status | Anyone | Where an order stands, for a holder of a tracking token: ?token=… |
POST /api/orderstakes the quote'slines,kitchen_code,fulfillmentandaddress, plusphone,payment_method(stripe,paypalordemo, asGET /api/payments/methodslists),terms_accepted: true, and for a guestnameandemail. Optional:promo_code,redeem_points,delivery_instructions,locale,accept_changes, andreturn_url_okandreturn_url_cancel, which must be on aCORS_ORIGINaddress.- It answers
201with{ order, payment, tracking }.payment.kindisredirect, with the provider's page inpayment.url, ornonefor the demo payment, whichPOST /api/payments/checkout-sessionthen pays.trackingis a signed token, valid for three hours, forGET /api/orders/track/status. - The same
Idempotency-Keyfrom the same person returns the stored order instead of a second one. - A price that moved, or a dish that became unavailable, answers
422withcode: "changes"until the client sends the change's key inaccept_changes. - Guest tracking answers the same
404for an unknown number, a wrong email and missing parameters.
An order's status moves pending, confirmed, preparing, ready, then out_for_delivery and delivered for delivery, or straight to delivered for pickup. It can be cancelled with a reason before it leaves the kitchen, and becomes refunded only when the payment provider reports a refund.
Payments
| Method | Path | Who can call it | What it does |
|---|---|---|---|
GET | /api/payments/methods | Anyone | What checkout may offer: { methods, hosted, modes }, each method test or live |
POST | /api/payments/checkout-session | Anyone; a customer token is read when sent | Pay for, or reopen the payment of, an unpaid pending order: { order_number, email? } |
GET | /api/payments/return/stripe | The browser, back from Stripe (signed address) | Confirms the payment with Stripe, then redirects to the website with ?order=…&payment=paid|pending|cancelled|failed |
GET | /api/payments/return/paypal | The browser, back from PayPal (signed address) | Captures the approved payment, then redirects the same way |
POST | /api/payments/webhooks/:provider | Stripe, PayPal, or the demo payment's caller | stripe, paypal or demo. An unsigned or altered call answers 401 |
Addresses, saved dishes, rewards, reviews and contact
| Method | Path | Who can call it | What it does |
|---|---|---|---|
GET | /api/addresses | Customer | The saved delivery addresses |
GET | /api/addresses/:id | Customer | One address |
POST | /api/addresses | Customer | Save an address |
PATCH | /api/addresses/:id | Customer | Edit an address |
PATCH | /api/addresses/:id/default | Customer | Make it the default |
DELETE | /api/addresses/:id | Customer | Delete an address |
GET | /api/wishlist | Customer | The saved dishes |
PUT | /api/wishlist/:productId | Customer | Save a dish |
DELETE | /api/wishlist/:productId | Customer | Remove a dish |
DELETE | /api/wishlist | Customer | Remove every saved dish |
POST | /api/wishlist/merge | Customer | Add a guest's saved dishes on sign-in |
GET | /api/rewards/me | Customer | The points balance, one redemption's cost and value, and the history |
POST | /api/reviews | Customer | Review a dish |
PATCH | /api/reviews/:id | Customer | Edit one's own review |
DELETE | /api/reviews/:id | Customer | Delete one's own review |
POST | /api/contact | Anyone | The contact form: name, email, message, client_token. It reaches staff as a notification |
Staff: menu, categories, combos and homepage
| Method | Path | Who can call it | What it does |
|---|---|---|---|
POST | /api/products | Staff with products.create | Create a dish |
PATCH | /api/products/:id | Staff with products.edit | Edit a dish. images and option_groups replace the whole set when sent |
PATCH | /api/products/:id/availability | Staff with products.edit | { is_available }: the switch a kitchen flips during service |
DELETE | /api/products/:id | Staff with products.delete | Archive a dish |
GET | /api/products/deleted | Staff with products.delete | The archived dishes |
POST | /api/products/deleted/:id/restore | Staff with products.restore | Restore a dish |
GET | /api/products/statistic | Staff with products.view | Menu counts |
GET | /api/products/drafts | Any signed-in staff member | The dish form's autosaved draft |
PUT | /api/products/drafts | Any signed-in staff member | Save the draft |
DELETE | /api/products/drafts | Any signed-in staff member | Discard the draft |
POST | /api/categories | Staff with categories.create | Create a category |
PATCH | /api/categories/:id | Staff with categories.edit | Edit a category |
DELETE | /api/categories/:id | Staff with categories.delete | Archive a category |
GET | /api/categories/deleted | Staff with categories.delete | The archived categories |
POST | /api/categories/deleted/:id/restore | Staff with categories.restore | Restore a category |
GET | /api/categories/statistic | Staff with categories.view | Category counts |
POST | /api/offers | Staff with products.create | Create a combo |
PATCH | /api/offers/:code | Staff with products.edit | Edit a combo |
DELETE | /api/offers/:code | Staff with products.delete | Delete a combo |
GET | /api/homepage-sections/:key | Staff with products.view | One home page section |
PUT | /api/homepage-sections/:key | Staff with products.edit | Edit a section: title, content, position, is_visible, product_ids. What is left out stays as it was |
Staff: orders and the overview
| Method | Path | Who can call it | What it does |
|---|---|---|---|
GET | /api/orders | Staff with orders.view | Every order the member may see: status (comma separated), fulfillment, date (today, yesterday or a day), q, sort_by, sort_order, paging |
GET | /api/orders/:id | Staff with orders.view | One order with its timeline |
GET | /api/orders/statistic | Staff with orders.view | Counts per status, and the paid revenue for accounts that see money |
PATCH | /api/orders/:id/status | Staff with orders.edit | Move an order: { status, reason? }. A cancellation needs a reason |
GET | /api/overview | Staff with orders.view | The dashboard's front page: ?range=today or yesterday. Refused to kitchen accounts |
- A move the status line does not allow answers
409 errors.transition_not_allowed; one this account may not make,403 errors.transition_forbidden; confirming an unpaid order,409 errors.payment_required. - A kitchen account sees only its own kitchen's orders, with no prices, and any other order answers
404.
Staff: customers, team and roles
| Method | Path | Who can call it | What it does |
|---|---|---|---|
GET | /api/users | Staff with users.view | Customers, with search and filters |
GET | /api/users/statistic | Staff with users.view | Customer counts |
GET | /api/users/:username | Staff with users.view | One customer |
POST | /api/users | Staff with users.create | Create a customer |
PATCH | /api/users/:username | Staff with users.update | Edit a customer |
PATCH | /api/users/:username/change-password | Staff with users.update | Set a customer's password |
POST | /api/users/:username/make-verified | Staff with users.verify | Mark the email verified |
POST | /api/users/:username/make-unverified | Staff with users.verify | Mark the email unverified |
DELETE | /api/users/:username | Staff with users.delete | Remove a customer |
GET | /api/users/deleted | Staff with users.view | Removed customers |
GET | /api/users/deleted/:username | Staff with users.view | One removed customer |
POST | /api/users/deleted/:username/restore | Staff with users.restore | Restore a customer |
GET | /api/admins | Staff with admins.view | The team |
GET | /api/admins/statistics | Staff with admins.view | Team counts |
GET | /api/admins/:id | Staff with admins.view | One member, by id or username |
POST | /api/admins | Staff with admins.create | Add a member |
PATCH | /api/admins/:id | Staff with admins.edit | Edit a member, including their kitchen |
PATCH | /api/admins/:id/roles | Staff with admins.assign_roles | Set a member's roles |
DELETE | /api/admins/:id | Staff with admins.delete | Remove a member |
PATCH | /api/admins/profile | Staff with admins.edit | Edit one's own profile |
PATCH | /api/admins/profile/password | Any signed-in staff member | Change one's own password |
GET | /api/roles | Staff with roles.view | The roles |
GET | /api/roles/statistics | Staff with roles.view | Role counts |
GET | /api/roles/select | Staff with roles.view | The roles, shaped for a picker |
GET | /api/roles/permissions | Staff with roles.view | Every permission |
GET | /api/roles/:id | Staff with roles.view | One role with its permissions |
POST | /api/roles | Staff with roles.create | Create a role |
PUT | /api/roles/:id | Staff with roles.edit | Rename a role |
POST | /api/roles/:id/permissions | Staff with roles.assign_permissions | Set a role's permissions |
DELETE | /api/roles/:id | Staff with roles.delete | Delete a role |
Staff: settings, kitchens, promo codes, uploads and notifications
| Method | Path | Who can call it | What it does |
|---|---|---|---|
GET | /api/settings | Staff with settings.view | Every stored setting: search, category, paging |
GET | /api/settings/:key | Staff with settings.view | One setting, such as delivery_fee |
PATCH | /api/settings/:key | Staff with settings.edit | Change a setting |
DELETE | /api/settings/:key | Staff with settings.edit | Delete a setting |
POST | /api/kitchens | Staff with settings.edit | Add a kitchen |
PATCH | /api/kitchens/:code | Staff with settings.edit | Edit a kitchen |
DELETE | /api/kitchens/:code | Staff with settings.edit | Delete a kitchen |
GET | /api/promo | Staff with settings.view | The promo codes |
POST | /api/promo | Staff with settings.edit | Create a code: code, percent, excludes_delivery, stackable_with_rewards, is_active |
PATCH | /api/promo/:id | Staff with settings.edit | Edit a code |
DELETE | /api/promo/:id | Staff with settings.edit | Delete a code. Orders keep it as text |
POST | /api/helpers/upload | Any signed-in staff member | Upload an image or a video, multipart/form-data with file, up to 150 MB |
GET | /api/notifications | Any signed-in staff member | The member's 30 newest notifications and the unread count |
PATCH | /api/notifications/read-all | Any signed-in staff member | Mark every notification read |
PATCH | /api/notifications/:id/read | Any signed-in staff member | Mark one read |
DELETE | /api/notifications/:id | Any signed-in staff member | Remove one |
- The settings the restaurant runs on are
delivery_fee,pickup_fee,delivery_minimum,rewards_points_per_dollar,rewards_redeem_points,rewards_redeem_valueandservice_clock(realordemo-fixed), besidesite_name,site_tagline,support_emailandsupport_phone. - An uploaded image is stored as JPEG in several sizes and the answer maps each version to its address. Files go to the bucket when the
R2_*variables are set, and to/media/uploads/…on the API otherwise. - Every notifications route answers the whole feed, so a client replaces what it holds with the answer.
Limits per address
| Route | Limit | Variable |
|---|---|---|
POST /api/auth/login, POST /api/auth/customer/login | 10 failed sign-ins per 15 minutes, per form | RATE_LIMIT_LOGIN |
POST /api/orders | 10 per 10 minutes | RATE_LIMIT_ORDERS |
POST /api/payments/checkout-session | 30 per 10 minutes | RATE_LIMIT_PAYMENT_SESSION |
GET /api/orders/track | 30 per 10 minutes | RATE_LIMIT_TRACKING |
POST /api/auth/customer/register | 10 per hour | RATE_LIMIT_REGISTER |
POST /api/contact | 5 per hour | RATE_LIMIT_CONTACT |
POST /api/auth/customer/forgot-password | 10 per hour, and 5 messages per recipient a day | RATE_LIMIT_PASSWORD_RESET, MAIL_MAX_PER_ADDRESS_PER_DAY |
A request over a limit answers 429 with errors.rate_limited. The counts live in the API's memory, so a restart clears them. Behind a reverse proxy, set TRUST_PROXY so each visitor is counted on their own address.
AI assistant and its conversations
Included with your purchase. Sign in to read, or open it in your download.
The assistant's streaming routes, its model list and the saved conversations.
AI studio
Included with your purchase. Sign in to read, or open it in your download.
The studio's routes: listings, generations, saving results and background removal.
MCP server
Included with your purchase. Sign in to read, or open it in your download.
The MCP endpoint for coding agents and how it is authorised.
Live updates over WebSocket
Included with your purchase. Sign in to read, or open it in your download.
The Socket.IO namespaces the dashboard listens on, their events and how a connection is authorised.
Demo mode routes
Included with your purchase. Sign in to read, or open it in your download.
The routes that exist only on a public demo build.