Developers · REST · v1

API

Read and change your menu, pick up orders and tables from your own code. One key, JSON in and out, free on every plan.

Want an assistant to do it instead of code? The same account works from ChatGPT, Claude and Cursor through the MCP server.

Start in a minute

Make a key, then ask for your menus.

Your API keys
Terminal
export GUSTOQR_API_KEY="sk-…"

curl "https://api.gustoqr.com/v1/menus" \
  -H "Authorization: Bearer $GUSTOQR_API_KEY"

Basics

Base addresshttps://api.gustoqr.com/v1
Sign each requestAuthorization: Bearer sk-…
Read-only keysStart with gq_ro_. They answer every GET and refuse the rest with 403. Make one in the box above.
Limits1200 reads and 300 changes a minute per key. More
UndoThe menu as it was before each of its last 50 changes is kept, whether it came from the API, an assistant or the panel. Restore a version
FormatJSON, UTF-8. Send Content-Type: application/json with a body.
OpenAPI filehttps://api.gustoqr.com/v1/openapi.json, no key needed
PriceFree on every plan. Orders and tables need ordering turned on, which comes with Pro.
BrowsersAny origin may call it (CORS is open), but a key in a web page is a key anyone can read. Call from a server.

Errors

A failed request answers with its HTTP status and one shape:

JSON
{
  "error": {
    "code": "unauthorized",
    "message": "The API key is missing, wrong or deleted.",
    "docs": "https://www.gustoqr.com/docs/api"
  }
}
StatusCodeWhat happened
400invalid_jsonThe body is not a JSON object.
401unauthorizedThe key is missing, wrong or deleted.
403forbiddenYour plan or your role does not allow it.
403read_only_keyA read-only key tried to change something.
404not_found, no_versionNo such thing, or it belongs to another account.
422invalid_requestA field is missing or wrong. The message says which.
429rate_limitedOver the limit for this minute. Retry-After says how long to wait.
5xxinternal_error, bad_gatewayOur side. Safe to retry a GET.

Limits

WhatA minuteCounted
Reads (every GET)1200per key
Changes (POST and PATCH)300per key
Without a key (the OpenAPI file, sign-in)300per address

Every answer says where you stand: RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset, the seconds until the count starts again. Over the limit you get a 429 with Retry-After.

A menu has two parts. menu_data is the content: its languages, then categories, each with its items. menu_style is the look. To change the content, read the menu, change menu_data and send it back whole.

Item fieldTypeWhat it is
idstringYours to choose, unique in the menu. Keep it stable: orders and translations follow it.
name, descriptionstringIn the menu’s main language. The other languages are translated for you.
pricenumberIn the menu’s currency, as guests read it: 12 or 12.5.
imagestringThe address of a photo.
dietary, allergensstring[]Diet tags and allergens shown on the item.

Fields you do not know are safe to leave as they are. Do not drop them when you send the menu back.

List menus

GET /menus

Every menu in the account, without its content.

Request
curl "https://api.gustoqr.com/v1/menus" \
  -H "Authorization: Bearer $GUSTOQR_API_KEY"
Response
{
  "menus": [
    {
      "id": 355,
      "name": "Sunset Cafe",
      "restaurant_name": "Sunset Cafe",
      "status": "published",
      "currency": "EUR",
      "url": "https://sunset-cafe.gustoqr.com",
      "visits": 1840,
      "qr_scans": 1215,
      "published_at": "2026-10-02T09:12:44Z",
      "updated_at": "2026-11-12T08:30:02Z"
    }
  ]
}

Create a menu

POST /menus

A new draft menu. Guests see nothing until it is published.

BodyTypeWhat it is
namerequiredstringThe menu’s name in your panel.
currencyrequiredstringISO 4217 code, such as USD, EUR or TRY.
restaurant_namestringThe venue’s name as guests read it. Defaults to name.
taglinestringOne line under the venue’s name.
menu_dataobjectThe content: languages, categories and their items.
Request
curl -X POST "https://api.gustoqr.com/v1/menus" \
  -H "Authorization: Bearer $GUSTOQR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"Sunset Cafe","currency":"EUR","restaurant_name":"Sunset Cafe"}'
Response
{
  "menu": {
    "id": 355,
    "name": "Sunset Cafe",
    "restaurant_name": "Sunset Cafe",
    "status": "draft",
    "currency": "EUR",
    "url": "https://sunset-cafe.gustoqr.com",
    "menu_data": {
      "languages": [
        "en",
        "de"
      ],
      "categories": [
        {
          "id": "cocktails",
          "name": "Cocktails",
          "items": [
            {
              "id": "negroni",
              "name": "Negroni",
              "description": "Gin, Campari, sweet vermouth.",
              "price": 12,
              "dietary": [
                "vegan"
              ],
              "allergens": []
            }
          ]
        }
      ]
    },
    "menu_style": {
      "menuPrimaryLanguage": "en"
    }
  }
}

Get a menu

GET /menus/:id

One menu with its content (menu_data) and its look (menu_style).

Request
curl "https://api.gustoqr.com/v1/menus/355" \
  -H "Authorization: Bearer $GUSTOQR_API_KEY"
Response
{
  "menu": {
    "id": 355,
    "name": "Sunset Cafe",
    "restaurant_name": "Sunset Cafe",
    "status": "published",
    "currency": "EUR",
    "url": "https://sunset-cafe.gustoqr.com",
    "menu_data": {
      "languages": [
        "en",
        "de"
      ],
      "categories": [
        {
          "id": "cocktails",
          "name": "Cocktails",
          "items": [
            {
              "id": "negroni",
              "name": "Negroni",
              "description": "Gin, Campari, sweet vermouth.",
              "price": 12,
              "dietary": [
                "vegan"
              ],
              "allergens": []
            }
          ]
        }
      ]
    },
    "menu_style": {
      "menuPrimaryLanguage": "en"
    }
  }
}

Update a menu

PATCH /menus/:id

Send only what changes. menu_data and menu_style are replaced whole, so read the menu, change it and send it back. On a published menu guests see the change at once; translations into the menu’s other languages follow by themselves. The menu as it was is kept as a version you can restore.

BodyTypeWhat it is
namestringThe menu’s name in your panel.
restaurant_namestringThe venue’s name as guests read it.
currencystringISO 4217 code. Prices are relabelled, not converted.
menu_dataobjectThe whole content.
menu_styleobjectThe whole look.
Request
curl -X PATCH "https://api.gustoqr.com/v1/menus/355" \
  -H "Authorization: Bearer $GUSTOQR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"restaurant_name":"Sunset Cafe"}'
Response
{
  "menu": {
    "id": 355,
    "name": "Sunset Cafe",
    "restaurant_name": "Sunset Cafe",
    "status": "published",
    "currency": "EUR",
    "url": "https://sunset-cafe.gustoqr.com",
    "menu_data": {
      "languages": [
        "en",
        "de"
      ],
      "categories": [
        {
          "id": "cocktails",
          "name": "Cocktails",
          "items": [
            {
              "id": "negroni",
              "name": "Negroni",
              "description": "Gin, Campari, sweet vermouth.",
              "price": 12,
              "dietary": [
                "vegan"
              ],
              "allergens": []
            }
          ]
        }
      ]
    },
    "menu_style": {
      "menuPrimaryLanguage": "en"
    }
  }
}

List a menu’s versions

GET /menus/:id/versions

The menu as it was before each of its last 50 changes, newest first, whoever made them: source is panel (the owner or the panel’s assistant), api, mcp (a connected assistant) or restore. Edits in the panel save by themselves, so they are kept at most once every two minutes.

Request
curl "https://api.gustoqr.com/v1/menus/355/versions" \
  -H "Authorization: Bearer $GUSTOQR_API_KEY"
Response
{
  "versions": [
    {
      "id": "4812",
      "saved_at": "2026-11-12T18:04:11.000Z",
      "source": "mcp",
      "before": "Negroni: €12 → €13"
    }
  ]
}

Restore a version

POST /menus/:id/restore

Undo: puts the menu’s content and look back as a version has them, taking back the change it was kept before and every change after it. The menu as it was a moment ago is kept too, so a restore can be undone. Whether the menu is published does not change.

BodyTypeWhat it is
version_idstringThe version, from the list. Left out: the newest, which undoes the last change.
Request
curl -X POST "https://api.gustoqr.com/v1/menus/355/restore" \
  -H "Authorization: Bearer $GUSTOQR_API_KEY"
Response
{
  "restored": {
    "id": "4812",
    "saved_at": "2026-11-12T18:04:11.000Z",
    "source": "mcp",
    "before": "Negroni: €12 → €13"
  },
  "undo_version_id": "4815",
  "guests_see_it": "now"
}

Publish a menu

POST /menus/:id/publish

Makes a draft live at its own address.

BodyTypeWhat it is
custom_urlstringThe first part of the address: 3 to 30 letters, digits or hyphens.
Request
curl -X POST "https://api.gustoqr.com/v1/menus/355/publish" \
  -H "Authorization: Bearer $GUSTOQR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"custom_url":"sunset-cafe"}'
Response
{
  "url": "https://sunset-cafe.gustoqr.com"
}

Menu analytics

GET /menus/:id/analytics

Visits, QR scans, orders and revenue by day. Best-selling and most-viewed items come with Pro.

QueryTypeWhat it is
daysintegerHow far back, 1 to 365. Default 30.
Request
curl "https://api.gustoqr.com/v1/menus/355/analytics?days=30" \
  -H "Authorization: Bearer $GUSTOQR_API_KEY"
Response
{
  "analytics": {
    "summary": {
      "total_visits": 1840,
      "total_qr_scans": 1215,
      "total_orders": 96,
      "total_revenue": 2890.5
    },
    "daily_stats": [
      {
        "date": "2026-11-11",
        "visits": 71,
        "qr_scans": 48,
        "orders": 5,
        "revenue": 142
      }
    ],
    "popular_items": [],
    "hourly_distribution": {}
  }
}

Orders

List orders

GET /orders

Orders from the POS and from guests ordering on the menu. Poll with date_from to pick up new ones. Ordering comes with Pro.

QueryTypeWhat it is
menu_idintegerOnly this menu’s orders.
statusstringOne status, several with commas, or all. One of: pending, preparing, ready, served, paid, cancelled, all.
date_fromstringA day (2026-11-01) or a moment (2026-11-01T18:00:00Z).
date_tostringThe same, for the end.
Request
curl "https://api.gustoqr.com/v1/orders?status=pending,preparing&date_from=2026-11-12" \
  -H "Authorization: Bearer $GUSTOQR_API_KEY"
Response
{
  "orders": [
    {
      "id": 9120,
      "order_number": "ORD-0042",
      "status": "preparing",
      "table_number": "7",
      "total_amount": 31.5,
      "items": [
        {
          "id": "negroni",
          "name": "Negroni",
          "quantity": 2,
          "price": 12
        }
      ],
      "created_at": "2026-11-12T18:04:11Z"
    }
  ],
  "summary": {
    "total_orders": 1,
    "total_amount": 31.5,
    "pending_count": 0,
    "preparing_count": 1,
    "ready_count": 0
  }
}

Update an order’s status

PATCH /orders/:id

Moves an order along: preparing, ready, served, paid or cancelled.

BodyTypeWhat it is
statusrequiredstringThe new status. One of: pending, preparing, ready, served, paid, cancelled.
Request
curl -X PATCH "https://api.gustoqr.com/v1/orders/9120" \
  -H "Authorization: Bearer $GUSTOQR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"status":"ready"}'
Response
{
  "order": {
    "id": 9120,
    "order_number": "ORD-0042",
    "status": "ready",
    "table_number": "7",
    "total_amount": 31.5,
    "items": [
      {
        "id": "negroni",
        "name": "Negroni",
        "quantity": 2,
        "price": 12
      }
    ],
    "created_at": "2026-11-12T18:04:11Z"
  }
}

Tables

List tables

GET /tables

The venue’s tables and whether each is free or occupied.

QueryTypeWhat it is
menu_idintegerOnly this menu’s tables.
Request
curl "https://api.gustoqr.com/v1/tables?menu_id=355" \
  -H "Authorization: Bearer $GUSTOQR_API_KEY"
Response
{
  "tables": [
    {
      "id": 12,
      "table_number": "7",
      "capacity": 4,
      "location": "Terrace",
      "current_status": "occupied",
      "current_order_id": 9120
    }
  ]
}

Activity

Activity log

GET /logs

What happened at the till: orders taken and paid, tables opened, moved and closed, staff changes. The newest 500 in the period.

QueryTypeWhat it is
menu_idrequiredintegerThe menu whose venue you ask about.
date_fromstringA day (2026-11-01) or a moment (2026-11-01T18:00:00Z). Default: 30 days ago.
date_tostringThe same, for the end. Default: now.
typestringOnly one kind of entry; waiter is staff changes. One of: order, payment, table, waiter.
waiter_namestringOnly this member of staff.
Request
curl "https://api.gustoqr.com/v1/logs?menu_id=355&date_from=2026-11-12" \
  -H "Authorization: Bearer $GUSTOQR_API_KEY"
Response
{
  "logs": [
    {
      "id": "order_9120",
      "type": "order",
      "action": "Order created",
      "details": "2 × Negroni",
      "amount": 24,
      "table_number": "7",
      "order_id": 9120,
      "waiter_name": "Deniz",
      "timestamp": "2026-11-12T18:04:11Z"
    }
  ]
}

Keys

Make a read-only key

POST /keys/read-only

The read-only form of the key you call with: it reads everything that key reads and changes nothing. Give it to dashboards, reports and assistants that only need to look. The same key always gives the same read-only key, and deleting the key cuts its read-only form off too. Limits are counted per key: 1200 reads and 300 changes a minute.

Request
curl -X POST "https://api.gustoqr.com/v1/keys/read-only" \
  -H "Authorization: Bearer $GUSTOQR_API_KEY"
Response
{
  "key": "gq_ro_Zk3…",
  "read_only": true
}

Help

Something missing or not working? Write to support@luvi.io and say what you are building.