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
get_menu: items, RON prices, options, allergensget_availability: is it open now?check_delivery (delivery only)quote_order: server-computed total → confirm with the humanplace_order with a fresh idempotency key → give the human paymentUrlget_order_status with statusToken, every 30 s or morecancel_order with statusToken (paid orders get 409 order_not_cancellable)| Tool / operationId | HTTP | Rate bucket |
|---|---|---|
get_menu | GET /api/v1/agent/menu | read |
get_availability | GET /api/v1/agent/availability | read |
check_delivery | POST /api/v1/agent/delivery/check | write |
quote_order | POST /api/v1/agent/quote | write |
place_order | POST /api/v1/agent/orders | orders |
get_order_status | GET /api/v1/agent/orders/{orderNumber} | read |
cancel_order | POST /api/v1/agent/orders/{orderNumber}/cancel | write |
https://manubbq.ro/api/v1/openapi.json.https://manubbq.ro/privacy-policy.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.
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"
]
}
}
}
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.
# 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"}'
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.
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
}
}
| Code | HTTP | Retryable | When |
|---|---|---|---|
validation_error | 400 | no | Body or arguments fail validation (details lists each field) |
invalid_json | 400 | no | Malformed JSON body |
idempotency_key_required | 400 | no | place_order without a valid idempotency key |
invalid_api_key | 401 | no | Authorization header with an unknown key |
cors_forbidden | 403 | no | Browser write from a non-allowed Origin |
forbidden_origin | 403 | no | MCP request from a non-allowed Origin |
not_found | 404 | no | Unknown order, wrong or missing token, unknown path or tool |
method_not_allowed | 405 | no | Wrong HTTP method on a known path (GET/DELETE on /mcp) |
restaurant_closed | 409 | no | place_order while the restaurant is closed or paused |
ordering_closed | 409 | no | quote_order/place_order after the last-order cutoff (LAST_ORDER_TIME, default 22:00 Europe/Bucharest) |
price_changed | 409 | no | expectedTotalRon differs from the current total |
request_in_progress | 409 | yes | Same idempotency key while the first request is still running |
order_not_cancellable | 409 | no | cancel_order on a paid, processing or in-kitchen order |
payload_too_large | 413 | no | Body over 16 KB |
unsupported_media_type | 415 | no | Body sent without Content-Type: application/json |
item_unavailable | 422 | no | Unknown or unavailable itemId |
invalid_options | 422 | no | optionIds break the item's option group rules |
too_many_items | 422 | no | Total quantity over the limit |
order_too_large | 422 | no | Total over the RON limit |
below_minimum_order | 422 | no | place_order with an items subtotal below the minimum order (MIN_ORDER_LEI, default 50 RON) |
outside_delivery_zone | 422 | no | Delivery address outside the radius |
address_not_found | 422 | no | Address cannot be geocoded |
idempotency_key_reused | 422 | no | Same idempotency key with a different body |
invalid_request | 422 | no | Order rejected by the shared order rules (e.g. no priceable items) |
rate_limited | 429 | yes | Too many requests (Retry-After header) |
internal_error | 500 | yes | Unexpected server error |
payment_unavailable | 502 | yes | Stripe payment link could not be created |
menu_unavailable | 503 | yes | Menu backend unreachable |
geocoding_unavailable | 503 | yes | Address lookup unavailable |
ordering_unavailable | 503 | yes | Ordering not configured or temporarily off |
ordering_paused | 503 | yes | place_order while AGENT_ORDERING_PAUSED=true (kill switch) |
cancel_unavailable | 503 | yes | Payment state could not be verified, so nothing was cancelled |
unavailable | 503 | yes | Order status temporarily unavailable |
disabled | 503 | no | Agent API switched off (AGENT_API_ENABLED=false) |
API contract version 1.0.0 (the v1 in the paths).