Endpoint details
Endpoints
| Purpose | REST | Query-string |
|---|---|---|
| Published locations | GET /locations |
task=publicApi.locations |
| A location's live menu | GET /locations/{id}/menu |
task=publicApi.menu&location_id={id} |
| Place an order | POST /orders |
task=publicApi.createOrder |
Locations
REST GET /api/index.php/v1/nextrestaurant/locations
Query-string GET /index.php?option=com_nextrestaurant&format=json&task=publicApi.locations
curl -s "https://your-site.example/api/index.php/v1/nextrestaurant/locations" \
-H "Authorization: Bearer $NR_TOKEN"
Returns every published location. Unpublished ones are never listed.
On REST, service_modes is an array, opening_hours an object and tax_rate a number:
{
"data": [
{
"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" },
"sat": { "open": "10:00", "close": "23:30" }
},
"tax_rate": 8.25
}
]
}
The query-string lane returns the same data exactly as stored, which means two of those fields arrive as JSON-encoded strings and need a second decode:
{
"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
}
]
}
opening_hours is keyed by mon–sun on both lanes; a day that is absent or empty means closed.
The query-string lane also carries a state field, which is always 1 because unpublished
locations are never returned.
The id from here is the location id every other endpoint needs.
Menu
REST GET /api/index.php/v1/nextrestaurant/locations/{id}/menu
Query-string GET /index.php?option=com_nextrestaurant&format=json&task=publicApi.menu&location_id={id}
curl -s "https://your-site.example/api/index.php/v1/nextrestaurant/locations/2/menu" \
-H "Authorization: Bearer $NR_TOKEN"
| Parameter | Required | Meaning |
|---|---|---|
id (REST path) / location_id (query-string) |
Yes | A published location's id |
Both lanes return the same item structure. On REST it is {"data": {"location_id": …, "categories": [ … ]}}; on the query-string lane the same object sits inside Joomla's envelope.
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
REST POST /api/index.php/v1/nextrestaurant/orders -> 201 Created
Query-string POST /index.php?...&task=publicApi.createOrder -> 200 OK
Content-Type: application/json
curl -s -X POST "https://your-site.example/api/index.php/v1/nextrestaurant/orders" \
-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.currencyis oftennull. 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.