Skip to the article
Aniq-UI

Food StudioAPI reference

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:

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:

Terminal
curl http://localhost:8000/api/health
Response
{"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:

Error
{
  "success": false,
  "data": null,
  "message": "errors.kitchen_closed",
  "errors": { "email": ["…"] },
  "code": "…"
}
  • Errors a customer sees are keys such as errors.kitchen_closed, errors.outside_area or errors.rate_limited, which the website translates itself. Other messages are sentences in the request's language.
  • errors appears on a validation failure, with the problems per field.
  • code carries a machine-readable reason where one exists, such as changes with a changes list when a price moved before an order was placed.
  • A guarded route with no token answers 401; a token without the permission answers 403.

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.

Menu and orders
{ "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

  1. Sign in as staff

    Terminal
    curl -X POST http://localhost:8000/api/auth/login \
      -H "Content-Type: application/json" \
      -d '{"email":"owner@foodstudio.example","password":"FoodDemo2026!"}'

    data.access_token is the staff token, with token_type Bearer, expires_in (7d by default, JWT_EXPIRATION) and the member with their roles. GET /api/auth/me returns the signed-in member with their permissions.

  2. Call a protected route

    Terminal
    curl http://localhost:8000/api/orders -H "Authorization: Bearer YOUR_TOKEN"
  3. Sign in as a customer

    POST /api/auth/customer/login with the same body answers { token, user }; POST /api/auth/customer/register takes first_name, last_name, email, password (8 characters or more) and optionally phone, and answers the same. The demo customer is sam@foodstudio.example with FoodDemo2026!.

  • 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 401 with the same message.
  • Ten failed sign-ins from one address within 15 minutes, per form, answer 429 until 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.
MethodPathWho can call itWhat it does
POST/api/auth/loginAnyoneStaff sign-in
GET/api/auth/meAny signed-in staff memberThe signed-in member, their roles and permissions
POST/api/auth/customer/registerAnyoneCreate a customer account and sign in. A repeated client_token returns the first attempt's account
POST/api/auth/customer/loginAnyoneCustomer sign-in
GET/api/auth/customer/meCustomerThe profile, with reward_points
PATCH/api/auth/customer/meCustomerEdit first_name, last_name, email, phone
PATCH/api/auth/customer/me/passwordCustomerChange the password: current_password, new_password
POST/api/auth/customer/forgot-passwordAnyoneEmail a reset link. Always answers { sent: true }, for any address
POST/api/auth/customer/reset-passwordAnyoneFinish 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.

ModulePermissions
Teamadmins.view, admins.create, admins.edit, admins.delete, admins.assign_roles
Rolesroles.view, roles.create, roles.edit, roles.delete, roles.assign_permissions
Settings, kitchens and promo codessettings.view, settings.edit
Customersusers.view, users.create, users.update, users.delete, users.restore, users.verify
Categoriescategories.view, categories.create, categories.edit, categories.delete, categories.restore
Menu, offers and homepageproducts.view, products.create, products.edit, products.delete, products.restore
Orders and the overvieworders.view, orders.edit
AIai_studio.use, ai_chat.use, ai_chat.view_models
Seeded roleGrants
OwnerEvery permission
ManagerEvery permission except admins.* and roles.*
Kitchenorders.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
MethodPathWho can call itWhat it does
GET/api/productsAnyone; staff also see hidden rowsThe 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/featuredAnyoneThe featured dishes
GET/api/products/slug/:slugAnyone; staff also see hidden rowsOne dish by its slug, with related dishes
GET/api/products/:idAnyone; staff also see hidden rowsOne dish by id
GET/api/products/:id/relatedAnyoneDishes from the same category, then the best sellers
GET/api/categoriesAnyone; staff also see hidden rowsThe categories, in their menu order
GET/api/categories/rootsAnyoneThe top-level categories
GET/api/categories/slug/:slugAnyone; staff also see hidden rowsOne category by its slug
GET/api/categories/:idAnyone; staff also see hidden rowsOne category by id
GET/api/offersAnyone; staff also see hidden rowsThe combos, with their choices, surcharges and savings
GET/api/offers/:codeAnyone; staff also see hidden rowsOne combo, such as classic-combo
GET/api/homepage-sectionsAnyone; staff also see hidden rowsThe home page's sections, in page order
GET/api/homepage-sections/:key/productsAnyoneThe dishes a section shows, in order
GET/api/reviews/product/:productIdAnyoneA dish's reviews
GET/api/reviews/product/:productId/summaryAnyoneThe 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

MethodPathWho can call itWhat it does
GET/api/kitchensAnyone; staff also see hidden rowsThe kitchens, each with its hours, timezone, preparation time, delivery postcodes and whether it is open now
GET/api/kitchens/:codeAnyone; staff also see hidden rowsOne kitchen, such as central
POST/api/kitchens/checkAnyoneWhich kitchen can serve a postcode
POST /api/kitchens/check
{ "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

MethodPathWho can call itWhat it does
POST/api/checkout/quoteAnyone; a customer token is read when sentPrice a cart against a kitchen without writing anything
POST/api/promo/validateAnyoneWhat a code would take off a subtotal: { code, subtotal }
GET/api/cartCustomerThe signed-in customer's server cart, with its quote
POST/api/cart/itemsCustomerAdd a selection
PATCH/api/cart/items/:idCustomerChange a line's quantity, options or note
DELETE/api/cart/items/:idCustomerRemove a line
DELETE/api/cartCustomerEmpty the cart
PUT/api/cart/metaCustomerChange the kitchen and delivery or pickup
POST/api/cart/mergeCustomerFold a guest cart in on sign-in, never replacing a line
POST /api/checkout/quote
{
  "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_id or an offer_id, with a quantity from 1 to 10 and instructions of 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 a changes list 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: true takes one redemption off for a signed-in customer with enough points.

Placing and tracking an order

MethodPathWho can call itWhat it does
POST/api/ordersAnyone; a customer token is read when sentPlace an order. Needs an Idempotency-Key header
GET/api/orders/myCustomerThe customer's orders, newest first
GET/api/orders/number/:orderNumberCustomerOne of the customer's own orders, such as FS-1042
GET/api/orders/trackAnyoneGuest tracking: ?number=FS-1042&email=…
GET/api/orders/track/statusAnyoneWhere an order stands, for a holder of a tracking token: ?token=…
  • POST /api/orders takes the quote's lines, kitchen_code, fulfillment and address, plus phone, payment_method (stripe, paypal or demo, as GET /api/payments/methods lists), terms_accepted: true, and for a guest name and email. Optional: promo_code, redeem_points, delivery_instructions, locale, accept_changes, and return_url_ok and return_url_cancel, which must be on a CORS_ORIGIN address.
  • It answers 201 with { order, payment, tracking }. payment.kind is redirect, with the provider's page in payment.url, or none for the demo payment, which POST /api/payments/checkout-session then pays. tracking is a signed token, valid for three hours, for GET /api/orders/track/status.
  • The same Idempotency-Key from the same person returns the stored order instead of a second one.
  • A price that moved, or a dish that became unavailable, answers 422 with code: "changes" until the client sends the change's key in accept_changes.
  • Guest tracking answers the same 404 for 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

MethodPathWho can call itWhat it does
GET/api/payments/methodsAnyoneWhat checkout may offer: { methods, hosted, modes }, each method test or live
POST/api/payments/checkout-sessionAnyone; a customer token is read when sentPay for, or reopen the payment of, an unpaid pending order: { order_number, email? }
GET/api/payments/return/stripeThe 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/paypalThe browser, back from PayPal (signed address)Captures the approved payment, then redirects the same way
POST/api/payments/webhooks/:providerStripe, PayPal, or the demo payment's callerstripe, paypal or demo. An unsigned or altered call answers 401

Addresses, saved dishes, rewards, reviews and contact

MethodPathWho can call itWhat it does
GET/api/addressesCustomerThe saved delivery addresses
GET/api/addresses/:idCustomerOne address
POST/api/addressesCustomerSave an address
PATCH/api/addresses/:idCustomerEdit an address
PATCH/api/addresses/:id/defaultCustomerMake it the default
DELETE/api/addresses/:idCustomerDelete an address
GET/api/wishlistCustomerThe saved dishes
PUT/api/wishlist/:productIdCustomerSave a dish
DELETE/api/wishlist/:productIdCustomerRemove a dish
DELETE/api/wishlistCustomerRemove every saved dish
POST/api/wishlist/mergeCustomerAdd a guest's saved dishes on sign-in
GET/api/rewards/meCustomerThe points balance, one redemption's cost and value, and the history
POST/api/reviewsCustomerReview a dish
PATCH/api/reviews/:idCustomerEdit one's own review
DELETE/api/reviews/:idCustomerDelete one's own review
POST/api/contactAnyoneThe contact form: name, email, message, client_token. It reaches staff as a notification

Staff: menu, categories, combos and homepage

MethodPathWho can call itWhat it does
POST/api/productsStaff with products.createCreate a dish
PATCH/api/products/:idStaff with products.editEdit a dish. images and option_groups replace the whole set when sent
PATCH/api/products/:id/availabilityStaff with products.edit{ is_available }: the switch a kitchen flips during service
DELETE/api/products/:idStaff with products.deleteArchive a dish
GET/api/products/deletedStaff with products.deleteThe archived dishes
POST/api/products/deleted/:id/restoreStaff with products.restoreRestore a dish
GET/api/products/statisticStaff with products.viewMenu counts
GET/api/products/draftsAny signed-in staff memberThe dish form's autosaved draft
PUT/api/products/draftsAny signed-in staff memberSave the draft
DELETE/api/products/draftsAny signed-in staff memberDiscard the draft
POST/api/categoriesStaff with categories.createCreate a category
PATCH/api/categories/:idStaff with categories.editEdit a category
DELETE/api/categories/:idStaff with categories.deleteArchive a category
GET/api/categories/deletedStaff with categories.deleteThe archived categories
POST/api/categories/deleted/:id/restoreStaff with categories.restoreRestore a category
GET/api/categories/statisticStaff with categories.viewCategory counts
POST/api/offersStaff with products.createCreate a combo
PATCH/api/offers/:codeStaff with products.editEdit a combo
DELETE/api/offers/:codeStaff with products.deleteDelete a combo
GET/api/homepage-sections/:keyStaff with products.viewOne home page section
PUT/api/homepage-sections/:keyStaff with products.editEdit a section: title, content, position, is_visible, product_ids. What is left out stays as it was

Staff: orders and the overview

MethodPathWho can call itWhat it does
GET/api/ordersStaff with orders.viewEvery order the member may see: status (comma separated), fulfillment, date (today, yesterday or a day), q, sort_by, sort_order, paging
GET/api/orders/:idStaff with orders.viewOne order with its timeline
GET/api/orders/statisticStaff with orders.viewCounts per status, and the paid revenue for accounts that see money
PATCH/api/orders/:id/statusStaff with orders.editMove an order: { status, reason? }. A cancellation needs a reason
GET/api/overviewStaff with orders.viewThe 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

MethodPathWho can call itWhat it does
GET/api/usersStaff with users.viewCustomers, with search and filters
GET/api/users/statisticStaff with users.viewCustomer counts
GET/api/users/:usernameStaff with users.viewOne customer
POST/api/usersStaff with users.createCreate a customer
PATCH/api/users/:usernameStaff with users.updateEdit a customer
PATCH/api/users/:username/change-passwordStaff with users.updateSet a customer's password
POST/api/users/:username/make-verifiedStaff with users.verifyMark the email verified
POST/api/users/:username/make-unverifiedStaff with users.verifyMark the email unverified
DELETE/api/users/:usernameStaff with users.deleteRemove a customer
GET/api/users/deletedStaff with users.viewRemoved customers
GET/api/users/deleted/:usernameStaff with users.viewOne removed customer
POST/api/users/deleted/:username/restoreStaff with users.restoreRestore a customer
GET/api/adminsStaff with admins.viewThe team
GET/api/admins/statisticsStaff with admins.viewTeam counts
GET/api/admins/:idStaff with admins.viewOne member, by id or username
POST/api/adminsStaff with admins.createAdd a member
PATCH/api/admins/:idStaff with admins.editEdit a member, including their kitchen
PATCH/api/admins/:id/rolesStaff with admins.assign_rolesSet a member's roles
DELETE/api/admins/:idStaff with admins.deleteRemove a member
PATCH/api/admins/profileStaff with admins.editEdit one's own profile
PATCH/api/admins/profile/passwordAny signed-in staff memberChange one's own password
GET/api/rolesStaff with roles.viewThe roles
GET/api/roles/statisticsStaff with roles.viewRole counts
GET/api/roles/selectStaff with roles.viewThe roles, shaped for a picker
GET/api/roles/permissionsStaff with roles.viewEvery permission
GET/api/roles/:idStaff with roles.viewOne role with its permissions
POST/api/rolesStaff with roles.createCreate a role
PUT/api/roles/:idStaff with roles.editRename a role
POST/api/roles/:id/permissionsStaff with roles.assign_permissionsSet a role's permissions
DELETE/api/roles/:idStaff with roles.deleteDelete a role

Staff: settings, kitchens, promo codes, uploads and notifications

MethodPathWho can call itWhat it does
GET/api/settingsStaff with settings.viewEvery stored setting: search, category, paging
GET/api/settings/:keyStaff with settings.viewOne setting, such as delivery_fee
PATCH/api/settings/:keyStaff with settings.editChange a setting
DELETE/api/settings/:keyStaff with settings.editDelete a setting
POST/api/kitchensStaff with settings.editAdd a kitchen
PATCH/api/kitchens/:codeStaff with settings.editEdit a kitchen
DELETE/api/kitchens/:codeStaff with settings.editDelete a kitchen
GET/api/promoStaff with settings.viewThe promo codes
POST/api/promoStaff with settings.editCreate a code: code, percent, excludes_delivery, stackable_with_rewards, is_active
PATCH/api/promo/:idStaff with settings.editEdit a code
DELETE/api/promo/:idStaff with settings.editDelete a code. Orders keep it as text
POST/api/helpers/uploadAny signed-in staff memberUpload an image or a video, multipart/form-data with file, up to 150 MB
GET/api/notificationsAny signed-in staff memberThe member's 30 newest notifications and the unread count
PATCH/api/notifications/read-allAny signed-in staff memberMark every notification read
PATCH/api/notifications/:id/readAny signed-in staff memberMark one read
DELETE/api/notifications/:idAny signed-in staff memberRemove one
  • The settings the restaurant runs on are delivery_fee, pickup_fee, delivery_minimum, rewards_points_per_dollar, rewards_redeem_points, rewards_redeem_value and service_clock (real or demo-fixed), beside site_name, site_tagline, support_email and support_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

RouteLimitVariable
POST /api/auth/login, POST /api/auth/customer/login10 failed sign-ins per 15 minutes, per formRATE_LIMIT_LOGIN
POST /api/orders10 per 10 minutesRATE_LIMIT_ORDERS
POST /api/payments/checkout-session30 per 10 minutesRATE_LIMIT_PAYMENT_SESSION
GET /api/orders/track30 per 10 minutesRATE_LIMIT_TRACKING
POST /api/auth/customer/register10 per hourRATE_LIMIT_REGISTER
POST /api/contact5 per hourRATE_LIMIT_CONTACT
POST /api/auth/customer/forgot-password10 per hour, and 5 messages per recipient a dayRATE_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.

Stuck on a step?

Find a fix before you start over.

Troubleshooting

Cookie Preferences

We use cookies to enhance your browsing experience, analyze site traffic, and personalize content. By clicking "Accept All", you consent to our use of cookies for analytics and personalized advertising.