What you can do
- Show your menu on your own website or app. A public
GET returns the whole menu as JSON, ready to render however you like. No key needed. - Manage the menu from your system. Categories, products, prices, availability, variants, photos, logo and banner: everything the dashboard does, over HTTP and with an API key.
If what you want is an AI agent managing the menu through conversation, that is the MCP server (https://delimenu.co/api/mcp), which uses the same operations.
Get started in three steps
- In the dashboard, go to Settings › Developer API and click Create key. Choose a name and the permissions: view the menu, or view and edit.
- Copy the key when it appears. It starts with
dmk_ and is shown only once; if you lose it, revoke it and create another. - Send it with every request in the
Authorization header:
curl https://delimenu.co/api/v1/restaurants \
-H "Authorization: Bearer dmk_…"
The base URL is https://delimenu.co/api/v1. Responses are JSON in snake_case. Each account can have up to 10 active keys, and one key works for every restaurant in the account.
Your menu in your own frontend
The public menu of any restaurant is at https://delimenu.co/api/v1/menus/{identifier}, where identifier is the same one that appears in https://delimenu.co/{identifier}. It returns exactly what a diner sees: the categories with at least one visible product, in order, and their products with prices, promotions, availability, variants and photos.
const res = await fetch('https://delimenu.co/api/v1/menus/demo')
const { restaurant, categories } = await res.json()
for (const category of categories) {
console.log(category.name)
for (const product of category.products) {
console.log(' ', product.name, product.price, restaurant.currency)
}
}
It can be called straight from the browser (CORS is open) and it is cached at the edge: every change you make in the dashboard, through the API or with an agent refreshes it within seconds. If the restaurant has no active plan, it answers 403 with only its name, just like the public page.
Complete examples. Two Next.js sites that read a real menu with that single call and present it with a design of their own: Maison (code) and Qitchen (code). Copy the one you prefer and replace the identifier with yours.
Webhooks: hear about every change
Instead of polling the API, register a URL and Delimenu will send it a POST every time the menu changes: when a product or a category is created, edited or deleted, and when the restaurant details change. Create them from Settings › Webhooks in the dashboard or with POST /restaurants/{ref}/webhooks, up to 5 per restaurant. Available events: product.created, product.updated, product.deleted, category.created, category.updated, category.deleted, restaurant.updated, restaurant.deleted.
Every request is signed in the Delimenu-Signature header with the endpoint secret, which is shown only once. Always verify it, against the body exactly as it arrived:
import { createHmac, timingSafeEqual } from 'node:crypto'
// rawBody: the request body exactly as received (a string or Buffer), not re-serialized JSON
export function verifyDelimenuWebhook(rawBody, header, secret) {
const parts = Object.fromEntries(header.split(',').map(part => part.split('=')))
const timestamp = Number(parts.t)
if (!timestamp || Math.abs(Date.now() / 1000 - timestamp) > 300) return false
const expected = createHmac('sha256', secret).update(`${timestamp}.${rawBody}`).digest('hex')
const received = Buffer.from(parts.v1 ?? '', 'hex')
return received.length === 32 && timingSafeEqual(received, Buffer.from(expected, 'hex'))
}
// const ok = verifyDelimenuWebhook(rawBody, req.headers['delimenu-signature'], process.env.DELIMENU_WEBHOOK_SECRET)
Answer with any 2xx in under 10 seconds and do the work afterwards. If it fails, we retry with increasing backoff up to 8 times over roughly a day, so use the event id to avoid processing it twice. An endpoint that exhausts its retries on 20 events in a row is disabled; you can enable it again from the dashboard.
Good to know
- Prices are whole units of the restaurant currency, never cents:
12500 is 12,500 pesos, 5.99 is US$5.99. The currency comes in restaurant.currency. - Changes are immediate and show up on the public menu just as if you had made them in the dashboard. There is no draft and no publish button.
- A restaurant without an active plan (subscription or trial) answers
402 on every endpoint that takes a key, just as the dashboard shows the subscription notice. - Permissions are per key. A read-only key gets
403 on any write, with the WWW-Authenticate header saying which permission is missing. - Editing many products at once takes a single request, up to 50 per call.
- Images are sent as a public URL (JPG, PNG or WEBP, up to 5 MB). Delimenu downloads it, optimizes it and answers once the photo is ready, so those calls take a few seconds.
- Revoking a key in the dashboard cuts off access on the next request.
For AI agents and tools
- This same documentation in Markdown, with the reference and one example per endpoint, is at /llms.txt. It is the right thing to hand to Claude Code, Cursor or ChatGPT.
- The formal description is available as OpenAPI 3.0, with an
operationId and request and response examples on every operation, ready to generate a client from or to import into your tool. - If you want an agent to edit the menu through conversation, without writing any code, use the MCP server: same operations, authorized with OAuth from the agent itself.
Errors
Every error answers { "error", "message" }: error is a stable code in English for your program to branch on, and message is a text in Spanish that you can show as is. 401 and 403 responses add docs with the URL of this page, for anyone who reaches the API without having read it.
| HTTP | error | When |
|---|
400 | VALIDATION | A field is missing or has an invalid format. `issues` says which one. |
401 | UNAUTHORIZED | The key is missing, does not exist or was revoked. |
402 | NOT_PREMIUM | The restaurant has no active subscription or trial. |
403 | FORBIDDEN | The key lacks the permission the endpoint requires. |
404 | NOT_FOUND | The restaurant is not in your account, or the product or category does not exist in it. |
409 | CONFLICT | Duplicate category name, identifier already taken, or category that still has products. |
422 | LIMIT | A limit was reached. |
500 | INTERNAL | Something failed on our side. Try again in a moment. |
Reference
Every operation comes with its curl and an example response; the keys and ids in the examples are made up. Fields marked with * are required.
Public menu
No key needed. What delimenu.co/{identifier} shows.
Restaurants
The restaurants of the account the key belongs to.
get/restaurantsrequires menu:readlistRestaurants
List the restaurants of the account
Call this first; every other endpoint takes one of these by identifier or id.
Example
curl -X GET "https://delimenu.co/api/v1/restaurants" \
-H "Authorization: Bearer dmk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
Response 200
{
"restaurants": [
{
"id": "aR3kX9pLm2QzT7vN4bYc",
"identifier": "pizzeria-roma",
"name": "Pizzería Roma",
"currency": "COP",
"type": "whatsapp",
"phone": "+573001234567",
"address": "Calle 10 # 5-20, Bogotá",
"menu_url": "https://delimenu.co/pizzeria-roma",
"logo_url": "https://firebasestorage.googleapis.com/v0/b/example/o/logo_512x512.png?alt=media",
"banner_url": null,
"language": "es",
"languages": [
"en"
],
"trial_active": false
}
]
}
Response fields
| Field | Type | Description |
|---|
restaurants* | object[] | |
id* | string | |
identifier* | string | |
name* | string | |
currency* | string | ISO 4217 code every price is in |
type* | "whatsapp" | "read_only" | "whatsapp" takes orders by WhatsApp; "read_only" only shows the menu |
phone* | string | null | WhatsApp number in E.164, when ordering is enabled |
address* | string | null | |
menu_url* | string | |
logo_url* | string | null | |
banner_url* | string | null | |
language* | "es" | "en" | "pt" | "fr" | "de" | "it" | The primary language: the one name, description and every untranslated field are written in |
languages* | "es" | "en" | "pt" | "fr" | "de" | "it"[] | The additional languages diners can read the menu in, translated through `translations`. Empty for a menu in one language. |
trial_active* | boolean | |
Errors
401 UNAUTHORIZED — the key is missing, unknown or revoked402 NOT_PREMIUM — the restaurant has no active subscription or trial403 FORBIDDEN — the key lacks the scope this endpoint needs404 NOT_FOUND — no such restaurant in this account, or no such item in it500 INTERNAL — something failed on our side; retry in a moment
get/restaurants/{ref}requires menu:readgetRestaurant
Get a restaurant
Parameters
| Name | In | Description |
|---|
ref* | path | The restaurant identifier (the slug in its public URL, e.g. "pizzeria-roma") or its id, as returned by GET /restaurants |
Example
curl -X GET "https://delimenu.co/api/v1/restaurants/pizzeria-roma" \
-H "Authorization: Bearer dmk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
Response 200
{
"restaurant": {
"id": "aR3kX9pLm2QzT7vN4bYc",
"identifier": "pizzeria-roma",
"name": "Pizzería Roma",
"currency": "COP",
"type": "whatsapp",
"phone": "+573001234567",
"address": "Calle 10 # 5-20, Bogotá",
"menu_url": "https://delimenu.co/pizzeria-roma",
"logo_url": "https://firebasestorage.googleapis.com/v0/b/example/o/logo_512x512.png?alt=media",
"banner_url": null,
"language": "es",
"languages": [
"en"
],
"trial_active": false
}
}
Response fields
| Field | Type | Description |
|---|
restaurant* | object | |
id* | string | |
identifier* | string | |
name* | string | |
currency* | string | ISO 4217 code every price is in |
type* | "whatsapp" | "read_only" | "whatsapp" takes orders by WhatsApp; "read_only" only shows the menu |
phone* | string | null | WhatsApp number in E.164, when ordering is enabled |
address* | string | null | |
menu_url* | string | |
logo_url* | string | null | |
banner_url* | string | null | |
language* | "es" | "en" | "pt" | "fr" | "de" | "it" | The primary language: the one name, description and every untranslated field are written in |
languages* | "es" | "en" | "pt" | "fr" | "de" | "it"[] | The additional languages diners can read the menu in, translated through `translations`. Empty for a menu in one language. |
trial_active* | boolean | |
Errors
401 UNAUTHORIZED — the key is missing, unknown or revoked402 NOT_PREMIUM — the restaurant has no active subscription or trial403 FORBIDDEN — the key lacks the scope this endpoint needs404 NOT_FOUND — no such restaurant in this account, or no such item in it500 INTERNAL — something failed on our side; retry in a moment
patch/restaurants/{ref}requires menu:writeupdateRestaurant
Update a restaurant
Changes only the fields given: name, WhatsApp phone, address, currency, type, language (the primary one) or languages (the additional ones, never the primary). Changing the currency converts no prices and changing a language translates nothing. The identifier has its own endpoint.
Parameters
| Name | In | Description |
|---|
ref* | path | The restaurant identifier (the slug in its public URL, e.g. "pizzeria-roma") or its id, as returned by GET /restaurants |
Body (JSON)
| Field | Type | Description |
|---|
name | string | up to 50 characters |
phone | string | WhatsApp number that receives orders, in E.164 format (+573001234567) |
address | string | Street address shown on the menu. Empty string removes it.up to 65 characters |
currency | string | ISO 4217 code every price is shown in. Changing it converts nothing. |
type | "whatsapp" | "read_only" | "whatsapp" takes orders by WhatsApp; "read_only" only shows the menu |
language | "es" | "en" | "pt" | "fr" | "de" | "it" | The primary language of the menu: the one its names and descriptions are written in, and the one WhatsApp orders are composed in. Changing it translates nothing. If the new one was among `languages`, it is taken out of that list. |
languages | "es" | "en" | "pt" | "fr" | "de" | "it"[] | Up to 3 additional languages diners can read the menu in, translated by hand through `translations`. Never the primary language. Replaces the whole list; [] leaves the menu in one language. |
Example
curl -X PATCH "https://delimenu.co/api/v1/restaurants/pizzeria-roma" \
-H "Authorization: Bearer dmk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{"phone":"+573009876543","address":"Carrera 7 # 45-10, Bogotá"}'
Response 200
{
"restaurant": {
"id": "aR3kX9pLm2QzT7vN4bYc",
"identifier": "pizzeria-roma",
"name": "Pizzería Roma",
"currency": "COP",
"type": "whatsapp",
"phone": "+573009876543",
"address": "Carrera 7 # 45-10, Bogotá",
"menu_url": "https://delimenu.co/pizzeria-roma",
"logo_url": "https://firebasestorage.googleapis.com/v0/b/example/o/logo_512x512.png?alt=media",
"banner_url": null,
"language": "es",
"languages": [
"en"
],
"trial_active": false
}
}
Response fields
| Field | Type | Description |
|---|
restaurant* | object | |
id* | string | |
identifier* | string | |
name* | string | |
currency* | string | ISO 4217 code every price is in |
type* | "whatsapp" | "read_only" | "whatsapp" takes orders by WhatsApp; "read_only" only shows the menu |
phone* | string | null | WhatsApp number in E.164, when ordering is enabled |
address* | string | null | |
menu_url* | string | |
logo_url* | string | null | |
banner_url* | string | null | |
language* | "es" | "en" | "pt" | "fr" | "de" | "it" | The primary language: the one name, description and every untranslated field are written in |
languages* | "es" | "en" | "pt" | "fr" | "de" | "it"[] | The additional languages diners can read the menu in, translated through `translations`. Empty for a menu in one language. |
trial_active* | boolean | |
Errors
400 VALIDATION — a field is missing or malformed; `issues` names it401 UNAUTHORIZED — the key is missing, unknown or revoked402 NOT_PREMIUM — the restaurant has no active subscription or trial403 FORBIDDEN — the key lacks the scope this endpoint needs404 NOT_FOUND — no such restaurant in this account, or no such item in it500 INTERNAL — something failed on our side; retry in a moment
put/restaurants/{ref}/identifierrequires menu:writeupdateRestaurantIdentifier
Change the public URL
Changes the slug of the menu (delimenu.co/{identifier}). The old URL stops working immediately; printed QR codes keep working because they point at the restaurant id.
Parameters
| Name | In | Description |
|---|
ref* | path | The restaurant identifier (the slug in its public URL, e.g. "pizzeria-roma") or its id, as returned by GET /restaurants |
Body (JSON)
| Field | Type | Description |
|---|
identifier* | string | The new public slug: lowercase letters, digits, dots, hyphens and underscores, e.g. "pizzeria-roma"up to 50 characters |
Example
curl -X PUT "https://delimenu.co/api/v1/restaurants/pizzeria-roma/identifier" \
-H "Authorization: Bearer dmk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{"identifier":"pizzeria-roma-chapinero"}'
Response 200
{
"restaurant": {
"id": "aR3kX9pLm2QzT7vN4bYc",
"identifier": "pizzeria-roma-chapinero",
"name": "Pizzería Roma",
"currency": "COP",
"type": "whatsapp",
"phone": "+573001234567",
"address": "Calle 10 # 5-20, Bogotá",
"menu_url": "https://delimenu.co/pizzeria-roma-chapinero",
"logo_url": "https://firebasestorage.googleapis.com/v0/b/example/o/logo_512x512.png?alt=media",
"banner_url": null,
"language": "es",
"languages": [
"en"
],
"trial_active": false
}
}
Response fields
| Field | Type | Description |
|---|
restaurant* | object | |
id* | string | |
identifier* | string | |
name* | string | |
currency* | string | ISO 4217 code every price is in |
type* | "whatsapp" | "read_only" | "whatsapp" takes orders by WhatsApp; "read_only" only shows the menu |
phone* | string | null | WhatsApp number in E.164, when ordering is enabled |
address* | string | null | |
menu_url* | string | |
logo_url* | string | null | |
banner_url* | string | null | |
language* | "es" | "en" | "pt" | "fr" | "de" | "it" | The primary language: the one name, description and every untranslated field are written in |
languages* | "es" | "en" | "pt" | "fr" | "de" | "it"[] | The additional languages diners can read the menu in, translated through `translations`. Empty for a menu in one language. |
trial_active* | boolean | |
Errors
400 VALIDATION — a field is missing or malformed; `issues` names it401 UNAUTHORIZED — the key is missing, unknown or revoked402 NOT_PREMIUM — the restaurant has no active subscription or trial403 FORBIDDEN — the key lacks the scope this endpoint needs404 NOT_FOUND — no such restaurant in this account, or no such item in it409 CONFLICT — the identifier is already taken500 INTERNAL — something failed on our side; retry in a moment
Categories
get/restaurants/{ref}/categoriesrequires menu:readlistCategories
List the categories
Parameters
| Name | In | Description |
|---|
ref* | path | The restaurant identifier (the slug in its public URL, e.g. "pizzeria-roma") or its id, as returned by GET /restaurants |
Example
curl -X GET "https://delimenu.co/api/v1/restaurants/pizzeria-roma/categories" \
-H "Authorization: Bearer dmk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
Response 200
{
"categories": [
{
"id": "k3Qm8vXb2LpN7wRt1YaZ",
"name": "Pizzas",
"position": 1
},
{
"id": "c7Wd2Bn5Kq9Xr4Ms8Lt1",
"name": "Bebidas",
"position": 2
}
]
}
Response fields
| Field | Type | Description |
|---|
categories* | object[] | |
id* | string | |
name* | string | |
translations | object | The name in other languages, keyed by language code, e.g. { "en": { "name": "Large" } }. Absent when nothing is translated; a language missing here falls back to `name`. |
es | object | |
name | string | |
en | object | |
name | string | |
pt | object | |
name | string | |
fr | object | |
name | string | |
de | object | |
name | string | |
it | object | |
name | string | |
position* | integer | min -9007199254740991 · max 9007199254740991 |
Errors
401 UNAUTHORIZED — the key is missing, unknown or revoked402 NOT_PREMIUM — the restaurant has no active subscription or trial403 FORBIDDEN — the key lacks the scope this endpoint needs404 NOT_FOUND — no such restaurant in this account, or no such item in it500 INTERNAL — something failed on our side; retry in a moment
post/restaurants/{ref}/categoriesrequires menu:writecreateCategory
Create a category
Adds a category at the end of the menu. name is in the menu's primary language; translations carries it in the others.
Parameters
| Name | In | Description |
|---|
ref* | path | The restaurant identifier (the slug in its public URL, e.g. "pizzeria-roma") or its id, as returned by GET /restaurants |
Body (JSON)
| Field | Type | Description |
|---|
name* | string | Category nameup to 50 characters |
translations | object | The category name in the menu's other languages, e.g. { "en": { "name": "Drinks" } }. Keyed by language code (es, en, pt, fr, de, it). Shown to diners who read the menu in that language; a language without a translation falls back to the original text, and an entry in the menu's primary language is ignored. Replaces the whole map when provided; {} removes every translation. |
es | object | |
name | string | The name in that languageup to 50 characters |
en | object | |
name | string | The name in that languageup to 50 characters |
pt | object | |
name | string | The name in that languageup to 50 characters |
fr | object | |
name | string | The name in that languageup to 50 characters |
de | object | |
name | string | The name in that languageup to 50 characters |
it | object | |
name | string | The name in that languageup to 50 characters |
Example
curl -X POST "https://delimenu.co/api/v1/restaurants/pizzeria-roma/categories" \
-H "Authorization: Bearer dmk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{"name":"Postres","translations":{"en":{"name":"Desserts"}}}'
Response 201
{
"category": {
"id": "d4Fg6Hj8Kl0Zx2Cv4Bn6",
"name": "Postres",
"translations": {
"en": {
"name": "Desserts"
}
}
}
}
Response fields
| Field | Type | Description |
|---|
category* | object | |
id* | string | |
name* | string | |
translations | object | The name in other languages, keyed by language code, e.g. { "en": { "name": "Large" } }. Absent when nothing is translated; a language missing here falls back to `name`. |
es | object | |
name | string | |
en | object | |
name | string | |
pt | object | |
name | string | |
fr | object | |
name | string | |
de | object | |
name | string | |
it | object | |
name | string | |
Errors
400 VALIDATION — a field is missing or malformed; `issues` names it401 UNAUTHORIZED — the key is missing, unknown or revoked402 NOT_PREMIUM — the restaurant has no active subscription or trial403 FORBIDDEN — the key lacks the scope this endpoint needs404 NOT_FOUND — no such restaurant in this account, or no such item in it409 CONFLICT — a category with that name already exists500 INTERNAL — something failed on our side; retry in a moment
put/restaurants/{ref}/category-orderrequires menu:writereorderCategories
Reorder the categories
Sets the display order. Pass every category id exactly once, in the new order.
Parameters
| Name | In | Description |
|---|
ref* | path | The restaurant identifier (the slug in its public URL, e.g. "pizzeria-roma") or its id, as returned by GET /restaurants |
Body (JSON)
| Field | Type | Description |
|---|
category_ids* | string[] | Every category id exactly once, in the new display order |
Example
curl -X PUT "https://delimenu.co/api/v1/restaurants/pizzeria-roma/category-order" \
-H "Authorization: Bearer dmk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{"category_ids":["c7Wd2Bn5Kq9Xr4Ms8Lt1","k3Qm8vXb2LpN7wRt1YaZ"]}'
Response 200
{
"order_categories": [
"c7Wd2Bn5Kq9Xr4Ms8Lt1",
"k3Qm8vXb2LpN7wRt1YaZ"
]
}
Response fields
| Field | Type | Description |
|---|
order_categories* | string[] | |
Errors
400 VALIDATION — a field is missing or malformed; `issues` names it401 UNAUTHORIZED — the key is missing, unknown or revoked402 NOT_PREMIUM — the restaurant has no active subscription or trial403 FORBIDDEN — the key lacks the scope this endpoint needs404 NOT_FOUND — no such restaurant in this account, or no such item in it500 INTERNAL — something failed on our side; retry in a moment
patch/restaurants/{ref}/categories/{id}requires menu:writeupdateCategory
Rename or translate a category
Pass name, translations or both; what is not passed is left as it is. translations replaces the whole map, and {} removes every translation.
Parameters
| Name | In | Description |
|---|
ref* | path | The restaurant identifier (the slug in its public URL, e.g. "pizzeria-roma") or its id, as returned by GET /restaurants |
id* | path | The category id, from GET /restaurants/{ref}/categories |
Body (JSON)
| Field | Type | Description |
|---|
name | string | Category nameup to 50 characters |
translations | object | The category name in the menu's other languages, e.g. { "en": { "name": "Drinks" } }. Keyed by language code (es, en, pt, fr, de, it). Shown to diners who read the menu in that language; a language without a translation falls back to the original text, and an entry in the menu's primary language is ignored. Replaces the whole map when provided; {} removes every translation. |
es | object | |
name | string | The name in that languageup to 50 characters |
en | object | |
name | string | The name in that languageup to 50 characters |
pt | object | |
name | string | The name in that languageup to 50 characters |
fr | object | |
name | string | The name in that languageup to 50 characters |
de | object | |
name | string | The name in that languageup to 50 characters |
it | object | |
name | string | The name in that languageup to 50 characters |
Example
curl -X PATCH "https://delimenu.co/api/v1/restaurants/pizzeria-roma/categories/k3Qm8vXb2LpN7wRt1YaZ" \
-H "Authorization: Bearer dmk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{"name":"Pizzas artesanales"}'
Response 200
{
"category": {
"id": "k3Qm8vXb2LpN7wRt1YaZ",
"name": "Pizzas artesanales"
}
}
Response fields
| Field | Type | Description |
|---|
category* | object | |
id* | string | |
name* | string | |
translations | object | The name in other languages, keyed by language code, e.g. { "en": { "name": "Large" } }. Absent when nothing is translated; a language missing here falls back to `name`. |
es | object | |
name | string | |
en | object | |
name | string | |
pt | object | |
name | string | |
fr | object | |
name | string | |
de | object | |
name | string | |
it | object | |
name | string | |
Errors
400 VALIDATION — a field is missing or malformed; `issues` names it401 UNAUTHORIZED — the key is missing, unknown or revoked402 NOT_PREMIUM — the restaurant has no active subscription or trial403 FORBIDDEN — the key lacks the scope this endpoint needs404 NOT_FOUND — no such restaurant in this account, or no such item in it500 INTERNAL — something failed on our side; retry in a moment
delete/restaurants/{ref}/categories/{id}requires menu:writedeleteCategory
Delete a category
Refused with 409 while the category still has products, unless delete_products is true, in which case the products are deleted too.
Parameters
| Name | In | Description |
|---|
ref* | path | The restaurant identifier (the slug in its public URL, e.g. "pizzeria-roma") or its id, as returned by GET /restaurants |
id* | path | The category id, from GET /restaurants/{ref}/categories |
delete_products | query | Delete the products in the category too. Without it a non-empty category is refused with 409. |
Example
curl -X DELETE "https://delimenu.co/api/v1/restaurants/pizzeria-roma/categories/k3Qm8vXb2LpN7wRt1YaZ?delete_products=true" \
-H "Authorization: Bearer dmk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
Response 200
{
"deleted": "k3Qm8vXb2LpN7wRt1YaZ",
"deleted_products": 3
}
Response fields
| Field | Type | Description |
|---|
deleted* | string | |
deleted_products* | integer | min -9007199254740991 · max 9007199254740991 |
Errors
400 VALIDATION — a field is missing or malformed; `issues` names it401 UNAUTHORIZED — the key is missing, unknown or revoked402 NOT_PREMIUM — the restaurant has no active subscription or trial403 FORBIDDEN — the key lacks the scope this endpoint needs404 NOT_FOUND — no such restaurant in this account, or no such item in it409 CONFLICT — the category still has products500 INTERNAL — something failed on our side; retry in a moment
put/restaurants/{ref}/categories/{id}/product-orderrequires menu:writereorderProducts
Reorder the products of a category
Pass every product id of that category exactly once, in the new order.
Parameters
| Name | In | Description |
|---|
ref* | path | The restaurant identifier (the slug in its public URL, e.g. "pizzeria-roma") or its id, as returned by GET /restaurants |
id* | path | The category id, from GET /restaurants/{ref}/categories |
Body (JSON)
| Field | Type | Description |
|---|
product_ids* | string[] | Every product id of the category exactly once, in the new display order |
Example
curl -X PUT "https://delimenu.co/api/v1/restaurants/pizzeria-roma/categories/k3Qm8vXb2LpN7wRt1YaZ/product-order" \
-H "Authorization: Bearer dmk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{"product_ids":["q2Jk5Lm8Np1Qr4St7Uv0","p9Hs4TqL2mNc8VbX6Rdy"]}'
Response 200
{
"category_id": "k3Qm8vXb2LpN7wRt1YaZ",
"order_products": [
"q2Jk5Lm8Np1Qr4St7Uv0",
"p9Hs4TqL2mNc8VbX6Rdy"
]
}
Response fields
| Field | Type | Description |
|---|
category_id* | string | |
order_products* | string[] | |
Errors
400 VALIDATION — a field is missing or malformed; `issues` names it401 UNAUTHORIZED — the key is missing, unknown or revoked402 NOT_PREMIUM — the restaurant has no active subscription or trial403 FORBIDDEN — the key lacks the scope this endpoint needs404 NOT_FOUND — no such restaurant in this account, or no such item in it500 INTERNAL — something failed on our side; retry in a moment
Products
get/restaurants/{ref}/productsrequires menu:readlistProducts
List or search the products
Every product, hidden ones included and flagged. With q, only those whose name or description contains the text.
Parameters
| Name | In | Description |
|---|
ref* | path | The restaurant identifier (the slug in its public URL, e.g. "pizzeria-roma") or its id, as returned by GET /restaurants |
q | query | Only products whose name or description contains this text (accent- and case-insensitive)up to 100 characters |
Example
curl -X GET "https://delimenu.co/api/v1/restaurants/pizzeria-roma/products?q=margarita" \
-H "Authorization: Bearer dmk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
Response 200
{
"products": [
{
"id": "p9Hs4TqL2mNc8VbX6Rdy",
"category_id": "k3Qm8vXb2LpN7wRt1YaZ",
"name": "Pizza Margarita",
"price": 32000,
"original_price": 38000,
"description": "Tomate, mozzarella y albahaca fresca",
"available": true,
"hidden": false,
"has_image": true,
"image_url": "https://firebasestorage.googleapis.com/v0/b/example/o/margarita_1080x1080.jpg?alt=media",
"variants": [
{
"id": "26fed9f6-2e3b-4310-b899-5641bf98e2f0",
"name": "Tamaño",
"min_selections": 1,
"max_selections": 1,
"options": [
{
"id": "5c4f8896-06a3-4482-99d1-185a909d0415",
"name": "Personal",
"price": 0,
"show": true
},
{
"id": "b1e0c4d2-7f3a-4c8e-9d21-0a5f6e7b8c9d",
"name": "Familiar",
"price": 12000,
"show": true
}
]
}
]
}
],
"count": 1
}
Response fields
| Field | Type | Description |
|---|
products* | object[] | |
id* | string | |
category_id* | string | |
name* | string | |
price* | number | Major units in the restaurant's currency |
original_price | number | Present while the product is on promotion |
description | string | |
available* | boolean | false renders "Sin stock" |
hidden* | boolean | true keeps it off the public menu |
has_image* | boolean | |
image_url | string | |
variants* | object[] | |
id* | string | |
name* | string | |
min_selections* | integer | min -9007199254740991 · max 9007199254740991 |
max_selections* | integer | 0 is no limitmin -9007199254740991 · max 9007199254740991 |
max_per_option | integer | min -9007199254740991 · max 9007199254740991 |
options* | object[] | |
id* | string | |
name* | string | |
price* | number | Extra charge on top of the product price |
show* | boolean | |
translations | object | The name in other languages, keyed by language code, e.g. { "en": { "name": "Large" } }. Absent when nothing is translated; a language missing here falls back to `name`. |
translations | object | The name in other languages, keyed by language code, e.g. { "en": { "name": "Large" } }. Absent when nothing is translated; a language missing here falls back to `name`. |
es | object | |
en | object | |
pt | object | |
fr | object | |
de | object | |
it | object | |
translations | object | Name and description in other languages, keyed by language code. Absent when nothing is translated; a language or field missing here falls back to `name` / `description`. |
es | object | |
name | string | |
description | string | |
en | object | |
name | string | |
description | string | |
pt | object | |
name | string | |
description | string | |
fr | object | |
name | string | |
description | string | |
de | object | |
name | string | |
description | string | |
it | object | |
name | string | |
description | string | |
count* | integer | min -9007199254740991 · max 9007199254740991 |
Errors
400 VALIDATION — a field is missing or malformed; `issues` names it401 UNAUTHORIZED — the key is missing, unknown or revoked402 NOT_PREMIUM — the restaurant has no active subscription or trial403 FORBIDDEN — the key lacks the scope this endpoint needs404 NOT_FOUND — no such restaurant in this account, or no such item in it500 INTERNAL — something failed on our side; retry in a moment
post/restaurants/{ref}/productsrequires menu:writecreateProduct
Create a product
Adds a product to a category, at the end. Price is in the restaurant's currency, major units. Variants are optional choices (sizes, extras) with their own option prices.
Parameters
| Name | In | Description |
|---|
ref* | path | The restaurant identifier (the slug in its public URL, e.g. "pizzeria-roma") or its id, as returned by GET /restaurants |
Body (JSON)
| Field | Type | Description |
|---|
category_id* | string | The category the product belongs to (from get_menu)up to 128 characters |
name* | string | Product nameup to 50 characters |
price* | number | Price in the restaurant's currency, in major units (12500 for COP $12.000, 5.99 for USD $5.99). Never cents.min 0 |
description | string | Optional description. Pass an empty string to remove it.up to 3000 characters |
original_price | number | null | The price before a promotion. Must be greater than price; the menu shows it struck through. Pass null to end the promotion.min 0 |
available | boolean | false shows the product as "Sin stock": visible but not orderable. Default true. |
hidden | boolean | true keeps the product off the public menu entirely. Default false. |
variants | object[] | Choices the diner makes (sizes, extras). Replaces the whole list when provided; a variant or option sent without translations keeps the ones stored under the same name. |
name* | string | Variant name, e.g. "Tamaño" or "Adiciones"up to 50 characters |
translations | object | The variant name in the menu's other languages, e.g. { "en": { "name": "Size" } }. Omit it to keep the translations of the stored variant with the same name; {} removes them. |
es | object | |
name | string | The name in that languageup to 50 characters |
en | object | |
name | string | The name in that languageup to 50 characters |
pt | object | |
name | string | The name in that languageup to 50 characters |
fr | object | |
name | string | The name in that languageup to 50 characters |
de | object | |
name | string | The name in that languageup to 50 characters |
it | object | |
name | string | The name in that languageup to 50 characters |
min_selections | integer | Options the diner must pick. 0 makes the variant optional.min 0 · max 9007199254740991 · default 0 |
max_selections | integer | Options the diner may pick. 0 is no limit; 1 renders a single choice.min 0 · max 9007199254740991 · default 0 |
max_per_option | integer | How many times one option may be repeated. Omit or 0 for no limit.min 0 · max 9007199254740991 |
options* | object[] | |
name* | string | Option name, e.g. "Grande"up to 50 characters |
translations | object | The option name in the menu's other languages, e.g. { "en": { "name": "Large" } }. Omit it to keep the translations of the stored option with the same name; {} removes them. |
es | object | |
en | object | |
pt | object | |
fr | object | |
de | object | |
it | object | |
price | number | Extra charge for this option on top of the product price. 0 when it costs nothing.min 0 · default 0 |
show | boolean | false hides the option from diners without deleting itdefault true |
translations | object | The product name and description in the menu's other languages, e.g. { "en": { "name": "Lemonade", "description": "Freshly squeezed" } }. Keyed by language code (es, en, pt, fr, de, it). Shown to diners who read the menu in that language; a language without a translation falls back to the original text, and an entry in the menu's primary language is ignored. Replaces the whole map when provided; {} removes every translation. Variant and option names are translated inside each variant and option. |
es | object | |
name | string | The product name in that languageup to 50 characters |
description | string | The description in that languageup to 3000 characters |
en | object | |
name | string | The product name in that languageup to 50 characters |
description | string | The description in that languageup to 3000 characters |
pt | object | |
name | string | The product name in that languageup to 50 characters |
description | string | The description in that languageup to 3000 characters |
fr | object | |
name | string | The product name in that languageup to 50 characters |
description | string | The description in that languageup to 3000 characters |
de | object | |
name | string | The product name in that languageup to 50 characters |
description | string | The description in that languageup to 3000 characters |
it | object | |
name | string | The product name in that languageup to 50 characters |
description | string | The description in that languageup to 3000 characters |
Example
curl -X POST "https://delimenu.co/api/v1/restaurants/pizzeria-roma/products" \
-H "Authorization: Bearer dmk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{"category_id":"k3Qm8vXb2LpN7wRt1YaZ","name":"Pizza Margarita","price":32000,"description":"Tomate, mozzarella y albahaca fresca","variants":[{"name":"Tamaño","min_selections":1,"max_selections":1,"options":[{"name":"Personal","price":0},{"name":"Familiar","price":12000}]}]}'
Response 201
{
"product": {
"id": "p9Hs4TqL2mNc8VbX6Rdy",
"category_id": "k3Qm8vXb2LpN7wRt1YaZ",
"name": "Pizza Margarita",
"price": 32000,
"description": "Tomate, mozzarella y albahaca fresca",
"available": true,
"hidden": false,
"has_image": true,
"image_url": "https://firebasestorage.googleapis.com/v0/b/example/o/margarita_1080x1080.jpg?alt=media",
"variants": [
{
"id": "26fed9f6-2e3b-4310-b899-5641bf98e2f0",
"name": "Tamaño",
"min_selections": 1,
"max_selections": 1,
"options": [
{
"id": "5c4f8896-06a3-4482-99d1-185a909d0415",
"name": "Personal",
"price": 0,
"show": true
},
{
"id": "b1e0c4d2-7f3a-4c8e-9d21-0a5f6e7b8c9d",
"name": "Familiar",
"price": 12000,
"show": true
}
]
}
]
}
}
Response fields
| Field | Type | Description |
|---|
product* | object | |
id* | string | |
category_id* | string | |
name* | string | |
price* | number | Major units in the restaurant's currency |
original_price | number | Present while the product is on promotion |
description | string | |
available* | boolean | false renders "Sin stock" |
hidden* | boolean | true keeps it off the public menu |
has_image* | boolean | |
image_url | string | |
variants* | object[] | |
id* | string | |
name* | string | |
min_selections* | integer | min -9007199254740991 · max 9007199254740991 |
max_selections* | integer | 0 is no limitmin -9007199254740991 · max 9007199254740991 |
max_per_option | integer | min -9007199254740991 · max 9007199254740991 |
options* | object[] | |
id* | string | |
name* | string | |
price* | number | Extra charge on top of the product price |
show* | boolean | |
translations | object | The name in other languages, keyed by language code, e.g. { "en": { "name": "Large" } }. Absent when nothing is translated; a language missing here falls back to `name`. |
translations | object | The name in other languages, keyed by language code, e.g. { "en": { "name": "Large" } }. Absent when nothing is translated; a language missing here falls back to `name`. |
es | object | |
en | object | |
pt | object | |
fr | object | |
de | object | |
it | object | |
translations | object | Name and description in other languages, keyed by language code. Absent when nothing is translated; a language or field missing here falls back to `name` / `description`. |
es | object | |
name | string | |
description | string | |
en | object | |
name | string | |
description | string | |
pt | object | |
name | string | |
description | string | |
fr | object | |
name | string | |
description | string | |
de | object | |
name | string | |
description | string | |
it | object | |
name | string | |
description | string | |
Errors
400 VALIDATION — a field is missing or malformed; `issues` names it401 UNAUTHORIZED — the key is missing, unknown or revoked402 NOT_PREMIUM — the restaurant has no active subscription or trial403 FORBIDDEN — the key lacks the scope this endpoint needs404 NOT_FOUND — no such restaurant in this account, or no such item in it500 INTERNAL — something failed on our side; retry in a moment
patch/restaurants/{ref}/productsrequires menu:writebulkUpdateProducts
Update many products
Applies a patch to up to 50 products in one write — for "raise every price 10%" or "mark these as out of stock". Same fields as updating one product.
Parameters
| Name | In | Description |
|---|
ref* | path | The restaurant identifier (the slug in its public URL, e.g. "pizzeria-roma") or its id, as returned by GET /restaurants |
Body (JSON)
| Field | Type | Description |
|---|
updates* | object[] | Up to 50 products, each with only the fields to change |
product_id* | string | up to 128 characters |
changes* | object | |
category_id | string | Move the product to another categoryup to 128 characters |
name | string | Product nameup to 50 characters |
price | number | Price in the restaurant's currency, in major units (12500 for COP $12.000, 5.99 for USD $5.99). Never cents.min 0 |
description | string | Optional description. Pass an empty string to remove it.up to 3000 characters |
original_price | number | null | The price before a promotion. Must be greater than price; the menu shows it struck through. Pass null to end the promotion.min 0 |
available | boolean | false shows the product as "Sin stock": visible but not orderable. Default true. |
hidden | boolean | true keeps the product off the public menu entirely. Default false. |
variants | object[] | Choices the diner makes (sizes, extras). Replaces the whole list when provided; a variant or option sent without translations keeps the ones stored under the same name. |
name* | string | Variant name, e.g. "Tamaño" or "Adiciones"up to 50 characters |
translations | object | The variant name in the menu's other languages, e.g. { "en": { "name": "Size" } }. Omit it to keep the translations of the stored variant with the same name; {} removes them. |
min_selections | integer | Options the diner must pick. 0 makes the variant optional.min 0 · max 9007199254740991 · default 0 |
max_selections | integer | Options the diner may pick. 0 is no limit; 1 renders a single choice.min 0 · max 9007199254740991 · default 0 |
max_per_option | integer | How many times one option may be repeated. Omit or 0 for no limit.min 0 · max 9007199254740991 |
options* | object[] | |
translations | object | The product name and description in the menu's other languages, e.g. { "en": { "name": "Lemonade", "description": "Freshly squeezed" } }. Keyed by language code (es, en, pt, fr, de, it). Shown to diners who read the menu in that language; a language without a translation falls back to the original text, and an entry in the menu's primary language is ignored. Replaces the whole map when provided; {} removes every translation. Variant and option names are translated inside each variant and option. |
es | object | |
en | object | |
pt | object | |
fr | object | |
de | object | |
it | object | |
Example
curl -X PATCH "https://delimenu.co/api/v1/restaurants/pizzeria-roma/products" \
-H "Authorization: Bearer dmk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{"updates":[{"product_id":"p9Hs4TqL2mNc8VbX6Rdy","changes":{"price":35000}},{"product_id":"q2Jk5Lm8Np1Qr4St7Uv0","changes":{"available":false}}]}'
Response 200
{
"updated": 2
}
Response fields
| Field | Type | Description |
|---|
updated* | integer | min -9007199254740991 · max 9007199254740991 |
Errors
400 VALIDATION — a field is missing or malformed; `issues` names it401 UNAUTHORIZED — the key is missing, unknown or revoked402 NOT_PREMIUM — the restaurant has no active subscription or trial403 FORBIDDEN — the key lacks the scope this endpoint needs404 NOT_FOUND — no such restaurant in this account, or no such item in it500 INTERNAL — something failed on our side; retry in a moment
get/restaurants/{ref}/products/{id}requires menu:readgetProduct
Get a product
translations is present only where something is translated: on the product (name, description) and inside each variant and option (name).
Parameters
| Name | In | Description |
|---|
ref* | path | The restaurant identifier (the slug in its public URL, e.g. "pizzeria-roma") or its id, as returned by GET /restaurants |
id* | path | The product id, from GET /restaurants/{ref}/products |
Example
curl -X GET "https://delimenu.co/api/v1/restaurants/pizzeria-roma/products/p9Hs4TqL2mNc8VbX6Rdy" \
-H "Authorization: Bearer dmk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
Response 200
{
"product": {
"id": "p9Hs4TqL2mNc8VbX6Rdy",
"category_id": "k3Qm8vXb2LpN7wRt1YaZ",
"name": "Pizza Margarita",
"price": 32000,
"original_price": 38000,
"description": "Tomate, mozzarella y albahaca fresca",
"available": true,
"hidden": false,
"has_image": true,
"image_url": "https://firebasestorage.googleapis.com/v0/b/example/o/margarita_1080x1080.jpg?alt=media",
"variants": [
{
"id": "26fed9f6-2e3b-4310-b899-5641bf98e2f0",
"name": "Tamaño",
"min_selections": 1,
"max_selections": 1,
"options": [
{
"id": "5c4f8896-06a3-4482-99d1-185a909d0415",
"name": "Personal",
"price": 0,
"show": true
},
{
"id": "b1e0c4d2-7f3a-4c8e-9d21-0a5f6e7b8c9d",
"name": "Familiar",
"price": 12000,
"show": true,
"translations": {
"en": {
"name": "Family"
}
}
}
],
"translations": {
"en": {
"name": "Size"
}
}
}
],
"translations": {
"en": {
"name": "Margherita Pizza",
"description": "Tomato, mozzarella and fresh basil"
}
}
}
}
Response fields
| Field | Type | Description |
|---|
product* | object | |
id* | string | |
category_id* | string | |
name* | string | |
price* | number | Major units in the restaurant's currency |
original_price | number | Present while the product is on promotion |
description | string | |
available* | boolean | false renders "Sin stock" |
hidden* | boolean | true keeps it off the public menu |
has_image* | boolean | |
image_url | string | |
variants* | object[] | |
id* | string | |
name* | string | |
min_selections* | integer | min -9007199254740991 · max 9007199254740991 |
max_selections* | integer | 0 is no limitmin -9007199254740991 · max 9007199254740991 |
max_per_option | integer | min -9007199254740991 · max 9007199254740991 |
options* | object[] | |
id* | string | |
name* | string | |
price* | number | Extra charge on top of the product price |
show* | boolean | |
translations | object | The name in other languages, keyed by language code, e.g. { "en": { "name": "Large" } }. Absent when nothing is translated; a language missing here falls back to `name`. |
translations | object | The name in other languages, keyed by language code, e.g. { "en": { "name": "Large" } }. Absent when nothing is translated; a language missing here falls back to `name`. |
es | object | |
en | object | |
pt | object | |
fr | object | |
de | object | |
it | object | |
translations | object | Name and description in other languages, keyed by language code. Absent when nothing is translated; a language or field missing here falls back to `name` / `description`. |
es | object | |
name | string | |
description | string | |
en | object | |
name | string | |
description | string | |
pt | object | |
name | string | |
description | string | |
fr | object | |
name | string | |
description | string | |
de | object | |
name | string | |
description | string | |
it | object | |
name | string | |
description | string | |
Errors
401 UNAUTHORIZED — the key is missing, unknown or revoked402 NOT_PREMIUM — the restaurant has no active subscription or trial403 FORBIDDEN — the key lacks the scope this endpoint needs404 NOT_FOUND — no such restaurant in this account, or no such item in it500 INTERNAL — something failed on our side; retry in a moment
patch/restaurants/{ref}/products/{id}requires menu:writeupdateProduct
Update a product
Changes only the fields given: name, price, description, original_price (promotion), available, hidden, variants (replaces all), translations (replaces all) or category_id (moves it). Raising price above an existing original_price ends the promotion. A variant or option sent without translations keeps the ones stored under the same name, so repricing an option does not erase them; send translations: {} on it to remove them.
Parameters
| Name | In | Description |
|---|
ref* | path | The restaurant identifier (the slug in its public URL, e.g. "pizzeria-roma") or its id, as returned by GET /restaurants |
id* | path | The product id, from GET /restaurants/{ref}/products |
Body (JSON)
| Field | Type | Description |
|---|
category_id | string | Move the product to another categoryup to 128 characters |
name | string | Product nameup to 50 characters |
price | number | Price in the restaurant's currency, in major units (12500 for COP $12.000, 5.99 for USD $5.99). Never cents.min 0 |
description | string | Optional description. Pass an empty string to remove it.up to 3000 characters |
original_price | number | null | The price before a promotion. Must be greater than price; the menu shows it struck through. Pass null to end the promotion.min 0 |
available | boolean | false shows the product as "Sin stock": visible but not orderable. Default true. |
hidden | boolean | true keeps the product off the public menu entirely. Default false. |
variants | object[] | Choices the diner makes (sizes, extras). Replaces the whole list when provided; a variant or option sent without translations keeps the ones stored under the same name. |
name* | string | Variant name, e.g. "Tamaño" or "Adiciones"up to 50 characters |
translations | object | The variant name in the menu's other languages, e.g. { "en": { "name": "Size" } }. Omit it to keep the translations of the stored variant with the same name; {} removes them. |
es | object | |
name | string | The name in that languageup to 50 characters |
en | object | |
name | string | The name in that languageup to 50 characters |
pt | object | |
name | string | The name in that languageup to 50 characters |
fr | object | |
name | string | The name in that languageup to 50 characters |
de | object | |
name | string | The name in that languageup to 50 characters |
it | object | |
name | string | The name in that languageup to 50 characters |
min_selections | integer | Options the diner must pick. 0 makes the variant optional.min 0 · max 9007199254740991 · default 0 |
max_selections | integer | Options the diner may pick. 0 is no limit; 1 renders a single choice.min 0 · max 9007199254740991 · default 0 |
max_per_option | integer | How many times one option may be repeated. Omit or 0 for no limit.min 0 · max 9007199254740991 |
options* | object[] | |
name* | string | Option name, e.g. "Grande"up to 50 characters |
translations | object | The option name in the menu's other languages, e.g. { "en": { "name": "Large" } }. Omit it to keep the translations of the stored option with the same name; {} removes them. |
es | object | |
en | object | |
pt | object | |
fr | object | |
de | object | |
it | object | |
price | number | Extra charge for this option on top of the product price. 0 when it costs nothing.min 0 · default 0 |
show | boolean | false hides the option from diners without deleting itdefault true |
translations | object | The product name and description in the menu's other languages, e.g. { "en": { "name": "Lemonade", "description": "Freshly squeezed" } }. Keyed by language code (es, en, pt, fr, de, it). Shown to diners who read the menu in that language; a language without a translation falls back to the original text, and an entry in the menu's primary language is ignored. Replaces the whole map when provided; {} removes every translation. Variant and option names are translated inside each variant and option. |
es | object | |
name | string | The product name in that languageup to 50 characters |
description | string | The description in that languageup to 3000 characters |
en | object | |
name | string | The product name in that languageup to 50 characters |
description | string | The description in that languageup to 3000 characters |
pt | object | |
name | string | The product name in that languageup to 50 characters |
description | string | The description in that languageup to 3000 characters |
fr | object | |
name | string | The product name in that languageup to 50 characters |
description | string | The description in that languageup to 3000 characters |
de | object | |
name | string | The product name in that languageup to 50 characters |
description | string | The description in that languageup to 3000 characters |
it | object | |
name | string | The product name in that languageup to 50 characters |
description | string | The description in that languageup to 3000 characters |
Example
curl -X PATCH "https://delimenu.co/api/v1/restaurants/pizzeria-roma/products/p9Hs4TqL2mNc8VbX6Rdy" \
-H "Authorization: Bearer dmk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{"price":30000,"original_price":38000,"translations":{"en":{"name":"Margherita Pizza","description":"Tomato, mozzarella and fresh basil"}}}'
Response 200
{
"product": {
"id": "p9Hs4TqL2mNc8VbX6Rdy",
"category_id": "k3Qm8vXb2LpN7wRt1YaZ",
"name": "Pizza Margarita",
"price": 30000,
"original_price": 38000,
"description": "Tomate, mozzarella y albahaca fresca",
"available": true,
"hidden": false,
"has_image": true,
"image_url": "https://firebasestorage.googleapis.com/v0/b/example/o/margarita_1080x1080.jpg?alt=media",
"variants": [
{
"id": "26fed9f6-2e3b-4310-b899-5641bf98e2f0",
"name": "Tamaño",
"min_selections": 1,
"max_selections": 1,
"options": [
{
"id": "5c4f8896-06a3-4482-99d1-185a909d0415",
"name": "Personal",
"price": 0,
"show": true
},
{
"id": "b1e0c4d2-7f3a-4c8e-9d21-0a5f6e7b8c9d",
"name": "Familiar",
"price": 12000,
"show": true,
"translations": {
"en": {
"name": "Family"
}
}
}
],
"translations": {
"en": {
"name": "Size"
}
}
}
],
"translations": {
"en": {
"name": "Margherita Pizza",
"description": "Tomato, mozzarella and fresh basil"
}
}
}
}
Response fields
| Field | Type | Description |
|---|
product* | object | |
id* | string | |
category_id* | string | |
name* | string | |
price* | number | Major units in the restaurant's currency |
original_price | number | Present while the product is on promotion |
description | string | |
available* | boolean | false renders "Sin stock" |
hidden* | boolean | true keeps it off the public menu |
has_image* | boolean | |
image_url | string | |
variants* | object[] | |
id* | string | |
name* | string | |
min_selections* | integer | min -9007199254740991 · max 9007199254740991 |
max_selections* | integer | 0 is no limitmin -9007199254740991 · max 9007199254740991 |
max_per_option | integer | min -9007199254740991 · max 9007199254740991 |
options* | object[] | |
id* | string | |
name* | string | |
price* | number | Extra charge on top of the product price |
show* | boolean | |
translations | object | The name in other languages, keyed by language code, e.g. { "en": { "name": "Large" } }. Absent when nothing is translated; a language missing here falls back to `name`. |
translations | object | The name in other languages, keyed by language code, e.g. { "en": { "name": "Large" } }. Absent when nothing is translated; a language missing here falls back to `name`. |
es | object | |
en | object | |
pt | object | |
fr | object | |
de | object | |
it | object | |
translations | object | Name and description in other languages, keyed by language code. Absent when nothing is translated; a language or field missing here falls back to `name` / `description`. |
es | object | |
name | string | |
description | string | |
en | object | |
name | string | |
description | string | |
pt | object | |
name | string | |
description | string | |
fr | object | |
name | string | |
description | string | |
de | object | |
name | string | |
description | string | |
it | object | |
name | string | |
description | string | |
Errors
400 VALIDATION — a field is missing or malformed; `issues` names it401 UNAUTHORIZED — the key is missing, unknown or revoked402 NOT_PREMIUM — the restaurant has no active subscription or trial403 FORBIDDEN — the key lacks the scope this endpoint needs404 NOT_FOUND — no such restaurant in this account, or no such item in it500 INTERNAL — something failed on our side; retry in a moment
delete/restaurants/{ref}/products/{id}requires menu:writedeleteProduct
Delete a product
Permanent, photo included. Prefer hidden: true when the owner may want it back.
Parameters
| Name | In | Description |
|---|
ref* | path | The restaurant identifier (the slug in its public URL, e.g. "pizzeria-roma") or its id, as returned by GET /restaurants |
id* | path | The product id, from GET /restaurants/{ref}/products |
Example
curl -X DELETE "https://delimenu.co/api/v1/restaurants/pizzeria-roma/products/p9Hs4TqL2mNc8VbX6Rdy" \
-H "Authorization: Bearer dmk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
Response 200
{
"deleted": "p9Hs4TqL2mNc8VbX6Rdy"
}
Response fields
| Field | Type | Description |
|---|
deleted* | string | |
Errors
401 UNAUTHORIZED — the key is missing, unknown or revoked402 NOT_PREMIUM — the restaurant has no active subscription or trial403 FORBIDDEN — the key lacks the scope this endpoint needs404 NOT_FOUND — no such restaurant in this account, or no such item in it500 INTERNAL — something failed on our side; retry in a moment
Images
Photos, logo and banner from a public https URL (JPG, PNG or WEBP, up to 5 MB).
put/restaurants/{ref}/images/{kind}requires menu:writesetRestaurantImage
Set the logo or the banner
Downloads the image at a public https URL and sets it as the logo or the banner, replacing the current one. Takes a few seconds while the image is optimised.
Parameters
| Name | In | Description |
|---|
ref* | path | The restaurant identifier (the slug in its public URL, e.g. "pizzeria-roma") or its id, as returned by GET /restaurants |
kind* | path | "logo" is shown at 512×512; "banner" is the header image, 1080×1080 max"logo" | "banner" |
Body (JSON)
| Field | Type | Description |
|---|
image_url* | string | Public https URL of a JPG, PNG or WEBP image up to 5 MBup to 2048 characters |
Example
curl -X PUT "https://delimenu.co/api/v1/restaurants/pizzeria-roma/images/logo" \
-H "Authorization: Bearer dmk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{"image_url":"https://example.com/logo.png"}'
Response 200
{
"restaurant": {
"id": "aR3kX9pLm2QzT7vN4bYc",
"identifier": "pizzeria-roma",
"name": "Pizzería Roma",
"currency": "COP",
"type": "whatsapp",
"phone": "+573001234567",
"address": "Calle 10 # 5-20, Bogotá",
"menu_url": "https://delimenu.co/pizzeria-roma",
"logo_url": "https://firebasestorage.googleapis.com/v0/b/example/o/logo_512x512.png?alt=media",
"banner_url": null,
"language": "es",
"languages": [
"en"
],
"trial_active": false
}
}
Response fields
| Field | Type | Description |
|---|
restaurant* | object | |
id* | string | |
identifier* | string | |
name* | string | |
currency* | string | ISO 4217 code every price is in |
type* | "whatsapp" | "read_only" | "whatsapp" takes orders by WhatsApp; "read_only" only shows the menu |
phone* | string | null | WhatsApp number in E.164, when ordering is enabled |
address* | string | null | |
menu_url* | string | |
logo_url* | string | null | |
banner_url* | string | null | |
language* | "es" | "en" | "pt" | "fr" | "de" | "it" | The primary language: the one name, description and every untranslated field are written in |
languages* | "es" | "en" | "pt" | "fr" | "de" | "it"[] | The additional languages diners can read the menu in, translated through `translations`. Empty for a menu in one language. |
trial_active* | boolean | |
Errors
400 VALIDATION — a field is missing or malformed; `issues` names it401 UNAUTHORIZED — the key is missing, unknown or revoked402 NOT_PREMIUM — the restaurant has no active subscription or trial403 FORBIDDEN — the key lacks the scope this endpoint needs404 NOT_FOUND — no such restaurant in this account, or no such item in it500 INTERNAL — something failed on our side; retry in a moment
delete/restaurants/{ref}/images/{kind}requires menu:writeremoveRestaurantImage
Remove the logo or the banner
Parameters
| Name | In | Description |
|---|
ref* | path | The restaurant identifier (the slug in its public URL, e.g. "pizzeria-roma") or its id, as returned by GET /restaurants |
kind* | path | "logo" is shown at 512×512; "banner" is the header image, 1080×1080 max"logo" | "banner" |
Example
curl -X DELETE "https://delimenu.co/api/v1/restaurants/pizzeria-roma/images/logo" \
-H "Authorization: Bearer dmk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
Response 200
{
"restaurant": {
"id": "aR3kX9pLm2QzT7vN4bYc",
"identifier": "pizzeria-roma",
"name": "Pizzería Roma",
"currency": "COP",
"type": "whatsapp",
"phone": "+573001234567",
"address": "Calle 10 # 5-20, Bogotá",
"menu_url": "https://delimenu.co/pizzeria-roma",
"logo_url": null,
"banner_url": null,
"language": "es",
"languages": [
"en"
],
"trial_active": false
}
}
Response fields
| Field | Type | Description |
|---|
restaurant* | object | |
id* | string | |
identifier* | string | |
name* | string | |
currency* | string | ISO 4217 code every price is in |
type* | "whatsapp" | "read_only" | "whatsapp" takes orders by WhatsApp; "read_only" only shows the menu |
phone* | string | null | WhatsApp number in E.164, when ordering is enabled |
address* | string | null | |
menu_url* | string | |
logo_url* | string | null | |
banner_url* | string | null | |
language* | "es" | "en" | "pt" | "fr" | "de" | "it" | The primary language: the one name, description and every untranslated field are written in |
languages* | "es" | "en" | "pt" | "fr" | "de" | "it"[] | The additional languages diners can read the menu in, translated through `translations`. Empty for a menu in one language. |
trial_active* | boolean | |
Errors
401 UNAUTHORIZED — the key is missing, unknown or revoked402 NOT_PREMIUM — the restaurant has no active subscription or trial403 FORBIDDEN — the key lacks the scope this endpoint needs404 NOT_FOUND — no such restaurant in this account, or no such item in it500 INTERNAL — something failed on our side; retry in a moment
put/restaurants/{ref}/products/{id}/imagerequires menu:writesetProductImage
Set the product photo
Downloads the image at a public https URL and makes it the product photo, replacing the current one. Takes a few seconds while the image is optimised.
Parameters
| Name | In | Description |
|---|
ref* | path | The restaurant identifier (the slug in its public URL, e.g. "pizzeria-roma") or its id, as returned by GET /restaurants |
id* | path | The product id, from GET /restaurants/{ref}/products |
Body (JSON)
| Field | Type | Description |
|---|
image_url* | string | Public https URL of a JPG, PNG or WEBP image up to 5 MBup to 2048 characters |
Example
curl -X PUT "https://delimenu.co/api/v1/restaurants/pizzeria-roma/products/p9Hs4TqL2mNc8VbX6Rdy/image" \
-H "Authorization: Bearer dmk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{"image_url":"https://example.com/fotos/margarita.jpg"}'
Response 200
{
"product": {
"id": "p9Hs4TqL2mNc8VbX6Rdy",
"category_id": "k3Qm8vXb2LpN7wRt1YaZ",
"name": "Pizza Margarita",
"price": 32000,
"original_price": 38000,
"description": "Tomate, mozzarella y albahaca fresca",
"available": true,
"hidden": false,
"has_image": true,
"image_url": "https://firebasestorage.googleapis.com/v0/b/example/o/margarita_1080x1080.jpg?alt=media",
"variants": [
{
"id": "26fed9f6-2e3b-4310-b899-5641bf98e2f0",
"name": "Tamaño",
"min_selections": 1,
"max_selections": 1,
"options": [
{
"id": "5c4f8896-06a3-4482-99d1-185a909d0415",
"name": "Personal",
"price": 0,
"show": true
},
{
"id": "b1e0c4d2-7f3a-4c8e-9d21-0a5f6e7b8c9d",
"name": "Familiar",
"price": 12000,
"show": true
}
]
}
]
}
}
Response fields
| Field | Type | Description |
|---|
product* | object | |
id* | string | |
category_id* | string | |
name* | string | |
price* | number | Major units in the restaurant's currency |
original_price | number | Present while the product is on promotion |
description | string | |
available* | boolean | false renders "Sin stock" |
hidden* | boolean | true keeps it off the public menu |
has_image* | boolean | |
image_url | string | |
variants* | object[] | |
id* | string | |
name* | string | |
min_selections* | integer | min -9007199254740991 · max 9007199254740991 |
max_selections* | integer | 0 is no limitmin -9007199254740991 · max 9007199254740991 |
max_per_option | integer | min -9007199254740991 · max 9007199254740991 |
options* | object[] | |
id* | string | |
name* | string | |
price* | number | Extra charge on top of the product price |
show* | boolean | |
translations | object | The name in other languages, keyed by language code, e.g. { "en": { "name": "Large" } }. Absent when nothing is translated; a language missing here falls back to `name`. |
translations | object | The name in other languages, keyed by language code, e.g. { "en": { "name": "Large" } }. Absent when nothing is translated; a language missing here falls back to `name`. |
es | object | |
en | object | |
pt | object | |
fr | object | |
de | object | |
it | object | |
translations | object | Name and description in other languages, keyed by language code. Absent when nothing is translated; a language or field missing here falls back to `name` / `description`. |
es | object | |
name | string | |
description | string | |
en | object | |
name | string | |
description | string | |
pt | object | |
name | string | |
description | string | |
fr | object | |
name | string | |
description | string | |
de | object | |
name | string | |
description | string | |
it | object | |
name | string | |
description | string | |
Errors
400 VALIDATION — a field is missing or malformed; `issues` names it401 UNAUTHORIZED — the key is missing, unknown or revoked402 NOT_PREMIUM — the restaurant has no active subscription or trial403 FORBIDDEN — the key lacks the scope this endpoint needs404 NOT_FOUND — no such restaurant in this account, or no such item in it500 INTERNAL — something failed on our side; retry in a moment
delete/restaurants/{ref}/products/{id}/imagerequires menu:writeremoveProductImage
Remove the product photo
Parameters
| Name | In | Description |
|---|
ref* | path | The restaurant identifier (the slug in its public URL, e.g. "pizzeria-roma") or its id, as returned by GET /restaurants |
id* | path | The product id, from GET /restaurants/{ref}/products |
Example
curl -X DELETE "https://delimenu.co/api/v1/restaurants/pizzeria-roma/products/p9Hs4TqL2mNc8VbX6Rdy/image" \
-H "Authorization: Bearer dmk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
Response 200
{
"product_id": "p9Hs4TqL2mNc8VbX6Rdy",
"has_image": false
}
Response fields
| Field | Type | Description |
|---|
product_id* | string | |
has_image* | false | |
Errors
401 UNAUTHORIZED — the key is missing, unknown or revoked402 NOT_PREMIUM — the restaurant has no active subscription or trial403 FORBIDDEN — the key lacks the scope this endpoint needs404 NOT_FOUND — no such restaurant in this account, or no such item in it500 INTERNAL — something failed on our side; retry in a moment
Webhooks
Get notified when a menu changes instead of polling it. Events: product.created, product.updated, product.deleted, category.created, category.updated, category.deleted, restaurant.updated, restaurant.deleted. Each is a POST with a JSON body { id, type, api_version: "2026-10-01", created_at, restaurant: { id, identifier }, data: { object, changes? } }: object is the product, category or restaurant as the API returns it (categories add product_ids and restaurants add category_ids, in display order; a deleted item carries its last state), and changes lists the top-level fields an update touched. Writes that change nothing visible send nothing. Verify every request: the Delimenu-Signature header is t=<unix seconds>,v1=<hex>, where v1 is HMAC-SHA256 of "<t>.<raw body>" with the endpoint secret; compare in constant time and reject a t older than 5 minutes. Answer any 2xx within 10 seconds and do the work afterwards. Anything else is retried with exponential backoff, up to 8 attempts over about a day, so deduplicate by id (also in the Delimenu-Event-Id header). An endpoint that exhausts every retry on 20 events in a row is switched off. Only restaurants with an active subscription or trial send events.
get/restaurants/{ref}/webhooksrequires menu:readlistWebhooks
List the webhooks of a restaurant
Signing secrets are never returned here; only when an endpoint is created or its secret rotated.
Parameters
| Name | In | Description |
|---|
ref* | path | The restaurant identifier (the slug in its public URL, e.g. "pizzeria-roma") or its id, as returned by GET /restaurants |
Example
curl -X GET "https://delimenu.co/api/v1/restaurants/pizzeria-roma/webhooks" \
-H "Authorization: Bearer dmk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
Response 200
{
"webhooks": [
{
"id": "w7Kd2PqR9sLm4XvB1nTc",
"url": "https://pos.example.com/delimenu/webhooks",
"events": [
"product.created",
"product.updated",
"product.deleted"
],
"enabled": true,
"disabled_reason": null,
"created_at": "2026-10-01T15:04:05.000Z",
"last_delivery_at": "2026-10-02T09:30:00.000Z",
"last_status": 200
}
]
}
Response fields
| Field | Type | Description |
|---|
webhooks* | object[] | |
id* | string | |
url* | string | |
events* | "product.created" | "product.updated" | "product.deleted" | "category.created" | "category.updated" | "category.deleted" | "restaurant.updated" | "restaurant.deleted"[] | |
enabled* | boolean | |
disabled_reason* | "failing" | "manual" | "failing" when it was switched off for failing too many events in a row |
created_at* | string | null | |
last_delivery_at* | string | null | |
last_status* | integer | null | HTTP status of the last attempt; null if it never answeredmin -9007199254740991 · max 9007199254740991 |
Errors
401 UNAUTHORIZED — the key is missing, unknown or revoked402 NOT_PREMIUM — the restaurant has no active subscription or trial403 FORBIDDEN — the key lacks the scope this endpoint needs404 NOT_FOUND — no such restaurant in this account, or no such item in it500 INTERNAL — something failed on our side; retry in a moment
post/restaurants/{ref}/webhooksrequires menu:writecreateWebhook
Register a webhook
Starts sending the chosen events to the URL. The response carries the signing secret, the only time it is shown: store it and use it to verify Delimenu-Signature. Up to 5 per restaurant; one URL once per restaurant.
Parameters
| Name | In | Description |
|---|
ref* | path | The restaurant identifier (the slug in its public URL, e.g. "pizzeria-roma") or its id, as returned by GET /restaurants |
Body (JSON)
| Field | Type | Description |
|---|
url* | string | Where the events are POSTed: https, a public domain, port 443up to 2048 characters |
events* | "product.created" | "product.updated" | "product.deleted" | "category.created" | "category.updated" | "category.deleted" | "restaurant.updated" | "restaurant.deleted"[] | The events this endpoint receives |
Example
curl -X POST "https://delimenu.co/api/v1/restaurants/pizzeria-roma/webhooks" \
-H "Authorization: Bearer dmk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{"url":"https://pos.example.com/delimenu/webhooks","events":["product.created","product.updated","product.deleted"]}'
Response 201
{
"webhook": {
"id": "w7Kd2PqR9sLm4XvB1nTc",
"url": "https://pos.example.com/delimenu/webhooks",
"events": [
"product.created",
"product.updated",
"product.deleted"
],
"enabled": true,
"disabled_reason": null,
"created_at": "2026-10-01T15:04:05.000Z",
"last_delivery_at": null,
"last_status": null
},
"secret": "whsec_Zx8Qm2Lk5Vb9Tn3Rw7Yc1Hd4Fg6Js0Pa"
}
Response fields
| Field | Type | Description |
|---|
webhook* | object | |
id* | string | |
url* | string | |
events* | "product.created" | "product.updated" | "product.deleted" | "category.created" | "category.updated" | "category.deleted" | "restaurant.updated" | "restaurant.deleted"[] | |
enabled* | boolean | |
disabled_reason* | "failing" | "manual" | "failing" when it was switched off for failing too many events in a row |
created_at* | string | null | |
last_delivery_at* | string | null | |
last_status* | integer | null | HTTP status of the last attempt; null if it never answeredmin -9007199254740991 · max 9007199254740991 |
secret* | string | The signing secret. Shown only in this response; store it now |
Errors
400 VALIDATION — a field is missing or malformed; `issues` names it401 UNAUTHORIZED — the key is missing, unknown or revoked402 NOT_PREMIUM — the restaurant has no active subscription or trial403 FORBIDDEN — the key lacks the scope this endpoint needs404 NOT_FOUND — no such restaurant in this account, or no such item in it409 CONFLICT — this restaurant already has a webhook with that URL500 INTERNAL — something failed on our side; retry in a moment
patch/restaurants/{ref}/webhooks/{id}requires menu:writeupdateWebhook
Update a webhook
Changes only the fields given. enabled: true turns back on an endpoint that was switched off for failing, and clears its failure count.
Parameters
| Name | In | Description |
|---|
ref* | path | The restaurant identifier (the slug in its public URL, e.g. "pizzeria-roma") or its id, as returned by GET /restaurants |
id* | path | The webhook id, from GET /restaurants/{ref}/webhooks |
Body (JSON)
| Field | Type | Description |
|---|
url | string | Where the events are POSTed: https, a public domain, port 443up to 2048 characters |
events | "product.created" | "product.updated" | "product.deleted" | "category.created" | "category.updated" | "category.deleted" | "restaurant.updated" | "restaurant.deleted"[] | The events this endpoint receives |
enabled | boolean | false stops deliveries; true turns a switched-off endpoint back on and clears its failure count |
Example
curl -X PATCH "https://delimenu.co/api/v1/restaurants/pizzeria-roma/webhooks/w7Kd2PqR9sLm4XvB1nTc" \
-H "Authorization: Bearer dmk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{"enabled":true}'
Response 200
{
"webhook": {
"id": "w7Kd2PqR9sLm4XvB1nTc",
"url": "https://pos.example.com/delimenu/webhooks",
"events": [
"product.created",
"product.updated",
"product.deleted"
],
"enabled": true,
"disabled_reason": null,
"created_at": "2026-10-01T15:04:05.000Z",
"last_delivery_at": "2026-10-02T09:30:00.000Z",
"last_status": 200
}
}
Response fields
| Field | Type | Description |
|---|
webhook* | object | |
id* | string | |
url* | string | |
events* | "product.created" | "product.updated" | "product.deleted" | "category.created" | "category.updated" | "category.deleted" | "restaurant.updated" | "restaurant.deleted"[] | |
enabled* | boolean | |
disabled_reason* | "failing" | "manual" | "failing" when it was switched off for failing too many events in a row |
created_at* | string | null | |
last_delivery_at* | string | null | |
last_status* | integer | null | HTTP status of the last attempt; null if it never answeredmin -9007199254740991 · max 9007199254740991 |
Errors
400 VALIDATION — a field is missing or malformed; `issues` names it401 UNAUTHORIZED — the key is missing, unknown or revoked402 NOT_PREMIUM — the restaurant has no active subscription or trial403 FORBIDDEN — the key lacks the scope this endpoint needs404 NOT_FOUND — no such restaurant in this account, or no such item in it409 CONFLICT — this restaurant already has a webhook with that URL500 INTERNAL — something failed on our side; retry in a moment
delete/restaurants/{ref}/webhooks/{id}requires menu:writedeleteWebhook
Delete a webhook
Stops every delivery at once, retries already queued included.
Parameters
| Name | In | Description |
|---|
ref* | path | The restaurant identifier (the slug in its public URL, e.g. "pizzeria-roma") or its id, as returned by GET /restaurants |
id* | path | The webhook id, from GET /restaurants/{ref}/webhooks |
Example
curl -X DELETE "https://delimenu.co/api/v1/restaurants/pizzeria-roma/webhooks/w7Kd2PqR9sLm4XvB1nTc" \
-H "Authorization: Bearer dmk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
Response 200
{
"deleted": "w7Kd2PqR9sLm4XvB1nTc"
}
Response fields
| Field | Type | Description |
|---|
deleted* | string | |
Errors
401 UNAUTHORIZED — the key is missing, unknown or revoked402 NOT_PREMIUM — the restaurant has no active subscription or trial403 FORBIDDEN — the key lacks the scope this endpoint needs404 NOT_FOUND — no such restaurant in this account, or no such item in it500 INTERNAL — something failed on our side; retry in a moment
post/restaurants/{ref}/webhooks/{id}/secretrequires menu:writerotateWebhookSecret
Rotate the signing secret
Replaces the secret; the old one stops verifying on the very next delivery. The response is the only time the new one is shown.
Parameters
| Name | In | Description |
|---|
ref* | path | The restaurant identifier (the slug in its public URL, e.g. "pizzeria-roma") or its id, as returned by GET /restaurants |
id* | path | The webhook id, from GET /restaurants/{ref}/webhooks |
Example
curl -X POST "https://delimenu.co/api/v1/restaurants/pizzeria-roma/webhooks/w7Kd2PqR9sLm4XvB1nTc/secret" \
-H "Authorization: Bearer dmk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
Response 200
{
"webhook": {
"id": "w7Kd2PqR9sLm4XvB1nTc",
"url": "https://pos.example.com/delimenu/webhooks",
"events": [
"product.created",
"product.updated",
"product.deleted"
],
"enabled": true,
"disabled_reason": null,
"created_at": "2026-10-01T15:04:05.000Z",
"last_delivery_at": "2026-10-02T09:30:00.000Z",
"last_status": 200
},
"secret": "whsec_Zx8Qm2Lk5Vb9Tn3Rw7Yc1Hd4Fg6Js0Pa"
}
Response fields
| Field | Type | Description |
|---|
webhook* | object | |
id* | string | |
url* | string | |
events* | "product.created" | "product.updated" | "product.deleted" | "category.created" | "category.updated" | "category.deleted" | "restaurant.updated" | "restaurant.deleted"[] | |
enabled* | boolean | |
disabled_reason* | "failing" | "manual" | "failing" when it was switched off for failing too many events in a row |
created_at* | string | null | |
last_delivery_at* | string | null | |
last_status* | integer | null | HTTP status of the last attempt; null if it never answeredmin -9007199254740991 · max 9007199254740991 |
secret* | string | The signing secret. Shown only in this response; store it now |
Errors
401 UNAUTHORIZED — the key is missing, unknown or revoked402 NOT_PREMIUM — the restaurant has no active subscription or trial403 FORBIDDEN — the key lacks the scope this endpoint needs404 NOT_FOUND — no such restaurant in this account, or no such item in it500 INTERNAL — something failed on our side; retry in a moment
post/restaurants/{ref}/webhooks/{id}/testrequires menu:writetestWebhook
Send a test event
POSTs a signed "ping" event now and returns how the endpoint answered. A failing endpoint is still a 200 here, with ok: false. Works on a switched-off endpoint too.
Parameters
| Name | In | Description |
|---|
ref* | path | The restaurant identifier (the slug in its public URL, e.g. "pizzeria-roma") or its id, as returned by GET /restaurants |
id* | path | The webhook id, from GET /restaurants/{ref}/webhooks |
Example
curl -X POST "https://delimenu.co/api/v1/restaurants/pizzeria-roma/webhooks/w7Kd2PqR9sLm4XvB1nTc/test" \
-H "Authorization: Bearer dmk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
Response 200
{
"delivery": {
"id": "d4Nf8Kq2Rm6Xs1Vb9Lt3",
"event_id": "evt_test_8c1e4a7f2b9d3e6a0c5f",
"type": "ping",
"attempt": 1,
"ok": true,
"http_status": 200,
"duration_ms": 184,
"error": null,
"response_excerpt": "{\"received\":true}",
"created_at": "2026-10-02T09:30:00.000Z"
}
}
Response fields
| Field | Type | Description |
|---|
delivery* | object | |
id* | string | |
event_id* | string | |
type* | string | |
attempt* | integer | 1 for the first try, then each retrymin -9007199254740991 · max 9007199254740991 |
ok* | boolean | |
http_status* | integer | null | min -9007199254740991 · max 9007199254740991 |
duration_ms* | integer | min -9007199254740991 · max 9007199254740991 |
error* | string | null | "timeout", a network error, or null when the endpoint answered |
response_excerpt* | string | null | |
created_at* | string | null | |
Errors
401 UNAUTHORIZED — the key is missing, unknown or revoked402 NOT_PREMIUM — the restaurant has no active subscription or trial403 FORBIDDEN — the key lacks the scope this endpoint needs404 NOT_FOUND — no such restaurant in this account, or no such item in it500 INTERNAL — something failed on our side; retry in a moment
get/restaurants/{ref}/webhooks/{id}/deliveriesrequires menu:readlistWebhookDeliveries
Latest delivery attempts
The most recent attempts to this endpoint, newest first, retries included. Kept for 14 days.
Parameters
| Name | In | Description |
|---|
ref* | path | The restaurant identifier (the slug in its public URL, e.g. "pizzeria-roma") or its id, as returned by GET /restaurants |
id* | path | The webhook id, from GET /restaurants/{ref}/webhooks |
Example
curl -X GET "https://delimenu.co/api/v1/restaurants/pizzeria-roma/webhooks/w7Kd2PqR9sLm4XvB1nTc/deliveries" \
-H "Authorization: Bearer dmk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
Response 200
{
"deliveries": [
{
"id": "d4Nf8Kq2Rm6Xs1Vb9Lt3",
"event_id": "evt_3f9a1c7e5b2d8a4c6e0f1b3d",
"type": "product.updated",
"attempt": 1,
"ok": true,
"http_status": 200,
"duration_ms": 184,
"error": null,
"response_excerpt": "{\"received\":true}",
"created_at": "2026-10-02T09:30:00.000Z"
}
]
}
Response fields
| Field | Type | Description |
|---|
deliveries* | object[] | |
id* | string | |
event_id* | string | |
type* | string | |
attempt* | integer | 1 for the first try, then each retrymin -9007199254740991 · max 9007199254740991 |
ok* | boolean | |
http_status* | integer | null | min -9007199254740991 · max 9007199254740991 |
duration_ms* | integer | min -9007199254740991 · max 9007199254740991 |
error* | string | null | "timeout", a network error, or null when the endpoint answered |
response_excerpt* | string | null | |
created_at* | string | null | |
Errors
401 UNAUTHORIZED — the key is missing, unknown or revoked402 NOT_PREMIUM — the restaurant has no active subscription or trial403 FORBIDDEN — the key lacks the scope this endpoint needs404 NOT_FOUND — no such restaurant in this account, or no such item in it500 INTERNAL — something failed on our side; retry in a moment