Conventions & errors
Conventions
Base URL.
REST https://your-site.example/api/index.php/v1/nextrestaurant
Query-string https://your-site.example/index.php?option=com_nextrestaurant&format=json&task=publicApi.<endpoint>
Response envelope. The two lanes wrap their payloads differently.
// REST — the resource under `data`, and nothing else
{ "data": { } }
// Query-string — Joomla's envelope, with `success` repeated inside
{ "success": true, "message": null, "messages": null, "data": { } }
Either way the payload you want is in data. Read data, not the outer object.
Errors carry a real HTTP status on both lanes. Do not parse the message to detect failure: check the status code, then read the code.
// REST — the same shape the Joomla API application uses for its own errors
{ "errors": [ { "title": "Unknown location", "code": "UNKNOWN_LOCATION" } ] }
// Query-string
{ "success": false, "message": "Unknown location", "data": { "success": false } }
Money and numbers. On REST everything numeric is a JSON number. On the query-string lane the
top-level grand_total is a number while the fields inside data.order are strings with two
decimals ("31.50") — parse defensively there.
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.
Errors
On REST, every failure is {"errors": [{"title": …, "code": …}]}:
| Status | code |
When |
|---|---|---|
400 |
INVALID_ORDER |
Missing input, or an item is unpublished, 86'd or unavailable there |
401 |
INVALID_TOKEN |
Token missing, wrong, or none has been generated |
403 |
API_DISABLED |
The Enable API checkbox is off under Integrations |
403 |
MODULE_DISABLED |
The API module is off under Apps |
404 |
UNKNOWN_LOCATION |
The location does not exist or is not published. Applies to the menu and to creating an order — omitting location_id entirely is the 400 above, not this |
404 |
404 |
No such route, or the wrong verb for it — this one comes from Joomla's router, so code is a number |
429 |
RATE_LIMITED |
Too many orders from one address |
500 |
ORDER_FAILED |
Something failed server-side |
On the query-string lane the same conditions produce Joomla's envelope instead:
| Status | Body |
|---|---|
400 |
{"success": false, "message": "location_id is required"} |
401 |
{"success": false, "message": "Invalid API token"} |
403 |
{"success": false, "message": "API is disabled"} |
403 |
{"success": false, "message": "This module is disabled.", "data": {"code": "MODULE_DISABLED"}} |
404 |
{"success": false, "message": "Unknown location"} |
404 |
{"success": false, "message": "Unknown task.", "data": {"code": "UNKNOWN_TASK"}} |
429 |
{"success": false, "message": "Too many orders …", "data": {"code": "RATE_LIMITED"}} |
500 |
{"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
Both lanes emit CORS headers so a browser-based front end on another origin can call them, and
both answer a 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. The same setting governs both lanes.
Error responses carry the headers too. Without that, a browser hands your JavaScript an opaque network failure with no status to read, and a wrong token looks exactly like the server being down.
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.
OpenAPI
The REST lane ships a machine-readable description in OpenAPI 3.1, and your own site serves it:
https://your-site.example/media/com_nextrestaurant/openapi.yaml
That is the copy installed with your version of the extension, so it describes the API that site actually exposes rather than whatever the current release documents. Load it into Swagger UI, Redoc, Postman or Insomnia, or generate a typed client:
# Interactive docs, no install
npx @redocly/cli preview-docs https://your-site.example/media/com_nextrestaurant/openapi.yaml
# A TypeScript client
npx openapi-typescript https://your-site.example/media/com_nextrestaurant/openapi.yaml -o api.d.ts
Point the host server variable at your site. Both authentication schemes are declared, so a
Swagger UI "Authorize" box accepts your token and the built-in Try it calls work against a live
site.
The document is verified against a running site on every test run — each documented response is validated against its schema — so it describes what the API returns rather than what it was intended to return.
Only the REST lane is described. The query-string lane cannot be, for the reason given at the top of this page.