Catering Quotes
A customer ordering lunch for two picks items and checks out. A customer ordering lunch for two hundred does not — they describe an event, get a price back from someone at the brand, and approve it. Catering quotes are how that conversation happens through the API.
The chain has three links:
Lead → an enquiry. The customer says what the event is, when it is, how many people, roughly what budget.
Quote → a priced basket, built by the brand's catering team in response to the lead and sent to the customer.
Order → what the quote becomes once the customer approves it and it is checked out.
Your client's job is the customer's half of this: submit the enquiry, show the quotes that come back, and let the customer approve, decline or ask for changes. Building and sending a quote is done by the brand's staff in the Management Center, not through this API — those endpoints require a staff session and are not available to an Ordering API client.
Step 1 — Submit the enquiry
Call Create Catering Lead. Only catering order types are accepted: 10 for catering delivery, 11 for catering take-out.
Request
{
"method": "post",
"url": "https://api-public-demo.menu.app/api/leads",
"headers": {
"X-Request-ID": "69da3547-204b-4093-a225-54e084c24215",
"Application": "f3a90488ffee32c3acb6fcd0ca417cf6",
"Api-Version": "4.78.0",
"Authorization": "Bearer {customer_token}",
"Content-Type": "application/json"
},
"body": {
"order_type": 10,
"event_info": {
"event_type": "wedding",
"event_description": "Reception for 40",
"event_datetime": "2026-10-02 18:00:00",
"party_size": 40,
"budget": 250000,
"special_instructions": "Use the back entrance"
},
"event_address_info": {
"street_number": "12",
"street_name": "W Adams St",
"postal_code": "60603",
"formatted": "12 W Adams St, Chicago",
"latitude": 41.879,
"longitude": -87.63
},
"dietary_preferences": {
"diet_types": ["vegan"],
"allergens": ["peanut"],
"other_preferences": "no pork"
}
}
}
budget is in minor currency units, so 250000 is $2,500.00.
event_info.event_type is required in practice — the event record is always created and validates it — so always send at least that much of event_info.
Guests and signed-in customers
A signed-in customer is attached automatically from the Authorization token; you do not send customer_account_id.
For a customer who has not signed in, send guest_customer_info instead:
{
"order_type": 10,
"guest_customer_info": {
"customer_type_id": 1,
"first_name": "Ana",
"last_name": "Perez",
"email": "ana@example.com",
"phone_number": "+13125550101"
},
"event_info": { "event_type": "corporate", "party_size": 25 }
}
customer_type_id is 1 for an individual or 3 for an organization. Guest accounts (2) are not accepted here.
One case to handle deliberately: if a guest's email already belongs to a registered customer of the brand, the request is refused with 401 and the message "Please login to complete quote request creation". This is not a broken token — it means "we know you, please sign in". Route the customer to your sign-in screen and submit again once they have a token.
Step 2 — Wait
Nothing happens through the API at this point. The lead lands with the brand's catering team, who price it and send a quote. Depending on the brand that can be minutes or days.
You can read the lead back with Get Catering Lead to show its status: 1 active or 2 declined. Once a quote exists the lead is considered converted, and the quote is the thing to show.
Step 3 — Show the customer their quotes
Call Get Catering Quotes for the list, or Get Catering Quote for one.
Only quotes the brand has actually sent are visible — a draft the team is still working on does not appear, which is what you want. There is no pagination; the full list comes back.
Response
{
"status": "OK",
"code": 200,
"data": {
"quotes": [
{
"id": 1421,
"code": "A7F3K9",
"status": 2,
"order_id": null,
"order_type": 11,
"venue_id": 312,
"total": 48950,
"party_size": 25,
"po_number": "PO-2026-114",
"cost_center": null,
"expires_at": "2026-10-01 17:00:00",
"assigned_rep": { "name": "Dana Reeve", "email": "dana@example.com" },
"cart_data": {},
"created_at": "2026-09-12 09:14:02"
}
]
}
}
The pieces worth putting on screen:
codeis the short human reference the catering team will use on the phone. Show it.totalis the quoted price in minor currency units.cart_datais the priced basket — the actual items and amounts the customer is being asked to approve. Render it as a line-by-line breakdown rather than showing only the total.expires_atis when the quote lapses. Show a deadline, and chase it: a quote that expires has to be redone.assigned_repis the person to contact with questions.po_numberandcost_centerappear for business customers whose organization requires them.
Quote statuses are 1 draft, 2 sent, 3 approved, 4 declined, 5 changes requested, 6 expired. A customer will normally only ever see 2 and onwards.
Step 4 — Let the customer respond
Call Respond to Catering Quote with the customer's decision.
Request
{
"method": "post",
"url": "https://api-public-demo.menu.app/api/customer-accounts/88211/quotes/1421/statuses",
"headers": {
"X-Request-ID": "8bf3c1d0-11ae-4a2f-9f77-0c1d2e3f4a5b",
"Application": "f3a90488ffee32c3acb6fcd0ca417cf6",
"Api-Version": "4.78.0",
"Authorization": "Bearer {customer_token}",
"Content-Type": "application/json"
},
"body": {
"status": 3,
"message": "Approved — please proceed."
}
}
Send 3 to approve, 4 to decline or 5 to request changes. Use message for a note to the team, and reason when declining — both are optional, but a declined quote with a reason is much more useful to the brand than a bare rejection, so prompt for it.
Declining or requesting changes emails the brand's catering staff. Approving notifies the customer.
What the customer may and may not do
Only certain moves are allowed, and an invalid one returns 422 with "The selected status change from X to Y is invalid."
| From | Customer can move to |
|---|---|
Sent (2) |
Approved (3), Declined (4), Changes requested (5) |
Declined (4) |
Nothing — the brand sends a revised quote, which returns it to Sent |
Changes requested (5) |
Nothing — as above |
Approved (3) |
Nothing. Final. |
Expired (6) |
Nothing. Final. |
A customer may not set a quote to Sent — that is the brand's action, and attempting it returns 400 with "Customers cannot set the quote status to sent."
Note the loop: declining or requesting changes does not kill the quote. The team revises and re-sends, and the customer sees it as Sent again. Build your UI so a declined quote can come back to life rather than disappearing.
Step 5 — The quote becomes an order
Approving records the decision. It does not by itself create the order — order_id stays null until the quote's basket is checked out.
The conversion happens through the normal cart flow: the quote's basket is placed with POST /cart/{cart_id}/order, and on success the quote is marked approved and linked to the new order. From then on order_id names the real order, and you track it like any other — see Server Side Cart.
A quote can only be converted once. A quote that already has an order_id is refused if it is used again.
Catering fees
Catering orders usually carry service charges that a normal order does not — staffing, setup, cleanup, cutlery. You never send these. They are applied automatically when the cart is calculated, based on the catering context in metadata.order_type.properties.catering_info: number_of_people, event_type and catering_services.
What you can do is show the customer what to expect, by reading the fees the store has configured:
GET /api/v2/catering-feesfor the brand's fee definitionsGET /api/v2/venues/{venue_uuid}/catering-feesfor what a specific store actually charges
Fee types are 1 full service, 2 party size, 3 subtotal and 4 event type, each calculating as a flat amount, a percentage or per person.
Errors worth handling
| Situation | What you get |
|---|---|
| Guest email already belongs to a registered customer | 401 — prompt to sign in, then resubmit |
| Customer tried to set status to Sent | 400 |
| Status move not allowed from the current status | 422 |
Non-catering order_type on a lead |
422 — only 10 and 11 are valid |
| Quote or lead belongs to another customer | 404 |
customer_account_id does not match the signed-in customer |
401 |
| Brand not on the NMG product structure | 403 |