Munchl orders, inside your own system.
The Partner API lets an establishment connect its existing ordering or POS system, so Munchl orders and reservations appear there automatically and status changes flow back to the customer. One HTTP integration, no custom build on our side.
Overview
JSON over HTTPS. Times are ISO 8601 in UTC. Amounts are in the establishment's own currency. Every key belongs to exactly one establishment and can only see and change that establishment's data.
Base URL
https://clebckfghqevunsukaru.supabase.co/functions/v1/partner_api/v1
1. Create a key
In the Munchl app: Profile → Connect your ordering system → Create API key. It's shown once; Munchl stores only a hash.
2. Add a webhook (optional)
A public https:// address to be told about new orders instantly. Copy the signing secret.
3. Test the connection
Call GET /restaurant, or press Send test event to receive a ping webhook.
4. Mirror orders
Poll or receive order.created, create the ticket in your system, then post accepted and later completed.
Authentication
Send the key as a bearer token. A missing, unknown or revoked key returns 401. Revoking a key in the app takes effect immediately.
Authorization: Bearer mk_live_xxxxxxxxxxxxxxxxxxxxxxxx
Orders
An order is one checkout (a ticket), with its items folded into lines with quantities.
placed_at). Query: status (placed, accepted, completed, cancelled), updated_since (ISO time; returns orders with a line updated at or after it, so use it to poll for new orders and changes), limit (default 50, kept between 1 and 200). An unknown status or an unreadable updated_since returns 400.id.{"status": "accepted"}. Values: accepted, completed (alias ready), cancelled. Returns the updated order. Sending the status the order already has returns it unchanged; changing a completed or cancelled order returns 409. The customer sees the change in their order tracking, and gets a notification when an order is accepted (from placed) or completed. Changes made through the API don't send you a webhook.Order object
{
"id": "og-1790510467291804",
"status": "placed",
"fulfillment_type": "dine_in",
"table_number": "12",
"pickup_name": "Sam",
"scheduled_for": null,
"placed_at": "2026-09-27T12:01:07.291+00:00",
"updated_at": "2026-09-27T12:01:07.291+00:00",
"customer": { "name": "Sam" },
"note": "ALLERGIES: Peanut, Sesame. Diet: Vegetarian. no onions",
"allergies": ["Peanut", "Sesame"],
"items": [
{ "menu_item_id": "3f6c2a9e-8b1d-4c57-9e0a-5d2b7f41c8a3", "name": "Caprese Panini", "quantity": 2, "unit_price": 9.5 }
],
"total": 19,
"source": { "post_id": "a81d4e02-6f3b-4b9c-b2e7-0c9f5d13e6a4", "creator_id": "c27e9b51-0d4a-4f83-a6c1-7b8e2f90d35e" }
} | Field | Notes |
|---|---|
status | placed → accepted → completed, or cancelled |
fulfillment_type | dine_in (with table_number), pickup, or delivery (the address is at the start of note, as DELIVER TO: …). pickup_name is the name the order was placed under and can be set on any type. |
scheduled_for | null means as soon as possible; otherwise the requested time |
allergies | Read from the ALLERGIES: part of note, which carries the customer's allergens from their profile. Empty when there are none. Safety-critical: show it to the kitchen. note also carries any diet and the customer's own requests. |
items / total | Identical lines are folded into one with a quantity. Cancelled lines are left out unless the whole order is cancelled. total is the sum of the lines, rounded to 2 decimals. |
source | Present when the order came from a creator's post, otherwise null. Attribution only. |
customer | Display name only: "Guest" when the account has no name, or null. Email and location are never exposed. |
Example: poll for new orders
curl -H "Authorization: Bearer $MUNCHL_KEY" \ "$BASE/orders?status=placed&updated_since=2026-09-27T11:00:00Z" # {"orders": [ {...}, {...} ]}
Example: accept an order
curl -X POST -H "Authorization: Bearer $MUNCHL_KEY" \ -H "Content-Type: application/json" -d '{"status":"accepted"}' \ "$BASE/orders/og-1789755507290284/status"
Reservations
Table requests made from your Munchl page, or created from scheduled table orders.
status (requested, confirmed, seated, completed, cancelled), from, to (ISO times on reserved_for, both inclusive).status: confirmed, seated, completed, cancelled. Returns the updated reservation.{ "reservations": [ {
"id": "9d2e4b17-5c80-4a6f-b3e9-60a1f7c28d54", "status": "requested",
"reserved_for": "2026-09-28T19:30:00+00:00", "party_size": 4,
"guest_name": "Sam", "note": "Window table if possible",
"created_at": "2026-09-27T09:14:52.118+00:00"
} ] } guest_name follows the same rule as an order's customer name, and note is null when there is none.
Webhooks
Instead of waiting for your next poll, Munchl can POST events to your endpoint as they happen.
| Event type | When |
|---|---|
order.created | A customer placed an order |
order.status_changed | The establishment marked an order ready, started a delivery, or marked it delivered in the Munchl app |
ping | You pressed Send test event. data is {"message": "Hello from Munchl. Your webhook endpoint is reachable."} |
Payload (data is the order object for order events)
{
"id": "5b1f0c7e-2a94-4d3b-8e61-9f07c4a2d815",
"type": "order.created",
"created_at": "2026-09-27T12:01:08.412Z",
"restaurant_id": "e4b7a1c9-3d52-4f06-9a8e-12c6f0b5d7e3",
"data": { …the order object… }
} Headers
Content-Type: application/json User-Agent: Munchl-Webhooks/1 Munchl-Event: order.created Munchl-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256>
The signature is the lowercase hex HMAC_SHA256(secret, t + "." + body) over the raw request body, keyed with the whole signing secret (including its whsec_ prefix). We suggest rejecting events whose timestamp is more than about five minutes old, as the examples do.
Node.js
const crypto = require('crypto');
// rawBody: the exact bytes you received (a Buffer), before any JSON parsing.
// secret: the whole signing secret, including its whsec_ prefix.
function verify(rawBody, header, secret) {
const parts = {};
for (const p of String(header || '').split(',')) {
const i = p.indexOf('=');
if (i > 0) parts[p.slice(0, i).trim()] = p.slice(i + 1).trim();
}
const t = Number(parts.t);
if (!parts.v1 || !Number.isInteger(t)) return false;
if (Math.abs(Date.now() / 1000 - t) > 300) return false;
const expected = crypto.createHmac('sha256', secret)
.update(parts.t + '.').update(rawBody).digest('hex');
const a = Buffer.from(expected);
const b = Buffer.from(parts.v1);
return a.length === b.length && crypto.timingSafeEqual(a, b);
} Python
import hmac, hashlib, time
# raw_body: the exact bytes you received, before any JSON parsing.
# secret: the whole signing secret, including its whsec_ prefix.
def verify(raw_body: bytes, header: str, secret: str) -> bool:
parts = dict(p.strip().split("=", 1) for p in header.split(",") if "=" in p)
t, v1 = parts.get("t", ""), parts.get("v1", "")
if not t.isdigit() or not v1:
return False
if abs(time.time() - int(t)) > 300:
return False
signed = t.encode() + b"." + raw_body
expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, v1) 2xx within 8 seconds. Redirects aren't followed. If the first attempt fails with a network error, a timeout or any response other than 2xx or 4xx, Munchl tries once more about a second later (same payload id, new timestamp and signature); a 4xx is not retried. Poll GET /orders?updated_since=… every 30 to 60 seconds as a safety net, and dedupe on order id plus status.Errors
Errors share one shape:
{ "error": { "code": "not_found", "message": "Order not found." } } | HTTP | Code | Meaning |
|---|---|---|
| 400 | bad_request | Bad or missing parameter |
| 401 | unauthenticated / invalid_key | No key, or key not valid |
| 404 | not_found | Unknown route, order or reservation |
| 409 | conflict | For example, changing an order that's already completed or cancelled |
| 500 | server_error | Try again |
Limits and security
- Keys are per establishment, stored hashed, and can be revoked and re-created at any time.
- Keep keys and webhook secrets on your server only, never in a mobile app or web page.
- Webhook addresses must be public
https://hosts; localhost, private IPv4 ranges and IPv6 literals are refused. - Per request: up to 200 orders, 500 reservations and 1,000 menu items. Polling every 30 to 60 seconds is plenty.
- Not included yet: menu editing, payments and refunds, and customer contact details.