# ManuBBQ > ManuBBQ is a barbecue restaurant in Ploiești, Romania. AI agents (ChatGPT, Claude, Grok, Perplexity, etc.) can read the menu and place pickup or delivery orders for a human. The human always pays on a Stripe-hosted page; agents never handle card data. ## Connect - MCP (Streamable HTTP, no auth): https://manubbq.ro/mcp (7 tools: get_menu, get_availability, check_delivery, quote_order, place_order, get_order_status, cancel_order) - REST + OpenAPI 3.1: https://manubbq.ro/api/v1/openapi.json (base https://manubbq.ro/api/v1/agent) - Docs: https://manubbq.ro/developers. Full guide: https://manubbq.ro/llms-full.txt ## Site (Romanian) - FAQ: https://manubbq.ro/intrebari-frecvente (hours, minimum order, delivery area, payment, pickup addresses, catering) - Menu: https://manubbq.ro/menu. Contact: https://manubbq.ro/contact. Sitemap: https://manubbq.ro/sitemap.xml - Mici & grill (live prices): https://manubbq.ro/mici-ploiesti. Ciorbe (live prices): https://manubbq.ro/ciorba-ploiesti - Ordering through AI assistants (RO + EN summary): https://manubbq.ro/comanda-prin-asistenti-ai ## Flow 1. get_menu (GET /api/v1/agent/menu): item ids, RON prices, option groups, allergens if published. 2. get_availability (GET /api/v1/agent/availability): check isOpen, lastOrderTime and minOrderRon. 3. Delivery only: check_delivery (POST /api/v1/agent/delivery/check) with {"street":"Strada X 12","city":"Ploiești"}. 4. quote_order (POST /api/v1/agent/quote): server-computed lines and totalRon; if orderable is false, the minimum order is not met (minimumOrder.shortfallRon). 5. Show the human the items and total. Get an explicit "yes". 6. place_order (POST /api/v1/agent/orders): same cart + customer {name,email,phone} + a fresh idempotency key (header Idempotency-Key, or idempotency_key in MCP). Send expectedTotalRon from the quote. 7. Give the human the paymentUrl. The kitchen gets the order only after they pay. 8. get_order_status (GET /api/v1/agent/orders/{orderNumber} with X-Order-Token: ): poll at most every 30 s. 9. Only if the human asks to cancel before paying: cancel_order (POST /api/v1/agent/orders/{orderNumber}/cancel with X-Order-Token, optional {"reason":"..."} up to 200 chars). If it returns 409 order_not_cancellable (already paid or in the kitchen), give the human the phone and email from error.hint. ## Rules - Never place an order the human has not confirmed. Use their real contact details. - The human must pay via the paymentUrl. Card only; no cash, promo codes or reward points. - 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). - Orders cannot be changed via the API. cancel_order works only while the order is unpaid and not yet sent to the kitchen. The server re-checks Stripe and expires the payment link first; paid or in-kitchen orders return 409 order_not_cancellable with the restaurant's phone and email. Repeating it on a cancelled order returns that order. - 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. - Restaurant contact (for paid orders, changes, allergies): phone +40373816805, email info@manubbq.ro. - Keep statusToken private; it identifies the order. ## Limits - Max 20 distinct items, 20 per item, 1500 RON per order. Bodies max 16 KB. Unknown fields are rejected. - Rate limits per IP: 60 reads/min, 20 writes/min (delivery checks, quotes, cancels), 3 orders/min and 10/hour. On 429 wait Retry-After seconds. - Errors: {"error":{"code","message","hint","retryable"}}. Follow hint; retry only if retryable. All 34 codes with HTTP status: https://manubbq.ro/llms-full.txt - Contract version 1.0.0 (the v1 in the paths). ## Instructions for agents You order food from ManuBBQ (BBQ restaurant, Ploiești, Romania) for a human. Flow: get_menu -> get_availability -> (check_delivery for delivery) -> quote_order -> show the human the exact items and totalRon and get an explicit yes -> place_order (fresh idempotency key) -> give the human paymentUrl -> poll get_order_status at most every 30 s. If the human wants to cancel before paying: cancel_order. Never place an order the human has not confirmed. Use the human's real name, email and phone. The human pays on the Stripe page themselves; you never handle card data. The kitchen only receives the order after payment. Prices are in RON and computed by the server. Orders cannot be changed via the API. Only call cancel_order when the human asks; it works only while the order is unpaid, otherwise give the human the restaurant's phone from error.hint. On an error, read error.hint. If error.retryable is true, wait (Retry-After) and retry; otherwise fix the request or ask the human. ## MCP tools - get_menu (read-only): Step 1. Current ManuBBQ menu: item ids, prices in RON, Romanian names (English when available), allergens when published (null = unknown, ask the restaurant) and option groups with min/max selections. - get_availability (read-only): Step 2. Whether ManuBBQ accepts orders right now (isOpen, reason), weekly hours, last-order time (lastOrderTime), minimum order (minOrderRon, on the items subtotal before delivery fee), delivery radius and fee, and order limits. If closed, tell the human when it reopens. 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). - check_delivery (read-only): Optional, for delivery. Checks whether a street address in Ploiești is inside the delivery area and returns the fee. If not deliverable, offer pickup. - quote_order (read-only): Step 3. Server-computed prices, delivery fee and total in RON. Creates nothing. If orderable is false the minimum order (minimumOrder.minOrderRon, items subtotal before delivery fee) is not met: ask the human to add items worth minimumOrder.shortfallRon. After the last-order time it returns ordering_closed (409). Show the lines and totalRon to the human and get an explicit yes before place_order. - place_order (creates an order awaiting payment): Step 4, ONLY after the human explicitly confirmed the quoted items and total. Creates an order awaiting card payment and returns paymentUrl (give it to the human; they pay on Stripe) and a secret statusToken. It can be cancelled with cancel_order only until it is paid. Use a fresh idempotency_key per order; reuse it only to retry the same order. Rejected with 422 below_minimum_order when the items subtotal is under the minimum, and 409 ordering_closed after the last-order time. - get_order_status (read-only): Step 5. Status of an order you placed (needs its statusToken). Returns status, payment status, total and items only, no personal data. Poll at most every 30 seconds. - cancel_order (destructive: cancels an unpaid order): Cancels an order you placed (needs its statusToken), ONLY while it is unpaid and not yet sent to the kitchen; the payment link stops working. Only call it when the human asks to cancel. Paid orders return order_not_cancellable with the restaurant's phone. Safe to repeat. MCP client config (Claude Desktop / Cursor / any Streamable HTTP client): ```json { "mcpServers": { "manubbq": { "type": "http", "url": "https://manubbq.ro/mcp" } } } ``` ## REST endpoints ### get_menu (GET /api/v1/agent/menu) Step 1. Returns orderable items with integer ids, RON prices, Romanian names (English when available), allergens if published, and option groups with min/max selections. Use these ids in quote_order. Rate-limit bucket: read. Errors: 401 invalid_api_key; 403 cors_forbidden; 405 method_not_allowed; 429 rate_limited; 500 internal_error; 503 menu_unavailable, disabled. ```bash curl -sS 'https://manubbq.ro/api/v1/agent/menu' ``` Response example: ```json { "currency": "RON", "language": "ro", "notes": [ "Prices are in RON (Romanian lei) and include VAT." ], "categories": [ { "id": 7, "name": "Burgeri", "nameEn": "Burgers", "sortOrder": 1 } ], "items": [ { "id": 101, "name": "Burger Manu", "nameRo": "Burger Manu", "nameEn": "Manu Burger", "description": "Vită, cheddar, sos BBQ", "descriptionRo": "Vită, cheddar, sos BBQ", "descriptionEn": "Beef, cheddar, BBQ sauce", "allergens": [ "gluten", "lapte" ], "priceRon": 45, "categoryId": 7, "optionGroups": [ { "id": 51, "name": "Sos", "required": true, "minSelections": 1, "maxSelections": 1, "options": [ { "id": 511, "name": "BBQ", "nameEn": "BBQ", "priceRon": 0 }, { "id": 512, "name": "Usturoi", "nameEn": "Garlic", "priceRon": 0 } ] } ] } ], "generatedAt": "2026-10-13T12:00:00.000Z" } ``` ### get_availability (GET /api/v1/agent/availability) Step 2. Check isOpen before quoting. Also returns weekly hours, delivery radius and fee, and order limits. If closed, tell the human when it reopens. Rate-limit bucket: read. Errors: 401 invalid_api_key; 403 cors_forbidden; 405 method_not_allowed; 429 rate_limited; 500 internal_error; 503 disabled. ```bash curl -sS 'https://manubbq.ro/api/v1/agent/availability' ``` Response example: ```json { "isOpen": true, "reason": "open", "timezone": "Europe/Bucharest", "alwaysOpen": false, "weeklyHours": { "mon": [ [ "11:00", "22:00" ] ] }, "closedDates": [], "fulfillment": [ "pickup", "delivery" ], "paymentMethods": [ "card" ], "currency": "RON", "agentOrderingPaused": false, "delivery": { "maxDistanceKm": 10, "feeRon": 10, "freeOverRon": 150, "minOrderRon": 0 }, "limits": { "maxDistinctItems": 20, "maxQuantityPerItem": 20, "maxOrderRon": 1500, "minOrderRon": 0 }, "serverTime": "2026-10-13T12:00:00.000Z" } ``` ### check_delivery (POST /api/v1/agent/delivery/check) Optional, for delivery orders. Send the street and number. If deliverable is false, offer the human pickup instead. Rate-limit bucket: write. Errors: 400 validation_error, invalid_json; 401 invalid_api_key; 403 cors_forbidden; 405 method_not_allowed; 413 payload_too_large; 415 unsupported_media_type; 422 address_not_found; 429 rate_limited; 500 internal_error; 503 geocoding_unavailable, disabled. ```bash curl -sS -X POST 'https://manubbq.ro/api/v1/agent/delivery/check' -H 'Content-Type: application/json' -d '{"street":"Strada Gheorghe Doja 12","city":"Ploiești"}' ``` Response example: ```json { "deliverable": true, "distanceKm": 2.4, "maxDistanceKm": 10, "formattedAddress": "Strada Gheorghe Doja 12, Ploiești, Romania", "deliveryFeeRon": 10, "freeOverRon": 150, "minOrderRon": 0, "currency": "RON" } ``` ### quote_order (POST /api/v1/agent/quote) Step 3. Server-computed prices, delivery fee and total in RON. Nothing is ordered. Show the lines and totalRon to the human and get explicit confirmation before place_order. Rate-limit bucket: write. Errors: 400 validation_error, invalid_json; 401 invalid_api_key; 403 cors_forbidden; 405 method_not_allowed; 409 restaurant_closed, ordering_closed; 413 payload_too_large; 415 unsupported_media_type; 422 item_unavailable, invalid_options, too_many_items, order_too_large, outside_delivery_zone, address_not_found, invalid_request; 429 rate_limited; 500 internal_error; 503 geocoding_unavailable, menu_unavailable, disabled. ```bash curl -sS -X POST 'https://manubbq.ro/api/v1/agent/quote' -H 'Content-Type: application/json' -d '{"fulfillment":"pickup","items":[{"itemId":101,"quantity":2,"optionIds":[511]}]}' ``` Response example: ```json { "currency": "RON", "fulfillment": "pickup", "lines": [ { "itemId": 101, "name": "Burger Manu", "quantity": 2, "unitPriceRon": 45, "options": [ { "id": 511, "name": "BBQ", "priceRon": 0 } ], "lineTotalRon": 90 } ], "subtotalRon": 90, "deliveryFeeRon": 0, "discountRon": 0, "totalRon": 90, "paymentMethods": [ "card" ], "validForSeconds": 600 } ``` ### place_order (POST /api/v1/agent/orders) Step 4, ONLY after the human confirmed the quote. Needs a fresh idempotency key per order (reuse it only to retry the same order). Returns paymentUrl for the human and a secret statusToken. Rate-limit bucket: orders. Errors: 400 validation_error, invalid_json, idempotency_key_required; 401 invalid_api_key; 403 cors_forbidden; 405 method_not_allowed; 409 restaurant_closed, ordering_closed, price_changed, request_in_progress; 413 payload_too_large; 415 unsupported_media_type; 422 item_unavailable, invalid_options, too_many_items, order_too_large, outside_delivery_zone, address_not_found, invalid_request, below_minimum_order, idempotency_key_reused; 429 rate_limited; 500 internal_error; 502 payment_unavailable; 503 geocoding_unavailable, menu_unavailable, ordering_unavailable, ordering_paused, disabled. ```bash curl -sS -X POST 'https://manubbq.ro/api/v1/agent/orders' -H 'Content-Type: application/json' -H 'Idempotency-Key: 3f2b8c1e-7a4d-4e8b-9c1a-2d5e6f7a8b9c' -d '{"fulfillment":"pickup","items":[{"itemId":101,"quantity":2,"optionIds":[511]}],"customer":{"name":"Ion Popescu","email":"ion@example.com","phone":"+40712345678"},"notes":"Fără ceapă","expectedTotalRon":90,"idempotencyKey":"3f2b8c1e-7a4d-4e8b-9c1a-2d5e6f7a8b9c"}' ``` Response example: ```json { "orderNumber": "MANUBBQ-LX9K2P7QAB12", "statusToken": "q2V9k3J0c1x8m4N7b6Z5a1S2d3F4g5H6", "status": "awaiting_payment", "paymentUrl": "https://checkout.stripe.com/c/pay/cs_live_a1B2c3", "paymentExpiresAt": "2026-10-13T13:00:00.000Z", "nextStep": "Give paymentUrl to the human now. The order goes to the kitchen only after they pay.", "currency": "RON", "fulfillment": "pickup", "lines": [ { "itemId": 101, "name": "Burger Manu", "quantity": 2, "unitPriceRon": 45, "options": [ { "id": 511, "name": "BBQ", "priceRon": 0 } ], "lineTotalRon": 90 } ], "subtotalRon": 90, "deliveryFeeRon": 0, "discountRon": 0, "totalRon": 90, "paymentMethods": [ "card" ] } ``` ### get_order_status (GET /api/v1/agent/orders/{orderNumber}) Step 5. Needs the statusToken from place_order in the X-Order-Token header. Returns status, payment status, totals and items only (no personal data). Poll at most every 30 seconds. Rate-limit bucket: read. Errors: 401 invalid_api_key; 403 cors_forbidden; 404 not_found; 405 method_not_allowed; 429 rate_limited; 500 internal_error; 503 unavailable, disabled. ```bash curl -sS -H 'X-Order-Token: q2V9k3J0c1x8m4N7b6Z5a1S2d3F4g5H6' 'https://manubbq.ro/api/v1/agent/orders/MANUBBQ-LX9K2P7QAB12' ``` Response example: ```json { "orderNumber": "MANUBBQ-LX9K2P7QAB12", "status": "preparing", "paymentStatus": "paid", "fulfillment": "pickup", "currency": "RON", "totalRon": 90, "lines": [ { "name": "Burger Manu", "quantity": 2, "lineTotalRon": 90 } ], "paymentUrl": null, "createdAt": "2026-10-13T12:00:00.000Z" } ``` ### cancel_order (POST /api/v1/agent/orders/{orderNumber}/cancel) Only when the human asks. Needs the statusToken in X-Order-Token. Works only while unpaid and not sent to the kitchen; else 409 order_not_cancellable with the restaurant phone in the hint. Safe to repeat. Rate-limit bucket: write. Errors: 400 validation_error, invalid_json; 401 invalid_api_key; 403 cors_forbidden; 404 not_found; 405 method_not_allowed; 409 order_not_cancellable; 413 payload_too_large; 415 unsupported_media_type; 429 rate_limited; 500 internal_error; 503 cancel_unavailable, unavailable, disabled. ```bash curl -sS -X POST 'https://manubbq.ro/api/v1/agent/orders/MANUBBQ-LX9K2P7QAB12/cancel' -H 'Content-Type: application/json' -H 'X-Order-Token: q2V9k3J0c1x8m4N7b6Z5a1S2d3F4g5H6' -d '{"reason":"Changed my mind"}' ``` Response example: ```json { "orderNumber": "MANUBBQ-LX9K2P7QAB12", "status": "cancelled", "paymentStatus": "pending", "fulfillment": "pickup", "currency": "RON", "totalRon": 90, "lines": [ { "name": "Burger Manu", "quantity": 2, "lineTotalRon": 90 } ], "paymentUrl": null, "createdAt": "2026-10-13T12:00:00.000Z", "cancelled": true, "alreadyCancelled": false } ``` ## Statuses awaiting_payment -> received -> preparing -> ready -> (out_for_delivery -> delivered). awaiting_payment -> cancelled (cancel_order, only while unpaid). Also payment_failed, refunded, processing, and expired (unpaid order auto-cancelled after its payment link lapsed). ## Minimum order and last orders 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). ## Cancelling cancel_order works only while the order is unpaid and not yet sent to the kitchen. The server re-checks Stripe and expires the payment link first; paid or in-kitchen orders return 409 order_not_cancellable with the restaurant's phone and email. Repeating it on a cancelled order returns that order. A wrong or missing token gives the same 404 not_found as an unknown order. ## Error codes (HTTP status, retryable) - validation_error (400): Body or arguments fail validation (details lists each field). Hint: Fix the fields listed in details and resend. Unknown fields are rejected. - invalid_json (400): Malformed JSON body. Hint: Send a valid JSON object body with Content-Type: application/json. - idempotency_key_required (400): place_order without a valid idempotency key. Hint: Generate a fresh random key (e.g. a UUID) per order and reuse it only when retrying that same order. - invalid_api_key (401): Authorization header with an unknown key. Hint: Remove the Authorization header (keys are optional) or use a valid key. - cors_forbidden (403): Browser write from a non-allowed Origin. Hint: Call the API server-side; browser writes are limited to allowed origins. - forbidden_origin (403): MCP request from a non-allowed Origin. Hint: MCP requests from browsers must come from an allowed Origin; server-side clients should omit Origin. - not_found (404): Unknown order, wrong or missing token, unknown path or tool. Hint: Check the order number and use the statusToken returned by place_order. - method_not_allowed (405): Wrong HTTP method on a known path (GET/DELETE on /mcp). Hint: See the OpenAPI spec at /api/v1/openapi.json for allowed methods. - restaurant_closed (409): place_order while the restaurant is closed or paused. Hint: Check get_availability for opening hours and tell the human when ordering reopens. - ordering_closed (409): quote_order/place_order after the last-order cutoff (LAST_ORDER_TIME, default 22:00 Europe/Bucharest). Hint: Online ordering closes at the last-order time (get_availability.lastOrderTime, Europe/Bucharest). Tell the human ordering reopens tomorrow; do not retry tonight. - price_changed (409): expectedTotalRon differs from the current total. Hint: Prices changed since the quote. Show the new total to the human, get confirmation, then resend with the new expectedTotalRon. - request_in_progress (409, retryable): Same idempotency key while the first request is still running. Hint: The same order is still being created; wait a few seconds and retry with the same key. - order_not_cancellable (409): cancel_order on a paid, processing or in-kitchen order. Hint: The order is paid or already with the kitchen; ask the human to contact the restaurant (phone and email are in this hint on live responses). - payload_too_large (413): Body over 16 KB. Hint: Request bodies are limited to 16 KB; send fewer items or shorter notes. - unsupported_media_type (415): Body sent without Content-Type: application/json. Hint: Use Content-Type: application/json. - item_unavailable (422): Unknown or unavailable itemId. Hint: Call get_menu (GET /menu) again and use a current item id. - invalid_options (422): optionIds break the item's option group rules. Hint: Respect each option group's minSelections/maxSelections and use option ids from that item. - too_many_items (422): Total quantity over the limit. Hint: Reduce quantities or split into separate orders. - order_too_large (422): Total over the RON limit. Hint: Reduce the order; larger orders must be placed by phone with the restaurant. - below_minimum_order (422): place_order with an items subtotal below the minimum order (MIN_ORDER_LEI, default 50 RON). Hint: The items subtotal (before delivery fee) is below the minimum order (details.minOrderRon, default 50 RON). Ask the human to add items worth at least details.shortfallRon RON, re-quote, then place the order. - outside_delivery_zone (422): Delivery address outside the radius. Hint: Offer the human pickup instead (fulfillment: pickup). - address_not_found (422): Address cannot be geocoded. Hint: Ask the human for a fuller street address (street name and number) in Ploiești. - idempotency_key_reused (422): Same idempotency key with a different body. Hint: This key was used for a different order. Use a new key for a new order. - invalid_request (422): Order rejected by the shared order rules (e.g. no priceable items). Hint: Read the message, fix the order (items, address, customer) and resend. - rate_limited (429, retryable): Too many requests (Retry-After header). Hint: Wait for Retry-After seconds before trying again. Do not retry in a tight loop. - internal_error (500, retryable): Unexpected server error. Hint: Retry once later; if it persists ask the human to order on https://manubbq.ro. - payment_unavailable (502, retryable): Stripe payment link could not be created. Hint: Retry place_order with the SAME idempotency key in a minute. - menu_unavailable (503, retryable): Menu backend unreachable. Hint: Retry in a minute. - geocoding_unavailable (503, retryable): Address lookup unavailable. Hint: Retry in a minute, or offer pickup. - ordering_unavailable (503, retryable): Ordering not configured or temporarily off. Hint: Online ordering is temporarily unavailable; ask the human to order on https://manubbq.ro or by phone. - ordering_paused (503, retryable): place_order while AGENT_ORDERING_PAUSED=true (kill switch). Hint: Agent ordering is paused by the restaurant right now. Menu, quotes and order status still work; ask the human to order on https://manubbq.ro or by phone, or try again later. - cancel_unavailable (503, retryable): Payment state could not be verified, so nothing was cancelled. Hint: Could not confirm the payment state; retry cancel_order in a minute (safe to repeat). - unavailable (503, retryable): Order status temporarily unavailable. Hint: Retry later. - disabled (503): Agent API switched off (AGENT_API_ENABLED=false). Hint: The agent API is switched off; ask the human to order on https://manubbq.ro. Any route may also return: invalid_api_key, cors_forbidden, method_not_allowed, disabled. MCP only: forbidden_origin.