Next Inventory 6.1.0: App Manager, dark mode, and more Joomla 6.1 "Nyota" Is Here Building the Future of Joomla Extensions
NextSoftware
Next Restaurant — Documentation

REST API

A small public REST surface for integrating what you run alongside Next Restaurant — a kiosk, a mobile app, a partner's ordering front end.

It is off by default. Public endpoints are reachable without an admin session, so they are opt-in rather than opt-out.

The surface is deliberately narrow: read the locations, read the live menu, place an order. It is not a remote-administration interface, and it is not a reporting interface.


Enabling it

  1. Go to Apps and turn on the API module.
  2. Go to Integrations and press Generate token.
  3. Copy the token immediately — see below.

The Integrations screen with the API token, webhook endpoints, event selector and test-webhook action

There are two switches and both must be on. The API module under Apps is the master shutoff; the Enable API checkbox under Integrations is the day-to-day one. Turning either off refuses every request, whoever holds the token.


Authentication

Every request carries a bearer token:

Authorization: Bearer <your-api-token>

X-Api-Token: <your-api-token> is accepted as an alternative for clients that cannot set an Authorization header.

The token is shown exactly once, when you generate it. Only a SHA-256 hash is stored, so it cannot be recovered later — if you lose it, generate a new one. Generating invalidates the previous token immediately, which is also the recovery path if one leaks.

There is one token per site. It is not scoped per integration, so two consumers sharing a site share a token, and regenerating cuts both off.


Conventions

Base URL. Every endpoint is the same URL with a different task:

https://your-site.example/index.php?option=com_nextrestaurant&format=json&task=publicApi.<endpoint>

Response envelope. Every response — success or failure — has this shape:

{ "success": true, "message": null, "messages": null, "data": { } }

The payload you want is always in data, and data repeats success inside it. Read data, not the outer object.

Errors carry a real HTTP status. Do not parse the message to detect failure; check the status code, then read data.message for something to show a human and data.code where present.

Money and numbers. Top-level grand_total is a JSON number; the fields inside data.order are strings with two decimals ("31.50"). Parse defensively — coerce both.

Ignore fields you do not recognise. New fields are added over time; a consumer that rejects unknown keys will break on an upgrade it did not need to care about.


Endpoints

Endpoint Method Purpose
publicApi.locations GET Published locations and their public details
publicApi.menu GET The live, orderable menu for one location
publicApi.createOrder POST Place an order

Locations

GET ?option=com_nextrestaurant&format=json&task=publicApi.locations
curl -s "https://your-site.example/index.php?option=com_nextrestaurant&format=json&task=publicApi.locations" \
  -H "Authorization: Bearer $NR_TOKEN"

Returns every published location. Unpublished ones are never listed.

{
  "success": true,
  "locations": [
    {
      "id": 1,
      "code": "MAIN",
      "title": "Downtown Bistro",
      "currency": "USD",
      "timezone": "America/Chicago",
      "service_modes": "[\"dine_in\",\"takeaway\",\"delivery\",\"qr\"]",
      "opening_hours": "{\"mon\":{\"open\":\"11:00\",\"close\":\"22:00\"}}",
      "tax_rate": "8.25",
      "state": 1
    }
  ]
}

Note: service_modes and opening_hours are returned as JSON-encoded strings, not nested objects — decode them a second time on your side. opening_hours is keyed by monsun; a day that is absent or empty means closed.

The id from here is the location_id every other endpoint needs.


GET ?option=com_nextrestaurant&format=json&task=publicApi.menu&location_id=<id>
curl -s "https://your-site.example/index.php?option=com_nextrestaurant&format=json&task=publicApi.menu&location_id=2" \
  -H "Authorization: Bearer $NR_TOKEN"
Parameter Required Meaning
location_id Yes A published location's id

The menu reflects live availability for that location: unpublished items, items switched off for that branch, and 86'd items are all absent, and the price is the branch's price where one overrides the base. An integration therefore cannot sell something you cannot make.

Items are grouped into categories, in the order you arranged them in the admin.

{
  "success": true,
  "location_id": 2,
  "categories": [
    {
      "id": 2,
      "title": "Starters",
      "items": [
        {
          "id": 1,
          "title": "Garlic Bread",
          "description": "Stone-baked sourdough with roasted garlic butter, melted mozzarella and parsley.",
          "price": 6.5,
          "has_variations": 0,
          "image": "images/nextrestaurant/items/1/gallery/cb901db407fa4277.jpg",
          "prep_time_minutes": 19,
          "calories": null,
          "spice_level": 0,
          "is_alcohol": 0,
          "variations": [],
          "modifier_groups": [],
          "allergens": [
            { "id": 1, "title": "Celery", "presence": "contains" }
          ]
        }
      ]
    },
    {
      "id": 5,
      "title": "Pizza",
      "items": [
        {
          "id": 16,
          "title": "Margherita Pizza",
          "description": "San Marzano tomato, fior di latte and fresh basil on a 48-hour dough.",
          "price": 13,
          "has_variations": 1,
          "image": "images/nextrestaurant/items/16/gallery/ca98f3a212f1aac0.jpg",
          "prep_time_minutes": 23,
          "calories": null,
          "spice_level": 0,
          "is_alcohol": 0,
          "variations": [
            { "id": 1, "title": "Regular", "price": 13, "is_default": 1 },
            { "id": 2, "title": "Large",   "price": 17, "is_default": 0 }
          ],
          "modifier_groups": [
            {
              "id": 2,
              "title": "Extra Toppings",
              "selection_type": "multiple",
              "is_required": 0,
              "min_select": 0,
              "max_select": 4,
              "free_quantity": 0,
              "modifiers": [
                { "id": 5, "title": "Extra Cheese", "price": 1.5,  "is_default": 0, "max_quantity": 1 },
                { "id": 6, "title": "Mushrooms",    "price": 1,    "is_default": 0, "max_quantity": 1 },
                { "id": 7, "title": "Olives",       "price": 1,    "is_default": 0, "max_quantity": 1 },
                { "id": 8, "title": "Jalapenos",    "price": 0.75, "is_default": 0, "max_quantity": 1 }
              ]
            }
          ],
          "allergens": [
            { "id": 5,  "title": "Fish", "presence": "contains" },
            { "id": 14, "title": "Sulphur dioxide / sulphites", "presence": "contains" }
          ]
        }
      ]
    }
  ]
}

Item fields

Field Meaning
price The branch price for the item as ordered with no variation
has_variations 1 when the item is sold in sizes or versions
image Path relative to the site root, or null. Prefix with your site URL to display it
prep_time_minutes, calories, spice_level, is_alcohol Optional presentation data; any may be null or 0
variations Sizes/versions. Empty when the item has none
modifier_groups Option groups attached to this item
allergens Declared allergens; presence is contains or may_contain

Variations

When variations is non-empty, pick one and send its id. The variation's price replaces the item's price — it is not added to it. is_default marks the one to pre-select.

Submitting an item that has variations without a variation_id is accepted, and is charged at the base price with no size recorded. That is almost never what a customer meant, so treat a missing selection as a validation error in your own UI.

Modifier groups

Field Meaning
selection_type single (choose one) or multiple (choose several)
is_required 1 when the customer must choose
min_select / max_select How many modifiers may be chosen from this group
free_quantity How many are included before the per-modifier price applies
modifiers[].max_quantity How many times one modifier may be repeated

min_select and max_select already include any per-item override, so use them as given.

Modifier prices are added to the line.


Create order

POST ?option=com_nextrestaurant&format=json&task=publicApi.createOrder
Content-Type: application/json
curl -s -X POST "https://your-site.example/index.php?option=com_nextrestaurant&format=json&task=publicApi.createOrder" \
  -H "Authorization: Bearer $NR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "location_id": 2,
    "channel": "web",
    "service_mode": "takeaway",
    "notes": "Ring the bell at the side door",
    "customer": { "name": "Dana Whitfield", "phone": "+1 555 0142", "email": "[email protected]" },
    "items": [
      { "item_id": 16, "variation_id": 2, "qty": 1, "notes": "Well done",
        "modifiers": [ { "modifier_id": 5, "qty": 1 } ] },
      { "item_id": 1, "qty": 2 }
    ]
  }'

Request body

Field Required Meaning
location_id Yes Where the order is placed
items Yes At least one line — see below
channel No web (default), phone, takeaway or delivery. Anything else falls back to web. Recorded on the order and visible in reports
service_mode No Free text stored with the order, e.g. takeaway, dine_in
notes No Order-level note, shown to the kitchen
customer No { name, phone, email } — all optional, stored with the order

Each entry in items:

Field Required Meaning
item_id Yes Menu item id from the menu endpoint
qty Yes Quantity
variation_id When the item has variations Chosen variation
modifiers No [{ "modifier_id": 5, "qty": 1 }, …]
notes No Line note, e.g. "no ice"

A body wrapped as {"data": { … }} is also accepted, for clients that always send that envelope.

Response

The created record, with its priced lines:

{
  "success": true,
  "id": 580,
  "order_number": "ORD-000580",
  "grand_total": 34.1,
  "status": "placed",
  "order": {
    "id": 580,
    "order_number": "ORD-000580",
    "location_id": 2,
    "channel": "web",
    "status": "placed",
    "fulfillment_status": "in_kitchen",
    "subtotal": "31.50",
    "discount_total": "0.00",
    "tax_total": "2.60",
    "service_charge": "0.00",
    "delivery_fee": "0.00",
    "grand_total": "34.10",
    "currency": null,
    "notes": "Ring the bell at the side door",
    "placed_at": "2026-09-07 20:42:51",
    "items": [
      {
        "id": 1290,
        "item_id": 16,
        "variation_id": 2,
        "name_snapshot": "Margherita Pizza (Large)",
        "qty": "1.000",
        "unit_price": "17.00",
        "modifiers_total": "1.50",
        "line_total": "18.50",
        "notes": "Well done"
      },
      {
        "id": 1291,
        "item_id": 1,
        "variation_id": null,
        "name_snapshot": "Garlic Bread",
        "qty": "2.000",
        "unit_price": "6.50",
        "modifiers_total": "0.00",
        "line_total": "13.00",
        "notes": null
      }
    ]
  }
}

name_snapshot is what the kitchen and the receipt will show — it records the item as it was priced, including the variation. placed_at is in the site's configured timezone.

Note: order.currency is often null. Take the currency from the location instead.

What the totals mean

Totals are recomputed on the server from your menu records. A submitted price is ignored, so a tampered request buys nothing at the wrong price — and so your integration never has to replicate your tax or pricing rules. In the example above: 17.00 + 1.50 for the pizza plus 6.50 × 2 for the bread gives a 31.50 subtotal, and tax brings the grand_total to 34.10.

The order's status is always placed. It cannot be set by the caller.

What an API order becomes

An ordinary order. It routes to kitchen stations by your routing rules, depletes stock at whatever status you configured, settles into the payments ledger, and appears in reports with its channel recorded — exactly like an order from the storefront.

Orders arrive unpaid. The API records an order; it does not take payment. Collect at the counter, on delivery, or through your own checkout.


Errors

Status When Body
400 Missing or malformed input {"success": false, "message": "location_id is required"}
400 An item is unpublished, 86'd or unavailable at that location {"success": false, "message": "One of the items is no longer available."}
401 Token missing, wrong, or none has been generated {"success": false, "message": "Invalid API token"}
403 The Enable API checkbox is off {"success": false, "message": "API is disabled"}
403 The API module is off under Apps {"success": false, "message": "This module is disabled.", "data": {"code": "MODULE_DISABLED"}}
404 location_id does not exist or is unpublished {"success": false, "message": "Unknown location"}
404 Endpoint name misspelled {"success": false, "message": "Unknown task.", "data": {"code": "UNKNOWN_TASK"}}
429 Too many orders from one address {"success": false, "message": "Too many orders from this address. Try again shortly.", "data": {"code": "RATE_LIMITED"}}
500 Something failed server-side {"success": false, "message": "The order could not be created."}

The two 403s are different switches — check both Apps and Integrations when you see one.

A 500 is deliberately vague to the caller; the detail is written to the Joomla log (administrator/logs/), which is where to look when an order will not go through.


Rate limits

Order creation is limited to 60 orders per IP address per 5 minutes. Beyond that you get 429 and no order is created — the response is safe to retry after a pause.

The read endpoints are not rate limited, but they are not free either: fetch the menu on a timer or on cache miss, not on every page view.


CORS

The API emits CORS headers so a browser-based front end on another origin can call it, and answers preflight OPTIONS with 204:

Access-Control-Allow-Origin: *
Access-Control-Allow-Headers: Authorization, X-Api-Token, Content-Type, X-CSRF-Token
Access-Control-Allow-Methods: GET, POST, OPTIONS

The allowed origin defaults to *. Set Allowed CORS origins in Integrations to a comma-separated list to narrow it.

Calling the API from browser JavaScript puts your token in the page, where any visitor can read it and place orders with it. Restricting the origin does not prevent that — CORS is enforced by browsers, not by your server, and a token lifted from a page can be replayed with curl. Call the API from your own backend and let your front end talk to that.


A worked example

Placing an order end to end, in the order a kiosk would do it:

BASE="https://your-site.example/index.php?option=com_nextrestaurant&format=json"
AUTH="Authorization: Bearer $NR_TOKEN"

# 1. Which branches exist?
curl -s "$BASE&task=publicApi.locations" -H "$AUTH" | jq '.data.locations[] | {id, title}'

# 2. What can branch 2 sell right now?
curl -s "$BASE&task=publicApi.menu&location_id=2" -H "$AUTH" \
  | jq '.data.categories[].items[] | {id, title, price, has_variations}'

# 3. What are the options on item 16?
curl -s "$BASE&task=publicApi.menu&location_id=2" -H "$AUTH" \
  | jq '.data.categories[].items[] | select(.id == 16) | {variations, modifier_groups}'

# 4. Place the order.
curl -s -X POST "$BASE&task=publicApi.createOrder" -H "$AUTH" \
  -H "Content-Type: application/json" \
  -d '{"location_id":2,"items":[{"item_id":16,"variation_id":2,"qty":1}]}' \
  | jq '.data | {order_number, grand_total, status}'

Practical advice

  • Cache the menu, briefly. It changes when staff 86 an item, so a 30–60 second cache keeps the kiosk responsive without selling something the kitchen ran out of ten minutes ago.
  • Re-fetch the menu before submitting a long-lived basket, and re-check the ids you are about to send. A 400 One of the items is no longer available at checkout is the item having gone off sale while the customer was deciding.
  • Retrying is not free. There is no idempotency key, so a retried createOrder that actually succeeded the first time creates a second order. On a timeout, treat the outcome as unknown and reconcile rather than blindly retrying.
  • Use webhooks for "what happened next". The API has no endpoint to read an order back after it is created, so keep the order_number from the response and subscribe to Webhooks for status changes. Polling is the wrong shape here, and there is nothing to poll.

What the API is not

Being explicit avoids disappointment:

  • It is not an admin API. There is no endpoint to edit menu items, manage staff, or change settings. Administration happens in the admin.
  • It is not a reporting API. Reports are read in the admin, or exported as CSV.
  • It cannot read orders back. There is no order-status endpoint. Use webhooks.
  • It does not take payment. Orders arrive unpaid.
  • It does not replace webhooks. For "tell me when something happens", use Webhooks.

Security notes

  • Treat the token as a credential. Anyone holding it can read your menu and place orders.
  • Keep it server-side. See the CORS warning above.
  • Use HTTPS. The token transits on every request.
  • Regenerate on suspicion. One click, and the old token dies immediately.
  • Turn the module off if you're not using it. An unused enabled API is a surface for no benefit.

See also

  • Webhooks — outbound events, and the only way to follow an order
  • Apps & Modules — enabling the API module
  • Online Ordering — what an API order becomes
  • Menu — variations, modifier groups and allergens in the admin

All Extensions