ManuBBQ for AI agents

Let an AI assistant read our menu and order BBQ for a human in Ploiești. The human confirms the order and pays on a Stripe page; agents never touch card data.

Rules for agents: show the quote to the human and get an explicit yes before placing an order; give the human the paymentUrl. The kitchen only receives paid orders. Orders can't be changed via the API; cancel_order works only while the order is unpaid and only when the human asks. For paid orders the human contacts us: +40373816805, [email protected].

Machine-readable: OpenAPI 3.1 · llms.txt · llms-full.txt · MCP server card · API catalog · Interactive reference

Flow

  1. get_menu: items, RON prices, options, allergens
  2. get_availability: is it open now?
  3. check_delivery (delivery only)
  4. quote_order: server-computed total → confirm with the human
  5. place_order with a fresh idempotency key → give the human paymentUrl
  6. get_order_status with statusToken, every 30 s or more
  7. Only if the human asks, before paying: cancel_order with statusToken (paid orders get 409 order_not_cancellable)

Operations

Tool / operationIdHTTPRate bucket
get_menuGET /api/v1/agent/menuread
get_availabilityGET /api/v1/agent/availabilityread
check_deliveryPOST /api/v1/agent/delivery/checkwrite
quote_orderPOST /api/v1/agent/quotewrite
place_orderPOST /api/v1/agent/ordersorders
get_order_statusGET /api/v1/agent/orders/{orderNumber}read
cancel_orderPOST /api/v1/agent/orders/{orderNumber}/cancelwrite

ChatGPT (Custom GPT → Actions)

  1. In the GPT editor open Configure → Actions → Create new action.
  2. Choose Import from URL and paste https://manubbq.ro/api/v1/openapi.json.
  3. Authentication: None. Privacy policy: https://manubbq.ro/privacy-policy.
  4. Add to the GPT instructions:
Before calling place_order, show the items and total from quote_order and ask the user to confirm. After place_order, give the user the paymentUrl and tell them the order is sent to the kitchen once they pay. Generate a new UUID for idempotencyKey per order. Only call cancel_order if the user asks to cancel before paying; if it returns order_not_cancellable, give them the phone number from the error hint.

Claude, Cursor and other MCP clients

Remote MCP server (Streamable HTTP, stateless, no auth): https://manubbq.ro/mcp. In Claude, add it under Settings → Connectors → Add custom connector. For config-file clients:

{
  "mcpServers": {
    "manubbq": {
      "type": "http",
      "url": "https://manubbq.ro/mcp"
    }
  }
}

Clients that only support stdio can use a bridge:

{
  "mcpServers": {
    "manubbq": {
      "command": "npx",
      "args": [
        "-y",
        "mcp-remote",
        "https://manubbq.ro/mcp"
      ]
    }
  }
}

Grok, Perplexity/Comet and generic HTTP tool callers

Point your tool loader at the OpenAPI spec https://manubbq.ro/api/v1/openapi.json (operationIds are snake_case and match the MCP tool names), or use MCP at https://manubbq.ro/mcp. Browsing agents can read https://manubbq.ro/llms.txt for the full flow.

curl

# 1. Menu and availability
curl -s https://manubbq.ro/api/v1/agent/menu
curl -s https://manubbq.ro/api/v1/agent/availability

# 2. Quote (no order created)
curl -s -X POST https://manubbq.ro/api/v1/agent/quote -H 'Content-Type: application/json' \
  -d '{"fulfillment":"pickup","items":[{"itemId":101,"quantity":2,"optionIds":[511]}]}'

# 3. Place the confirmed order (fresh key per order)
curl -s -X POST https://manubbq.ro/api/v1/agent/orders -H 'Content-Type: application/json' \
  -H "Idempotency-Key: $(uuidgen)" -d '{"fulfillment":"pickup","items":[{"itemId":101,"quantity":2,"optionIds":[511]}],"customer":{"name":"Ion Popescu","email":"[email protected]","phone":"+40712345678"},"expectedTotalRon":90}'

# 4. Status (no personal data returned)
curl -s https://manubbq.ro/api/v1/agent/orders/MANUBBQ-XXXX -H 'X-Order-Token: <statusToken>'

# 5. Cancel while still unpaid (only if the human asks)
curl -s -X POST https://manubbq.ro/api/v1/agent/orders/MANUBBQ-XXXX/cancel -H 'X-Order-Token: <statusToken>' \
  -H 'Content-Type: application/json' -d '{"reason":"Changed my mind"}'

# MCP: list tools
curl -s -X POST https://manubbq.ro/mcp -H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'

Safety

Minimum order: 50 RON, counted on the items subtotal (items + options, before delivery fee and discounts). quote_order reports minimumOrder {minOrderRon, met, shortfallRon} and orderable=false when it is not met; place_order then returns 422 below_minimum_order (details.shortfallRon), so ask the human to add items first. Last orders: 22:00 Europe/Bucharest time. From 22:00 until midnight get_availability shows isOpen=false, reason after_last_order, and quote_order/place_order return 409 ordering_closed (not retryable tonight).

Unpaid orders expire: the payment link is valid for AGENT_CHECKOUT_EXPIRES_MIN minutes (default 60) and the order is then automatically cancelled (status expired); paid orders are never touched. Phone numbers must be E.164 (+40712345678) or Romanian national format (0712 345 678) and are stored as E.164. The restaurant can pause agent ordering (place_order returns 503 ordering_paused) while menu, quotes, status and cancel keep working. All endpoints are public: an API key is optional and only raises rate limits.

Limits and errors

Max 20 distinct items, 20 per item, 1500 RON per order; 16 KB bodies; per-IP rate limits (HTTP 429 with Retry-After). Errors look like:

{
  "error": {
    "code": "price_changed",
    "message": "Total is now 95 RON",
    "hint": "Prices changed since the quote. Show the new total to the human, get confirmation, then resend with the new expectedTotalRon.",
    "retryable": false
  }
}

Error codes

CodeHTTPRetryableWhen
validation_error400noBody or arguments fail validation (details lists each field)
invalid_json400noMalformed JSON body
idempotency_key_required400noplace_order without a valid idempotency key
invalid_api_key401noAuthorization header with an unknown key
cors_forbidden403noBrowser write from a non-allowed Origin
forbidden_origin403noMCP request from a non-allowed Origin
not_found404noUnknown order, wrong or missing token, unknown path or tool
method_not_allowed405noWrong HTTP method on a known path (GET/DELETE on /mcp)
restaurant_closed409noplace_order while the restaurant is closed or paused
ordering_closed409noquote_order/place_order after the last-order cutoff (LAST_ORDER_TIME, default 22:00 Europe/Bucharest)
price_changed409noexpectedTotalRon differs from the current total
request_in_progress409yesSame idempotency key while the first request is still running
order_not_cancellable409nocancel_order on a paid, processing or in-kitchen order
payload_too_large413noBody over 16 KB
unsupported_media_type415noBody sent without Content-Type: application/json
item_unavailable422noUnknown or unavailable itemId
invalid_options422nooptionIds break the item's option group rules
too_many_items422noTotal quantity over the limit
order_too_large422noTotal over the RON limit
below_minimum_order422noplace_order with an items subtotal below the minimum order (MIN_ORDER_LEI, default 50 RON)
outside_delivery_zone422noDelivery address outside the radius
address_not_found422noAddress cannot be geocoded
idempotency_key_reused422noSame idempotency key with a different body
invalid_request422noOrder rejected by the shared order rules (e.g. no priceable items)
rate_limited429yesToo many requests (Retry-After header)
internal_error500yesUnexpected server error
payment_unavailable502yesStripe payment link could not be created
menu_unavailable503yesMenu backend unreachable
geocoding_unavailable503yesAddress lookup unavailable
ordering_unavailable503yesOrdering not configured or temporarily off
ordering_paused503yesplace_order while AGENT_ORDERING_PAUSED=true (kill switch)
cancel_unavailable503yesPayment state could not be verified, so nothing was cancelled
unavailable503yesOrder status temporarily unavailable
disabled503noAgent API switched off (AGENT_API_ENABLED=false)

API contract version 1.0.0 (the v1 in the paths).