{
    "specVersion": "3.1.0",
    "specFormat": "openapi",
    "specFile": "openapi.yaml",
    "installSpecPath": "/media/com_nextrestaurant/openapi.yaml",
    "title": "Next Restaurant REST API",
    "summary": "Read a restaurant's locations and live menu, and place orders.",
    "description": "The public integration surface for Next Restaurant — enough to run a kiosk, a\nmobile app or a partner ordering front end. It is deliberately narrow: it is not\nan administration API, not a reporting API, and it does not take payment.\n\n**It is off by default.** Two switches must both be on: the **API** module under\n*Apps*, and **Enable API** under *Integrations*. Then generate the bearer token\nunder *Integrations* — it is shown once and stored only as a hash.\n\nTotals are always recomputed on the server from the menu records, so a submitted\nprice is ignored. An integration never has to replicate your tax or pricing rules,\nand a tampered request buys nothing at the wrong price.\n",
    "apiVersion": "1.0.0",
    "license": {
        "name": "GNU General Public License v2.0 or later",
        "url": "https://www.gnu.org/licenses/old-licenses/gpl-2.0.html"
    },
    "contact": {
        "name": "NextSoftware",
        "url": "https://nextsoftware.dev/",
        "email": "support@nextsoftware.dev"
    },
    "style": "rest-path",
    "guide": "integrations/rest-api",
    "baseUrl": {
        "template": "{protocol}://{host}{basePath}/api/index.php",
        "variables": {
            "protocol": {
                "description": "Use http only for local development.",
                "example": "https",
                "enum": [
                    "https",
                    "http"
                ]
            },
            "host": {
                "description": "The host your Joomla site runs on.",
                "example": "your-site.example",
                "enum": null
            },
            "basePath": {
                "description": "Empty when Joomla is installed at the domain root. Otherwise the subfolder it is installed in, with a leading slash and no trailing slash — for example /restaurant.",
                "example": "",
                "enum": null
            }
        },
        "example": "https://your-site.example/api/index.php"
    },
    "auth": {
        "type": "bearer",
        "header": "Authorization",
        "scheme": "Bearer",
        "queryParam": null,
        "example": "Authorization: Bearer <your-api-token>",
        "alternatives": [
            {
                "type": "header",
                "name": "X-Api-Token",
                "note": "Alternative to the bearer scheme, for clients that cannot set `Authorization`."
            }
        ],
        "enablement": [],
        "notes": [
            "The API token generated under *Integrations*. Shown once at generation and\nstored only as a SHA-256 hash, so it cannot be recovered — generate a new\none if it is lost, which also invalidates the old one immediately.\n\nKeep it server-side. Calling this API from browser JavaScript puts the token\nin the page where any visitor can read it, and restricting origins does not\nhelp: CORS is enforced by browsers, not by the server.\n"
        ]
    },
    "conventions": [],
    "groups": [
        {
            "id": "Locations",
            "title": "Locations",
            "description": "The branches an integration may order from."
        },
        {
            "id": "Menu",
            "title": "Menu",
            "description": "What a branch can sell right now."
        },
        {
            "id": "Orders",
            "title": "Orders",
            "description": "Placing orders."
        }
    ],
    "endpoints": [
        {
            "id": "listLocations",
            "slug": "locations",
            "pagePath": "api/locations",
            "group": "Locations",
            "method": "GET",
            "operation": "/v1/nextrestaurant/locations",
            "summary": "List published locations",
            "title": "List Locations",
            "description": "Every published location with its public details. Unpublished locations are\nnever returned. The `id` here is what the menu and order endpoints need.\n",
            "stability": "stable",
            "deprecated": null,
            "auth": true,
            "params": [],
            "requestBody": null,
            "responses": [
                {
                    "status": 200,
                    "description": "The published locations.",
                    "example": {
                        "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
                            },
                            {
                                "id": 2,
                                "code": "RIVER",
                                "title": "Riverside Grill",
                                "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
                            }
                        ]
                    },
                    "fields": [
                        {
                            "path": "data[].id",
                            "type": "integer",
                            "description": "(int64)"
                        },
                        {
                            "path": "data[].code",
                            "type": "string",
                            "description": "Your short branch code."
                        },
                        {
                            "path": "data[].title",
                            "type": "string",
                            "description": ""
                        },
                        {
                            "path": "data[].currency",
                            "type": "string",
                            "description": "ISO 4217 currency code."
                        },
                        {
                            "path": "data[].timezone",
                            "type": "string",
                            "description": "IANA timezone name."
                        },
                        {
                            "path": "data[].service_modes[]",
                            "type": "string[]",
                            "description": "Which ways this branch serves guests. One of `dine_in`, `takeaway`, `delivery`, `qr`."
                        },
                        {
                            "path": "data[].opening_hours",
                            "type": "object",
                            "description": "Keyed `mon`–`sun`. A day that is absent, null or empty means closed."
                        },
                        {
                            "path": "data[].tax_rate",
                            "type": "number|null",
                            "description": "Percentage, e.g. 8.25 for 8.25%."
                        }
                    ],
                    "notes": ""
                }
            ],
            "errors": [
                "invalid_token",
                "module_disabled",
                "api_disabled"
            ],
            "rateLimit": null,
            "samples": [
                {
                    "lang": "bash",
                    "label": "curl",
                    "code": "curl -s \"https://your-site.example/api/index.php/v1/nextrestaurant/locations\" \\\n  -H \"Authorization: Bearer $API_TOKEN\""
                }
            ],
            "sections": [],
            "seoTitle": "Locations endpoint",
            "seoDescription": "GET /locations returns every published Next Restaurant branch with its currency, timezone, service modes, opening hours and tax rate.",
            "seoKeywords": "list locations, branches, opening hours, service modes, tax rate, restaurant api"
        },
        {
            "id": "getMenu",
            "slug": "locations-menu",
            "pagePath": "api/locations-menu",
            "group": "Menu",
            "method": "GET",
            "operation": "/v1/nextrestaurant/locations/{locationId}/menu",
            "summary": "The live, orderable menu for a location",
            "title": "Get Menu",
            "description": "Categories of items, in the order arranged in the admin, reflecting live\navailability **for this branch**: unpublished items, items switched off for\nthe location and 86'd items are absent, and `price` is the branch's price\nwhere one overrides the base.\n\nEach item carries everything needed to order it — its `variations`, its\n`modifier_groups` with the per-item min/max overrides already applied, and\nits declared `allergens`.\n",
            "stability": "stable",
            "deprecated": null,
            "auth": true,
            "params": [
                {
                    "name": "locationId",
                    "in": "path",
                    "type": "integer",
                    "required": true,
                    "default": null,
                    "enum": null,
                    "example": 2,
                    "description": "A published location's id. (min 1, int64)"
                }
            ],
            "requestBody": null,
            "responses": [
                {
                    "status": 200,
                    "description": "The menu.",
                    "example": {
                        "data": {
                            "location_id": 2,
                            "categories": [
                                {
                                    "id": 5,
                                    "title": "Pizza",
                                    "items": [
                                        {
                                            "id": 16,
                                            "title": "Margherita Pizza",
                                            "description": "San Marzano tomato, fior di latte and fresh basil.",
                                            "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
                                                        }
                                                    ]
                                                }
                                            ],
                                            "allergens": [
                                                {
                                                    "id": 5,
                                                    "title": "Fish",
                                                    "presence": "contains"
                                                }
                                            ]
                                        }
                                    ]
                                }
                            ]
                        }
                    },
                    "fields": [
                        {
                            "path": "data.location_id",
                            "type": "integer",
                            "description": "(int64)"
                        },
                        {
                            "path": "data.categories[].id",
                            "type": "integer",
                            "description": "(int64)"
                        },
                        {
                            "path": "data.categories[].title",
                            "type": "string",
                            "description": ""
                        },
                        {
                            "path": "data.categories[].items[].id",
                            "type": "integer",
                            "description": "(int64)"
                        },
                        {
                            "path": "data.categories[].items[].title",
                            "type": "string",
                            "description": ""
                        },
                        {
                            "path": "data.categories[].items[].description",
                            "type": "string|null",
                            "description": ""
                        },
                        {
                            "path": "data.categories[].items[].price",
                            "type": "number",
                            "description": "The branch price for the item ordered with no variation. When\n`variations` is non-empty the chosen variation's price replaces this —\nit is not added to it."
                        },
                        {
                            "path": "data.categories[].items[].has_variations",
                            "type": "integer",
                            "description": "1 when the item is sold in sizes or versions. One of `0`, `1`."
                        },
                        {
                            "path": "data.categories[].items[].image",
                            "type": "string|null",
                            "description": "Path relative to the site root. Prefix with your site URL to display it."
                        },
                        {
                            "path": "data.categories[].items[].prep_time_minutes",
                            "type": "integer|null",
                            "description": ""
                        },
                        {
                            "path": "data.categories[].items[].calories",
                            "type": "integer|null",
                            "description": ""
                        },
                        {
                            "path": "data.categories[].items[].spice_level",
                            "type": "integer|null",
                            "description": ""
                        },
                        {
                            "path": "data.categories[].items[].is_alcohol",
                            "type": "integer",
                            "description": "One of `0`, `1`."
                        },
                        {
                            "path": "data.categories[].items[].variations[]",
                            "type": "object[]",
                            "description": "Sizes or versions. Empty when the item has none. When it is non-empty,\npick one and send its `id` — submitting the item without a\n`variation_id` is accepted and charged at the base `price` with no size\nrecorded, which is almost never what a customer meant."
                        },
                        {
                            "path": "data.categories[].items[].variations[].id",
                            "type": "integer",
                            "description": "(int64)"
                        },
                        {
                            "path": "data.categories[].items[].variations[].title",
                            "type": "string",
                            "description": ""
                        },
                        {
                            "path": "data.categories[].items[].variations[].price",
                            "type": "number",
                            "description": "Replaces the item's price; not added to it."
                        },
                        {
                            "path": "data.categories[].items[].variations[].is_default",
                            "type": "integer",
                            "description": "One of `0`, `1`."
                        },
                        {
                            "path": "data.categories[].items[].modifier_groups[].id",
                            "type": "integer",
                            "description": "(int64)"
                        },
                        {
                            "path": "data.categories[].items[].modifier_groups[].title",
                            "type": "string",
                            "description": ""
                        },
                        {
                            "path": "data.categories[].items[].modifier_groups[].selection_type",
                            "type": "string",
                            "description": "One of `single`, `multiple`."
                        },
                        {
                            "path": "data.categories[].items[].modifier_groups[].is_required",
                            "type": "integer",
                            "description": "One of `0`, `1`."
                        },
                        {
                            "path": "data.categories[].items[].modifier_groups[].min_select",
                            "type": "integer",
                            "description": "Per-item overrides are already applied — use as given."
                        },
                        {
                            "path": "data.categories[].items[].modifier_groups[].max_select",
                            "type": "integer",
                            "description": "Per-item overrides are already applied — use as given."
                        },
                        {
                            "path": "data.categories[].items[].modifier_groups[].free_quantity",
                            "type": "integer",
                            "description": "How many are included before the per-modifier price applies."
                        },
                        {
                            "path": "data.categories[].items[].modifier_groups[].modifiers[].id",
                            "type": "integer",
                            "description": "(int64)"
                        },
                        {
                            "path": "data.categories[].items[].modifier_groups[].modifiers[].title",
                            "type": "string",
                            "description": ""
                        },
                        {
                            "path": "data.categories[].items[].modifier_groups[].modifiers[].price",
                            "type": "number",
                            "description": "Added to the line."
                        },
                        {
                            "path": "data.categories[].items[].modifier_groups[].modifiers[].is_default",
                            "type": "integer",
                            "description": "One of `0`, `1`."
                        },
                        {
                            "path": "data.categories[].items[].modifier_groups[].modifiers[].max_quantity",
                            "type": "integer|null",
                            "description": "How many times this modifier may be repeated on one line."
                        },
                        {
                            "path": "data.categories[].items[].allergens[].id",
                            "type": "integer",
                            "description": "(int64)"
                        },
                        {
                            "path": "data.categories[].items[].allergens[].title",
                            "type": "string",
                            "description": ""
                        },
                        {
                            "path": "data.categories[].items[].allergens[].presence",
                            "type": "string",
                            "description": "One of `contains`, `may_contain`."
                        }
                    ],
                    "notes": ""
                }
            ],
            "errors": [
                "invalid_token",
                "module_disabled",
                "api_disabled",
                "unknown_location"
            ],
            "rateLimit": null,
            "samples": [
                {
                    "lang": "bash",
                    "label": "curl",
                    "code": "curl -s \"https://your-site.example/api/index.php/v1/nextrestaurant/locations/2/menu\" \\\n  -H \"Authorization: Bearer $API_TOKEN\""
                }
            ],
            "sections": [],
            "seoTitle": "Menu endpoint",
            "seoDescription": "GET /locations/{id}/menu returns the live orderable menu for one branch: categories, items, variations, modifier groups and declared allergens.",
            "seoKeywords": "menu api, live menu, variations, modifier groups, allergens, 86'd items, branch price"
        },
        {
            "id": "createOrder",
            "slug": "orders",
            "pagePath": "api/orders",
            "group": "Orders",
            "method": "POST",
            "operation": "/v1/nextrestaurant/orders",
            "summary": "Place an order",
            "title": "Create Order",
            "description": "Creates an ordinary order: it routes to kitchen stations by your routing\nrules, depletes stock at the status you configured, settles into the payments\nledger, and appears in reports with its channel recorded.\n\nOrders arrive **unpaid** — this endpoint records an order, it does not take\npayment.\n\nThere is no idempotency key. A retried request that actually succeeded the\nfirst time creates a second order, so on a timeout reconcile rather than\nblindly retrying.\n",
            "stability": "stable",
            "deprecated": null,
            "auth": true,
            "params": [],
            "requestBody": {
                "contentType": "application/json",
                "required": true,
                "description": "",
                "fields": [
                    {
                        "name": "location_id",
                        "type": "integer",
                        "required": true,
                        "description": "Where the order is placed. (int64)",
                        "enum": null,
                        "default": null,
                        "children": []
                    },
                    {
                        "name": "items",
                        "type": "object[]",
                        "required": true,
                        "description": "",
                        "enum": null,
                        "default": null,
                        "children": [
                            {
                                "name": "item_id",
                                "type": "integer",
                                "required": true,
                                "description": "(int64)",
                                "enum": null,
                                "default": null,
                                "children": []
                            },
                            {
                                "name": "qty",
                                "type": "number",
                                "required": true,
                                "description": "",
                                "enum": null,
                                "default": null,
                                "children": []
                            },
                            {
                                "name": "variation_id",
                                "type": "integer|null",
                                "required": false,
                                "description": "Required in practice whenever the item has variations. (int64)",
                                "enum": null,
                                "default": null,
                                "children": []
                            },
                            {
                                "name": "notes",
                                "type": "string",
                                "required": false,
                                "description": "",
                                "enum": null,
                                "default": null,
                                "children": []
                            },
                            {
                                "name": "modifiers",
                                "type": "object[]",
                                "required": false,
                                "description": "",
                                "enum": null,
                                "default": null,
                                "children": [
                                    {
                                        "name": "modifier_id",
                                        "type": "integer",
                                        "required": true,
                                        "description": "(int64)",
                                        "enum": null,
                                        "default": null,
                                        "children": []
                                    },
                                    {
                                        "name": "qty",
                                        "type": "integer",
                                        "required": false,
                                        "description": "",
                                        "enum": null,
                                        "default": 1,
                                        "children": []
                                    }
                                ]
                            }
                        ]
                    },
                    {
                        "name": "channel",
                        "type": "string",
                        "required": false,
                        "description": "Recorded on the order and visible in reports. Any other value falls back\nto `web` rather than being refused.\n",
                        "enum": [
                            "web",
                            "phone",
                            "takeaway",
                            "delivery"
                        ],
                        "default": "web",
                        "children": []
                    },
                    {
                        "name": "service_mode",
                        "type": "string",
                        "required": false,
                        "description": "Free text stored with the order.",
                        "enum": null,
                        "default": null,
                        "children": []
                    },
                    {
                        "name": "notes",
                        "type": "string",
                        "required": false,
                        "description": "Order-level note, shown to the kitchen.",
                        "enum": null,
                        "default": null,
                        "children": []
                    },
                    {
                        "name": "customer",
                        "type": "object",
                        "required": false,
                        "description": "Guest details stored with the order. Every field optional.",
                        "enum": null,
                        "default": null,
                        "children": [
                            {
                                "name": "name",
                                "type": "string",
                                "required": false,
                                "description": "",
                                "enum": null,
                                "default": null,
                                "children": []
                            },
                            {
                                "name": "phone",
                                "type": "string",
                                "required": false,
                                "description": "",
                                "enum": null,
                                "default": null,
                                "children": []
                            },
                            {
                                "name": "email",
                                "type": "string",
                                "required": false,
                                "description": "(email)",
                                "enum": null,
                                "default": null,
                                "children": []
                            }
                        ]
                    }
                ],
                "example": {
                    "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": "dana@example.com"
                    },
                    "items": [
                        {
                            "item_id": 16,
                            "variation_id": 2,
                            "qty": 1,
                            "notes": "Well done",
                            "modifiers": [
                                {
                                    "modifier_id": 5,
                                    "qty": 1
                                }
                            ]
                        },
                        {
                            "item_id": 1,
                            "qty": 2
                        }
                    ]
                }
            },
            "responses": [
                {
                    "status": 201,
                    "description": "The order was created.",
                    "example": {
                        "data": {
                            "id": 591,
                            "order_number": "ORD-000591",
                            "location_id": 2,
                            "channel": "web",
                            "status": "placed",
                            "fulfillment_status": "in_kitchen",
                            "subtotal": 18.5,
                            "discount_total": 0,
                            "tax_total": 1.53,
                            "service_charge": 0,
                            "delivery_fee": 0,
                            "grand_total": 20.03,
                            "currency": null,
                            "notes": null,
                            "placed_at": "2026-09-07 21:20:26",
                            "items": [
                                {
                                    "id": 1300,
                                    "item_id": 16,
                                    "variation_id": 2,
                                    "name_snapshot": "Margherita Pizza (Large)",
                                    "qty": 1,
                                    "unit_price": 17,
                                    "modifiers_total": 1.5,
                                    "line_total": 18.5,
                                    "notes": null
                                }
                            ]
                        }
                    },
                    "fields": [
                        {
                            "path": "data.id",
                            "type": "integer",
                            "description": "(int64)"
                        },
                        {
                            "path": "data.order_number",
                            "type": "string",
                            "description": "(pattern `^ORD-\\d{6}$`)"
                        },
                        {
                            "path": "data.location_id",
                            "type": "integer",
                            "description": "(int64)"
                        },
                        {
                            "path": "data.channel",
                            "type": "string",
                            "description": ""
                        },
                        {
                            "path": "data.status",
                            "type": "string",
                            "description": "Always `placed` on creation; it cannot be set by the caller."
                        },
                        {
                            "path": "data.fulfillment_status",
                            "type": "string|null",
                            "description": ""
                        },
                        {
                            "path": "data.subtotal",
                            "type": "number",
                            "description": ""
                        },
                        {
                            "path": "data.discount_total",
                            "type": "number",
                            "description": ""
                        },
                        {
                            "path": "data.tax_total",
                            "type": "number",
                            "description": ""
                        },
                        {
                            "path": "data.service_charge",
                            "type": "number",
                            "description": ""
                        },
                        {
                            "path": "data.delivery_fee",
                            "type": "number",
                            "description": ""
                        },
                        {
                            "path": "data.grand_total",
                            "type": "number",
                            "description": ""
                        },
                        {
                            "path": "data.currency",
                            "type": "string|null",
                            "description": "Often null — take the currency from the location."
                        },
                        {
                            "path": "data.notes",
                            "type": "string|null",
                            "description": ""
                        },
                        {
                            "path": "data.placed_at",
                            "type": "string|null",
                            "description": "`Y-m-d H:i:s` in the site's configured timezone."
                        },
                        {
                            "path": "data.items[].id",
                            "type": "integer",
                            "description": "(int64)"
                        },
                        {
                            "path": "data.items[].item_id",
                            "type": "integer",
                            "description": "(int64)"
                        },
                        {
                            "path": "data.items[].variation_id",
                            "type": "integer|null",
                            "description": "(int64)"
                        },
                        {
                            "path": "data.items[].name_snapshot",
                            "type": "string",
                            "description": "The line as the kitchen and the receipt will show it, including the\nvariation."
                        },
                        {
                            "path": "data.items[].qty",
                            "type": "number",
                            "description": ""
                        },
                        {
                            "path": "data.items[].unit_price",
                            "type": "number",
                            "description": "Resolved from the catalogue. A submitted price is ignored."
                        },
                        {
                            "path": "data.items[].modifiers_total",
                            "type": "number",
                            "description": ""
                        },
                        {
                            "path": "data.items[].line_total",
                            "type": "number",
                            "description": ""
                        },
                        {
                            "path": "data.items[].notes",
                            "type": "string|null",
                            "description": ""
                        }
                    ],
                    "notes": ""
                }
            ],
            "errors": [
                "invalid_order",
                "invalid_token",
                "module_disabled",
                "api_disabled",
                "unknown_location",
                "rate_limited",
                "order_failed"
            ],
            "rateLimit": null,
            "samples": [
                {
                    "lang": "bash",
                    "label": "curl",
                    "code": "curl -s -X POST \"https://your-site.example/api/index.php/v1/nextrestaurant/orders\" \\\n  -H \"Authorization: Bearer $API_TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -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\":\"dana@example.com\"},\"items\":[{\"item_id\":16,\"variation_id\":2,\"qty\":1,\"notes\":\"Well done\",\"modifiers\":[{\"modifier_id\":5,\"qty\":1}]},{\"item_id\":1,\"qty\":2}]}'"
                }
            ],
            "sections": [],
            "seoTitle": "Create order endpoint",
            "seoDescription": "POST /orders places an order in Next Restaurant. Totals are recomputed server-side; covers variations, modifiers, the 201 response, rate limits and errors.",
            "seoKeywords": "create order, place order api, kiosk ordering, order totals, rate limit, idempotency"
        }
    ],
    "errors": [
        {
            "id": "invalid_token",
            "status": 401,
            "code": "INVALID_TOKEN",
            "when": "The token is missing, wrong, or none has been generated.",
            "body": {
                "errors": [
                    {
                        "title": "Invalid API token",
                        "code": "INVALID_TOKEN"
                    }
                ]
            },
            "global": false
        },
        {
            "id": "module_disabled",
            "status": 403,
            "code": "MODULE_DISABLED",
            "when": "This module is disabled.",
            "body": {
                "errors": [
                    {
                        "title": "This module is disabled.",
                        "code": "MODULE_DISABLED"
                    }
                ]
            },
            "global": false
        },
        {
            "id": "api_disabled",
            "status": 403,
            "code": "API_DISABLED",
            "when": "API is disabled",
            "body": {
                "errors": [
                    {
                        "title": "API is disabled",
                        "code": "API_DISABLED"
                    }
                ]
            },
            "global": false
        },
        {
            "id": "unknown_location",
            "status": 404,
            "code": "UNKNOWN_LOCATION",
            "when": "No such published location.",
            "body": {
                "errors": [
                    {
                        "title": "Unknown location",
                        "code": "UNKNOWN_LOCATION"
                    }
                ]
            },
            "global": false
        },
        {
            "id": "invalid_order",
            "status": 400,
            "code": "INVALID_ORDER",
            "when": "One of the items is no longer available.",
            "body": {
                "errors": [
                    {
                        "title": "One of the items is no longer available.",
                        "code": "INVALID_ORDER"
                    }
                ]
            },
            "global": false
        },
        {
            "id": "rate_limited",
            "status": 429,
            "code": "RATE_LIMITED",
            "when": "Too many orders from this address — 60 per 5 minutes. No order was\ncreated, so the request is safe to retry after a pause.\n",
            "body": {
                "errors": [
                    {
                        "title": "Too many orders from this address. Try again shortly.",
                        "code": "RATE_LIMITED"
                    }
                ]
            },
            "global": false
        },
        {
            "id": "order_failed",
            "status": 500,
            "code": "ORDER_FAILED",
            "when": "The order could not be created. The message is deliberately generic;\nthe detail is in the Joomla log under `administrator/logs/`.\n",
            "body": {
                "errors": [
                    {
                        "title": "The order could not be created.",
                        "code": "ORDER_FAILED"
                    }
                ]
            },
            "global": false
        }
    ],
    "rateLimits": [],
    "webhooks": {
        "description": "Outbound events. When something happens in Next Restaurant, an HTTP POST is sent to the endpoints you configure — so another system can react without polling.\n\nWebhooks tell you an order *changed*; they do not carry the whole order. There is no endpoint to fetch an order back, so keep the response you got when you created it.",
        "configuredIn": "Integrations",
        "transport": {
            "method": "POST",
            "contentType": "application/json"
        },
        "envelope": {
            "example": {
                "id": "msg_4e9c1b7a2f08d3516ac4be9017d2f5a3",
                "type": "order.placed",
                "event": "order.placed",
                "data": [],
                "timestamp": "2026-08-28T19:30:00+00:00"
            },
            "fields": [
                {
                    "path": "id",
                    "type": "string",
                    "description": "Unique id for this delivery. The same value as the `webhook-id` header — dedupe on it."
                },
                {
                    "path": "type",
                    "type": "string",
                    "description": "The event name."
                },
                {
                    "path": "event",
                    "type": "string",
                    "description": "The same name again, kept for integrations built before `type` existed."
                },
                {
                    "path": "data",
                    "type": "object",
                    "description": "Identifiers for what changed — **not** the whole order."
                },
                {
                    "path": "timestamp",
                    "type": "string",
                    "description": "ISO 8601, when the event was dispatched."
                }
            ]
        },
        "signing": {
            "header": "webhook-signature",
            "algorithm": "HMAC-SHA256",
            "format": "v1,<base64>",
            "signedPayload": "`{webhook-id}.{webhook-timestamp}.{raw body}`",
            "secretSetting": "Signing secret (Integrations)",
            "notes": [
                "Two schemes are sent on every signed delivery and you verify **either one, not both**: the preferred `webhook-signature` (Standard Webhooks, base64) and the legacy `X-NextRestaurant-Signature` (`sha256=` + hex over the raw body only).",
                "An endpoint URL is not a secret — it can be guessed, logged by a proxy or leak from a config file. Without verification, anyone who learns the URL can post fabricated orders to your integration.",
                "Verify **before** parsing or acting on the body, not after. Compute the HMAC over the bytes as received: re-serialising the JSON first changes them and the check will fail. Compare with a constant-time function.",
                "Reject a delivery whose `webhook-timestamp` is more than ~300 seconds old, *before* checking the signature. The legacy scheme has no timestamp in what it signs, so it cannot tell a replay from a fresh delivery.",
                "A `whsec_`-prefixed secret is base64: the HMAC key is the **decoded bytes**. A hand-typed secret is used as-is.",
                "**With no secret set, deliveries go out unsigned** — both signature headers are simply absent, while `webhook-id` and `webhook-timestamp` are still sent. This is the state a new install ships in."
            ],
            "sample": {
                "lang": "php",
                "label": "Verify a Standard Webhooks signature in PHP",
                "code": "$body = file_get_contents('php://input');\n$id   = $_SERVER['HTTP_WEBHOOK_ID'] ?? '';\n$ts   = $_SERVER['HTTP_WEBHOOK_TIMESTAMP'] ?? '';\n\n// Reject stale deliveries before doing any crypto.\nif (!$ts || abs(time() - (int) $ts) > 300) {\n    http_response_code(400);\n    exit;\n}\n\n$secret   = str_starts_with($MY_SECRET, 'whsec_')\n    ? base64_decode(substr($MY_SECRET, 6))\n    : $MY_SECRET;\n$expected = 'v1,' . base64_encode(hash_hmac('sha256', \"$id.$ts.$body\", $secret, true));\n\n$ok = false;\nforeach (explode(' ', $_SERVER['HTTP_WEBHOOK_SIGNATURE'] ?? '') as $candidate) {\n    // Several signatures may be present during a key rotation.\n    if (hash_equals($expected, $candidate)) {\n        $ok = true;\n    }\n}\n\nif (!$ok) {\n    http_response_code(401);\n    exit;\n}"
            }
        },
        "delivery": {
            "retries": 0,
            "ordering": "none",
            "log": "**Integrations** shows recent delivery attempts — whether an endpoint received an event, how it responded, whether it was signed and under which schemes, and the delivery id. It is the first place to look when an integration \"didn't get\" something.",
            "consumerAdvice": [
                "**There are no retries.** One attempt per endpoint with a five-second timeout. An endpoint that is down, slow, or answers outside the 2xx range loses that event permanently — treat webhooks as a fast path, not a guaranteed ledger, and reconcile from your own records if a gap would matter.",
                "**Be idempotent.** Dedupe on `webhook-id`, which is stable for one event across every endpoint it is sent to, so a consumer subscribed twice can tell the second copy is not a second event.",
                "**Respond quickly.** Acknowledge receipt and do the slow work asynchronously.",
                "**The endpoint must be reachable from your server** over HTTPS. A URL on a private network your Joomla host cannot reach will never receive anything.",
                "**Ignore fields you do not recognise.** New fields are added to payloads over time."
            ]
        },
        "selection": "Select only the events you actually consume. Every selected event is delivered to every configured endpoint. Selecting none means *all* of them, not none.",
        "events": [
            {
                "name": "order.placed",
                "slug": "order-placed",
                "pagePath": "api/events/order-placed",
                "summary": "A new order is created, on any channel",
                "trigger": "An order is created from the storefront, QR ordering, the REST API, the admin, or the POS.",
                "description": "`data.id` is the internal order id and the key to join on. Read both `source` and `status` defensively — which one is present depends on where the order came from.\n\n> **Two different `id`s, and they are not interchangeable.** `data.id` is the order; the top-level `id` identifies the *delivery*.",
                "testOnly": false,
                "fields": [],
                "payload": null,
                "variants": [
                    {
                        "when": "Storefront, QR ordering or the REST API",
                        "fields": [
                            {
                                "name": "id",
                                "type": "integer",
                                "description": "Internal order id — the key to join on."
                            },
                            {
                                "name": "location_id",
                                "type": "integer"
                            },
                            {
                                "name": "channel",
                                "type": "string"
                            },
                            {
                                "name": "source",
                                "type": "string",
                                "description": "`storefront`, `qr` or `api`."
                            }
                        ],
                        "payload": {
                            "id": "msg_4e9c1b7a2f08d3516ac4be9017d2f5a3",
                            "type": "order.placed",
                            "event": "order.placed",
                            "data": {
                                "id": 580,
                                "location_id": 2,
                                "channel": "web",
                                "source": "api"
                            },
                            "timestamp": "2026-09-07T20:42:51+00:00"
                        }
                    },
                    {
                        "when": "Created or edited in the admin or the POS",
                        "fields": [
                            {
                                "name": "id",
                                "type": "integer"
                            },
                            {
                                "name": "location_id",
                                "type": "integer"
                            },
                            {
                                "name": "channel",
                                "type": "string"
                            },
                            {
                                "name": "status",
                                "type": "string"
                            }
                        ],
                        "payload": {
                            "id": "msg_8b2f01de44c7a95310fe6b8827c4a1d2",
                            "type": "order.placed",
                            "event": "order.placed",
                            "data": {
                                "id": 581,
                                "location_id": 2,
                                "channel": "pos",
                                "status": "placed"
                            },
                            "timestamp": "2026-09-07T20:44:10+00:00"
                        }
                    }
                ],
                "seoTitle": null,
                "seoDescription": "The order.placed webhook fires when a Next Restaurant order is created on any channel — storefront, QR, REST API, admin or POS. Payload fields and variants.",
                "seoKeywords": "order.placed, webhook payload, order created, source, channel, storefront, qr, pos"
            },
            {
                "name": "order.updated",
                "slug": "order-updated",
                "pagePath": "api/events/order-updated",
                "summary": "An existing order changes",
                "trigger": "An order is created or edited in the admin or the POS.",
                "description": "",
                "testOnly": false,
                "fields": [
                    {
                        "name": "id",
                        "type": "integer",
                        "description": "Internal order id."
                    },
                    {
                        "name": "location_id",
                        "type": "integer"
                    },
                    {
                        "name": "channel",
                        "type": "string"
                    },
                    {
                        "name": "status",
                        "type": "string"
                    }
                ],
                "payload": {
                    "id": "msg_c41a7de90b2f6538a1cd4e07f9b3520e",
                    "type": "order.updated",
                    "event": "order.updated",
                    "data": {
                        "id": 581,
                        "location_id": 2,
                        "channel": "pos",
                        "status": "placed"
                    },
                    "timestamp": "2026-09-07T20:48:31+00:00"
                },
                "variants": [],
                "seoTitle": null,
                "seoDescription": "The order.updated webhook fires when an existing Next Restaurant order changes in the admin or the POS. Payload fields and delivery rules.",
                "seoKeywords": "order.updated, webhook payload, order changed, admin, pos"
            },
            {
                "name": "order.status_changed",
                "slug": "order-status-changed",
                "pagePath": "api/events/order-status-changed",
                "summary": "An order moves to a new status",
                "trigger": "An order's status is advanced in the admin, the POS or the kitchen display.",
                "description": "",
                "testOnly": false,
                "fields": [
                    {
                        "name": "id",
                        "type": "integer",
                        "description": "Internal order id."
                    },
                    {
                        "name": "status",
                        "type": "string",
                        "description": "The status it moved to."
                    }
                ],
                "payload": {
                    "id": "msg_2ef5b0c8137a49d6bb0e5c2a8471f36d",
                    "type": "order.status_changed",
                    "event": "order.status_changed",
                    "data": {
                        "id": 580,
                        "status": "ready"
                    },
                    "timestamp": "2026-09-07T20:51:02+00:00"
                },
                "variants": [],
                "seoTitle": null,
                "seoDescription": "The order.status_changed webhook fires when a Next Restaurant order moves to a new status in the admin, the POS or the kitchen display.",
                "seoKeywords": "order.status_changed, order status, kitchen display, fulfillment, webhook"
            },
            {
                "name": "order.cancelled",
                "slug": "order-cancelled",
                "pagePath": "api/events/order-cancelled",
                "summary": "An order is cancelled, with the reason",
                "trigger": "An order is cancelled.",
                "description": "",
                "testOnly": false,
                "fields": [
                    {
                        "name": "id",
                        "type": "integer",
                        "description": "Internal order id."
                    },
                    {
                        "name": "status",
                        "type": "string"
                    },
                    {
                        "name": "reason",
                        "type": "string",
                        "description": "Free text captured at cancellation."
                    },
                    {
                        "name": "location_id",
                        "type": "integer"
                    }
                ],
                "payload": {
                    "id": "msg_9d7c3a15e820b46fa5c1027de6394bb8",
                    "type": "order.cancelled",
                    "event": "order.cancelled",
                    "data": {
                        "id": 580,
                        "status": "cancelled",
                        "reason": "Customer no-show",
                        "location_id": 2
                    },
                    "timestamp": "2026-09-07T21:03:44+00:00"
                },
                "variants": [],
                "seoTitle": null,
                "seoDescription": "The order.cancelled webhook fires when a Next Restaurant order is cancelled, carrying the order id, status, cancellation reason and location.",
                "seoKeywords": "order.cancelled, cancellation reason, refund, webhook payload"
            },
            {
                "name": "ping",
                "slug": "ping",
                "pagePath": "api/events/ping",
                "summary": "You press Test webhook — for connectivity checks only",
                "trigger": "You press **Test webhook** in Integrations.",
                "description": "Use it to confirm an endpoint is reachable and that your signature verification accepts a real delivery. It never fires on restaurant activity.",
                "testOnly": true,
                "fields": [
                    {
                        "name": "message",
                        "type": "string"
                    },
                    {
                        "name": "at",
                        "type": "string",
                        "description": "ISO 8601."
                    }
                ],
                "payload": {
                    "id": "msg_0a6b48fc25d1937ec4b8021f5da76e39",
                    "type": "ping",
                    "event": "ping",
                    "data": {
                        "message": "Test webhook",
                        "at": "2026-09-07T21:10:00+00:00"
                    },
                    "timestamp": "2026-09-07T21:10:00+00:00"
                },
                "variants": [],
                "seoTitle": null,
                "seoDescription": "The ping webhook is sent when you press Test webhook in Integrations — a connectivity check only. It never fires on real restaurant activity.",
                "seoKeywords": "ping webhook, test webhook, connectivity check, endpoint verification"
            }
        ]
    },
    "seoOverview": {
        "title": "REST API Reference",
        "description": "Next Restaurant REST API reference: bearer token auth, locations, live menu with variations and modifiers, order creation, error codes and webhooks.",
        "keywords": "next restaurant api, rest api, openapi, bearer token, joomla web services, restaurant ordering api"
    },
    "seoEvents": {
        "title": "Webhooks",
        "description": "Next Restaurant webhooks: order.placed, order.updated, order.status_changed, order.cancelled and ping, with payloads and signature verification.",
        "keywords": "webhooks, standard webhooks, hmac signature, order events, delivery log, idempotent consumer"
    },
    "generatedBy": "NextExtManager",
    "generated": "2026-09-09T19:26:44+00:00"
}