Ownware
Home›Menura›API
Menura · API

Menura API, as shipped in the download

Menura REST API, Webhooks & MCP

Menura ships a JSON REST API and signed webhooks so a digitized menu can reach your POS, your website, a price-management sheet, Power Automate, Zapier, n8n, or your own code — without anyone retyping a menu.

The important part: the exports this API serves are the exports the screen serves. They come from the same three writers (toJson, toNestedCsv, toPosCsv). A POS import built against a second, nearly identical CSV writer is exactly how a menu ends up on a till with the wrong prices — so there isn't one.

Authentication

Create a key in API & Webhooks. A key belongs to a user and can do what a signed-in user can do. Send it on every call:

Authorization: Bearer apk_...

The key is shown once at creation and stored as a SHA-256 hash. Revoke any time.

Prices are integer cents, and may be null — a genuinely priceless line ("Market Price") is different from a free one, and Menura keeps that distinction.

Endpoints

Method & pathWhat it does
GET /healthzLiveness check — no auth. {"ok":true,"app":"menura","version":"3.1.6"}
GET /api/pingVerify a key
GET /api/menusList menus with item / section / priced-item counts. ?status= · `?reviewed=0\1 · ?q= · ?limit=`
GET /api/menus/{id}/itemsOne menu's items, in menu order, with its section list
POST /api/items/{id}Edit an item — typically a price update
GET /api/exportsExport for a POS. `?format=json\csv\csv-pos · ?scope=all\reviewed\<menu id>`
GET /api/openapi.jsonOpenAPI 3 spec — no auth

A menu's items

GET /api/menus/2/items
{
  "menu": {"id": 2, "name": "Bella Cucina — Dinner", "status": "reviewed",
           "item_count": 12, "section_count": 4, "priced_item_count": 11},
  "sections": ["Antipasti", "Primi", "Secondi", "Dolci"],
  "items": [
    {"id": 1, "section": "Antipasti", "name": "Bruschetta al Pomodoro",
     "description": "Grilled sourdough, vine tomatoes, basil, garlic",
     "price_cents": 950, "modifiers": ["Add buffalo mozzarella"],
     "allergens": ["gluten"], "dietary": ["vegetarian"], "sort": 1}
  ]
}

Sections come back in first-appearance order — the order the menu itself reads in, not alphabetical. The modifiers / allergens / dietary columns are decoded for you, so nothing downstream has to parse JSON twice.

priced_item_count is worth watching: the gap between it and item_count is how many lines the extraction could not find a price for.

Update an item (the price-change path)

curl -X POST https://your-install/api/items/1 \
  -H "Authorization: Bearer apk_..." -H "Content-Type: application/json" \
  -d '{"price_cents": 1975}'

Only the fields you send change. Send "price_cents": null to clear a price (for a "Market Price" line). modifiers, allergens and dietary take arrays of strings, or null to clear. An empty body is refused rather than silently doing nothing:

400  {"error": "bad_request", "detail": "Send at least one field to change."}

Every edit fires item.updated carrying both the before and the after, so a price change is auditable at the receiving end without it having to keep its own copy.

Exports — the POS handoff

GET /api/exports?format=csv-pos&scope=reviewed
SKU,Name,Category,Price,Description
antipasti-bruschetta-al-pomodoro,Bruschetta al Pomodoro,Antipasti,9.50,"Grilled sourdough, ..."

format=json returns the canonical export document verbatim; csv is the nested menu/section/item sheet; csv-pos is the flat SKU/Name/Category/Price sheet most tills import. scope=reviewed is the safe default for anything automated — it exports only menus a human has checked.

The output is byte-for-byte what the Export screen downloads.

Webhooks

Add receiver URLs in API & Webhooks. Events:

  • menu.processed — a menu finished digitizing, carrying every extracted item
  • item.updated — an item changed (through the API, MCP, a price update or the review screen), carrying the before and the after

Each delivery is an HTTP POST with a JSON body and:

X-Menura-Event: menu.processed
X-Menura-Signature: sha256=<hex HMAC-SHA256 of the RAW body, keyed with the endpoint's secret>

Verify the signature over the raw body before trusting a payload:

$raw = file_get_contents('php://input');
$expected = 'sha256=' . hash_hmac('sha256', $raw, $YOUR_SECRET);
if (!hash_equals($expected, $_SERVER['HTTP_X_MENURA_SIGNATURE'] ?? '')) { http_response_code(401); exit; }

One attempt, 5-second timeout; the Recent deliveries log shows every attempt and response code. Design receivers to be idempotent.

Getting a menu IN

Two routes, and neither is an API endpoint:

  1. Upload + AI extraction (the Upload screen) — a photo or PDF becomes structured items.
  2. CSV import (the Import CSV screen, new in 2.0) — name, price, section, description. If your menu is already in a spreadsheet you do not need the AI pass at all, and you should not pay for one. The importer runs a dry run first: it shows exactly which rows would land and why each of the rest would not, and writes nothing until you confirm. Import into an existing menu or let it create a new one. A hand-typed menu is marked reviewed on import — you wrote it.

There is no upload endpoint. Uploading is multipart, and the extraction behind it is long-running and spends your LLM credit per call, behind file-type checks and a demo limiter that live on the upload screen. Exposing that as JSON would have meant duplicating those guards or quietly weakening them — and an endpoint that can burn an operator's API budget deserves the deliberation a browser step provides. Everything after the menu exists — reading, editing, repricing, exporting — is fully available here.

Limits, honestly

  • Menura has one user type, so a key is not scoped more narrowly than a login.
  • One delivery attempt per webhook event (log + idempotent receivers, not a retry queue).
  • GET /api/menus caps at 500 rows.
  • There is no bulk price endpoint; loop POST /api/items/{id}. Menus are tens of items, not thousands, and one call per item means one item.updated per change, which is what an audit trail wants.
  • Extraction quality is your model's, not Menura's. The review screen exists for that reason, and ?scope=reviewed exists so automation can refuse to export anything unchecked.

MCP — the agent endpoint (new in 3.0)

POST /mcp
Authorization: Bearer apk_...          ← the SAME revocable key the REST API uses
ToolWrites?What it does
list_menusnoDigitized menus with status and item counts.
menu_detailnoEvery item: section, name, description, price in cents, modifiers, allergens, dietary.
item_updateYESCorrects one extracted item, through the same rule the review screen uses.
export_rowsnoPOS CSV, nested CSV or JSON — the product's own exporters.
allergen_reportnoItems with no allergen data. Gaps only.

Connect it to an assistant

Mint the key in the app first: Settings → API keys. Choose Read only when the assistant should answer questions but never change anything — the endpoint then lists only the read tools and refuses the rest by name, so a careless prompt cannot write. Full access behaves as before.

Every client needs the same three facts, and nothing in the handshake is vendor-specific:

The addresshttps://your-install/mcp
The keyheader Authorization: Bearer apk_xxxx
The transportMCP over streamable HTTP, stateless

Claude — one command, or the same URL and header as a custom connector in the desktop and web apps:

claude mcp add --transport http menura https://your-install/mcp \
  --header "Authorization: Bearer apk_xxxx"

ChatGPT and the OpenAI API — one entry in the Responses API's tools array (in ChatGPT itself, the same URL and key go in as a connector):

{
  "type": "mcp",
  "server_label": "menura",
  "server_url": "https://your-install/mcp",
  "authorization": "apk_xxxx",
  "require_approval": "never"
}

Own Your AI reads a list of servers in this shape:

{
  "mcpServers": [
    { "id": "menura", "name": "Menura", "url": "https://your-install/mcp",
      "token": "apk_xxxx", "enabled": true }
  ]
}

**Every other client spells the same three facts differently — copy the shape from its own documentation, not from here.** VS Code is the clearest example of why: its configuration reference (read 6 September 2026) puts servers in .vscode/mcp.json under a "servers" object — *"an object that maps server names to their configurations"* — not an mcpServers array. Pasted as-is, the block above will not load there. The id, the URL and the token are what travel; the JSON around them belongs to whichever client you are configuring.

A local model, n8n, or your own code — n8n's MCP Client node takes the URL and the same Authorization: Bearer header; a model running on your own machine reaches it through any MCP client, so nothing leaves your network at all. Writing it yourself is one POST of JSON-RPC 2.0:

curl -X POST https://your-install/mcp \
  -H "Authorization: Bearer apk_xxxx" -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'

Any client that speaks MCP's streamable-HTTP transport works, including ones that do not exist yet: Menura implements the protocol, not an integration with a particular vendor.

Why there is no extract tool

Extraction sends your menu to a vision model using your own API key and spends **your own credits**. A tool that lets an agent trigger that on a whim is a bill, not a feature — so extraction stays a button in the app. Agents work on data that already exists.

Refusals

  • Arguments must be single values. Only modifiers, allergens and dietary may be arrays — the same allowlist Api::body() enforces on the REST API.
  • Unknown arguments are refused by name; out-of-enum values list the valid options.
  • price_cents is integer cents and cannot be negative — the shared edit rule says so, not a second copy of it.
  • Roles apply exactly as in the browser: a viewer's key is refused item_update.

allergen_report never infers an allergen from a dish name. A missing allergen is reported as missing, because a confidently wrong allergen claim is a safety problem, not a UX one.

← Back to Menura · Manual · Quickstart · Test run

Affiliate program
Recommend tools people own — earn 35% on every sale. 90-day tracking, instant delivery, payouts by Lemon Squeezy.
Become an affiliate →