Log inSign up
    Log inSign up
    EspañolEnglishPortuguês

    Developer API

    Manage your menu from your own system, or show it in a frontend built your way.

    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

    1. 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.
    2. Copy the key when it appears. It starts with dmk_ and is shown only once; if you lose it, revoke it and create another.
    3. 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.

    HTTPerrorWhen
    400VALIDATIONA field is missing or has an invalid format. `issues` says which one.
    401UNAUTHORIZEDThe key is missing, does not exist or was revoked.
    402NOT_PREMIUMThe restaurant has no active subscription or trial.
    403FORBIDDENThe key lacks the permission the endpoint requires.
    404NOT_FOUNDThe restaurant is not in your account, or the product or category does not exist in it.
    409CONFLICTDuplicate category name, identifier already taken, or category that still has products.
    422LIMITA limit was reached.
    500INTERNALSomething 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.

    get/menus/{identifier}publicgetPublicMenu

    Public menu of a restaurant

    The categories with at least one visible product, in display order, each with its visible products in display order. Hidden products and empty categories are never included. restaurant.language is the language name and description are written in, and restaurant.languages the ones the menu is translated into: to render one of those, read translations[code] on each category, product, variant and option and fall back to the original field when it is missing. Cached at the edge and refreshed on every change the owner makes; a differently-cased identifier is redirected to the lowercase one.

    Parameters

    NameInDescription
    identifier*pathThe slug in the public URL: delimenu.co/{identifier}

    Example

    curl -X GET "https://delimenu.co/api/v1/menus/pizzeria-roma"

    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"
        ]
      },
      "categories": [
        {
          "id": "k3Qm8vXb2LpN7wRt1YaZ",
          "name": "Pizzas",
          "position": 1,
          "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
                    }
                  ]
                }
              ]
            }
          ]
        }
      ]
    }

    Response fields

    FieldTypeDescription
    restaurant*object
    id*string
    identifier*string
    name*string
    currency*stringISO 4217 code every price is in
    type*"whatsapp" | "read_only""whatsapp" takes orders by WhatsApp; "read_only" only shows the menu
    phone*string | nullWhatsApp 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.
    categories*object[]Categories with at least one visible product, in display order
    id*string
    name*string
    translationsobjectThe 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`.
    esobject
    namestring
    enobject
    namestring
    ptobject
    namestring
    frobject
    namestring
    deobject
    namestring
    itobject
    namestring
    position*integer1-based display positionmin -9007199254740991 · max 9007199254740991
    products*object[]
    id*string
    category_id*string
    name*string
    price*numberMajor units in the restaurant's currency
    original_pricenumberPresent while the product is on promotion
    descriptionstring
    available*booleanfalse renders "Sin stock"
    hidden*booleantrue keeps it off the public menu
    has_image*boolean
    image_urlstring
    variants*object[]
    id*string
    name*string
    min_selections*integermin -9007199254740991 · max 9007199254740991
    max_selections*integer0 is no limitmin -9007199254740991 · max 9007199254740991
    max_per_optionintegermin -9007199254740991 · max 9007199254740991
    options*object[]
    translationsobjectThe 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`.
    translationsobjectName 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`.
    esobject
    enobject
    ptobject
    frobject
    deobject
    itobject

    Errors

    • 403 RESTAURANT_UNAVAILABLE — the restaurant exists but has no active plan; only its name is returned
    • 404 NOT_FOUND — no such restaurant in this account, or no such item in it
    • 500 INTERNAL — something failed on our side; retry in a moment

    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

    FieldTypeDescription
    restaurants*object[]
    id*string
    identifier*string
    name*string
    currency*stringISO 4217 code every price is in
    type*"whatsapp" | "read_only""whatsapp" takes orders by WhatsApp; "read_only" only shows the menu
    phone*string | nullWhatsApp 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 revoked
    • 402 NOT_PREMIUM — the restaurant has no active subscription or trial
    • 403 FORBIDDEN — the key lacks the scope this endpoint needs
    • 404 NOT_FOUND — no such restaurant in this account, or no such item in it
    • 500 INTERNAL — something failed on our side; retry in a moment
    get/restaurants/{ref}requires menu:readgetRestaurant

    Get a restaurant

    Parameters

    NameInDescription
    ref*pathThe 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

    FieldTypeDescription
    restaurant*object
    id*string
    identifier*string
    name*string
    currency*stringISO 4217 code every price is in
    type*"whatsapp" | "read_only""whatsapp" takes orders by WhatsApp; "read_only" only shows the menu
    phone*string | nullWhatsApp 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 revoked
    • 402 NOT_PREMIUM — the restaurant has no active subscription or trial
    • 403 FORBIDDEN — the key lacks the scope this endpoint needs
    • 404 NOT_FOUND — no such restaurant in this account, or no such item in it
    • 500 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

    NameInDescription
    ref*pathThe restaurant identifier (the slug in its public URL, e.g. "pizzeria-roma") or its id, as returned by GET /restaurants

    Body (JSON)

    FieldTypeDescription
    namestringup to 50 characters
    phonestringWhatsApp number that receives orders, in E.164 format (+573001234567)
    addressstringStreet address shown on the menu. Empty string removes it.up to 65 characters
    currencystringISO 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

    FieldTypeDescription
    restaurant*object
    id*string
    identifier*string
    name*string
    currency*stringISO 4217 code every price is in
    type*"whatsapp" | "read_only""whatsapp" takes orders by WhatsApp; "read_only" only shows the menu
    phone*string | nullWhatsApp 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 it
    • 401 UNAUTHORIZED — the key is missing, unknown or revoked
    • 402 NOT_PREMIUM — the restaurant has no active subscription or trial
    • 403 FORBIDDEN — the key lacks the scope this endpoint needs
    • 404 NOT_FOUND — no such restaurant in this account, or no such item in it
    • 500 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

    NameInDescription
    ref*pathThe restaurant identifier (the slug in its public URL, e.g. "pizzeria-roma") or its id, as returned by GET /restaurants

    Body (JSON)

    FieldTypeDescription
    identifier*stringThe 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

    FieldTypeDescription
    restaurant*object
    id*string
    identifier*string
    name*string
    currency*stringISO 4217 code every price is in
    type*"whatsapp" | "read_only""whatsapp" takes orders by WhatsApp; "read_only" only shows the menu
    phone*string | nullWhatsApp 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 it
    • 401 UNAUTHORIZED — the key is missing, unknown or revoked
    • 402 NOT_PREMIUM — the restaurant has no active subscription or trial
    • 403 FORBIDDEN — the key lacks the scope this endpoint needs
    • 404 NOT_FOUND — no such restaurant in this account, or no such item in it
    • 409 CONFLICT — the identifier is already taken
    • 500 INTERNAL — something failed on our side; retry in a moment
    get/restaurants/{ref}/menurequires menu:readgetMenu

    Get the whole menu

    Every category in display order, each with its products in display order, prices, availability and variants. Hidden products are omitted unless include_hidden is true. Uncategorized products are listed apart.

    Parameters

    NameInDescription
    ref*pathThe restaurant identifier (the slug in its public URL, e.g. "pizzeria-roma") or its id, as returned by GET /restaurants
    include_hiddenqueryAlso return the products hidden from the public menu

    Example

    curl -X GET "https://delimenu.co/api/v1/restaurants/pizzeria-roma/menu?include_hidden=true" \
      -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
      },
      "categories": [
        {
          "id": "k3Qm8vXb2LpN7wRt1YaZ",
          "name": "Pizzas",
          "position": 1,
          "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
                    }
                  ]
                }
              ]
            }
          ]
        }
      ],
      "uncategorized_products": [],
      "hidden_products_omitted": 2,
      "note": "Prices are in COP, major units."
    }

    Response fields

    FieldTypeDescription
    restaurant*object
    id*string
    identifier*string
    name*string
    currency*stringISO 4217 code every price is in
    type*"whatsapp" | "read_only""whatsapp" takes orders by WhatsApp; "read_only" only shows the menu
    phone*string | nullWhatsApp 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
    categories*object[]
    id*string
    name*string
    translationsobjectThe 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`.
    esobject
    namestring
    enobject
    namestring
    ptobject
    namestring
    frobject
    namestring
    deobject
    namestring
    itobject
    namestring
    position*integer1-based display positionmin -9007199254740991 · max 9007199254740991
    products*object[]
    id*string
    category_id*string
    name*string
    price*numberMajor units in the restaurant's currency
    original_pricenumberPresent while the product is on promotion
    descriptionstring
    available*booleanfalse renders "Sin stock"
    hidden*booleantrue keeps it off the public menu
    has_image*boolean
    image_urlstring
    variants*object[]
    id*string
    name*string
    min_selections*integermin -9007199254740991 · max 9007199254740991
    max_selections*integer0 is no limitmin -9007199254740991 · max 9007199254740991
    max_per_optionintegermin -9007199254740991 · max 9007199254740991
    options*object[]
    translationsobjectThe 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`.
    translationsobjectName 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`.
    esobject
    enobject
    ptobject
    frobject
    deobject
    itobject
    uncategorized_products*object[]
    id*string
    category_id*string
    name*string
    price*numberMajor units in the restaurant's currency
    original_pricenumberPresent while the product is on promotion
    descriptionstring
    available*booleanfalse renders "Sin stock"
    hidden*booleantrue keeps it off the public menu
    has_image*boolean
    image_urlstring
    variants*object[]
    id*string
    name*string
    min_selections*integermin -9007199254740991 · max 9007199254740991
    max_selections*integer0 is no limitmin -9007199254740991 · max 9007199254740991
    max_per_optionintegermin -9007199254740991 · max 9007199254740991
    options*object[]
    id*string
    name*string
    price*numberExtra charge on top of the product price
    show*boolean
    translationsobjectThe 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`.
    translationsobjectThe 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`.
    esobject
    enobject
    ptobject
    frobject
    deobject
    itobject
    translationsobjectName 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`.
    esobject
    namestring
    descriptionstring
    enobject
    namestring
    descriptionstring
    ptobject
    namestring
    descriptionstring
    frobject
    namestring
    descriptionstring
    deobject
    namestring
    descriptionstring
    itobject
    namestring
    descriptionstring
    hidden_products_omitted*integermin -9007199254740991 · max 9007199254740991
    note*string

    Errors

    • 400 VALIDATION — a field is missing or malformed; `issues` names it
    • 401 UNAUTHORIZED — the key is missing, unknown or revoked
    • 402 NOT_PREMIUM — the restaurant has no active subscription or trial
    • 403 FORBIDDEN — the key lacks the scope this endpoint needs
    • 404 NOT_FOUND — no such restaurant in this account, or no such item in it
    • 500 INTERNAL — something failed on our side; retry in a moment

    Categories

    get/restaurants/{ref}/categoriesrequires menu:readlistCategories

    List the categories

    Parameters

    NameInDescription
    ref*pathThe 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

    FieldTypeDescription
    categories*object[]
    id*string
    name*string
    translationsobjectThe 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`.
    esobject
    namestring
    enobject
    namestring
    ptobject
    namestring
    frobject
    namestring
    deobject
    namestring
    itobject
    namestring
    position*integermin -9007199254740991 · max 9007199254740991

    Errors

    • 401 UNAUTHORIZED — the key is missing, unknown or revoked
    • 402 NOT_PREMIUM — the restaurant has no active subscription or trial
    • 403 FORBIDDEN — the key lacks the scope this endpoint needs
    • 404 NOT_FOUND — no such restaurant in this account, or no such item in it
    • 500 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

    NameInDescription
    ref*pathThe restaurant identifier (the slug in its public URL, e.g. "pizzeria-roma") or its id, as returned by GET /restaurants

    Body (JSON)

    FieldTypeDescription
    name*stringCategory nameup to 50 characters
    translationsobjectThe 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.
    esobject
    namestringThe name in that languageup to 50 characters
    enobject
    namestringThe name in that languageup to 50 characters
    ptobject
    namestringThe name in that languageup to 50 characters
    frobject
    namestringThe name in that languageup to 50 characters
    deobject
    namestringThe name in that languageup to 50 characters
    itobject
    namestringThe 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

    FieldTypeDescription
    category*object
    id*string
    name*string
    translationsobjectThe 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`.
    esobject
    namestring
    enobject
    namestring
    ptobject
    namestring
    frobject
    namestring
    deobject
    namestring
    itobject
    namestring

    Errors

    • 400 VALIDATION — a field is missing or malformed; `issues` names it
    • 401 UNAUTHORIZED — the key is missing, unknown or revoked
    • 402 NOT_PREMIUM — the restaurant has no active subscription or trial
    • 403 FORBIDDEN — the key lacks the scope this endpoint needs
    • 404 NOT_FOUND — no such restaurant in this account, or no such item in it
    • 409 CONFLICT — a category with that name already exists
    • 500 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

    NameInDescription
    ref*pathThe restaurant identifier (the slug in its public URL, e.g. "pizzeria-roma") or its id, as returned by GET /restaurants

    Body (JSON)

    FieldTypeDescription
    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

    FieldTypeDescription
    order_categories*string[]

    Errors

    • 400 VALIDATION — a field is missing or malformed; `issues` names it
    • 401 UNAUTHORIZED — the key is missing, unknown or revoked
    • 402 NOT_PREMIUM — the restaurant has no active subscription or trial
    • 403 FORBIDDEN — the key lacks the scope this endpoint needs
    • 404 NOT_FOUND — no such restaurant in this account, or no such item in it
    • 500 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

    NameInDescription
    ref*pathThe restaurant identifier (the slug in its public URL, e.g. "pizzeria-roma") or its id, as returned by GET /restaurants
    id*pathThe category id, from GET /restaurants/{ref}/categories

    Body (JSON)

    FieldTypeDescription
    namestringCategory nameup to 50 characters
    translationsobjectThe 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.
    esobject
    namestringThe name in that languageup to 50 characters
    enobject
    namestringThe name in that languageup to 50 characters
    ptobject
    namestringThe name in that languageup to 50 characters
    frobject
    namestringThe name in that languageup to 50 characters
    deobject
    namestringThe name in that languageup to 50 characters
    itobject
    namestringThe 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

    FieldTypeDescription
    category*object
    id*string
    name*string
    translationsobjectThe 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`.
    esobject
    namestring
    enobject
    namestring
    ptobject
    namestring
    frobject
    namestring
    deobject
    namestring
    itobject
    namestring

    Errors

    • 400 VALIDATION — a field is missing or malformed; `issues` names it
    • 401 UNAUTHORIZED — the key is missing, unknown or revoked
    • 402 NOT_PREMIUM — the restaurant has no active subscription or trial
    • 403 FORBIDDEN — the key lacks the scope this endpoint needs
    • 404 NOT_FOUND — no such restaurant in this account, or no such item in it
    • 500 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

    NameInDescription
    ref*pathThe restaurant identifier (the slug in its public URL, e.g. "pizzeria-roma") or its id, as returned by GET /restaurants
    id*pathThe category id, from GET /restaurants/{ref}/categories
    delete_productsqueryDelete 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

    FieldTypeDescription
    deleted*string
    deleted_products*integermin -9007199254740991 · max 9007199254740991

    Errors

    • 400 VALIDATION — a field is missing or malformed; `issues` names it
    • 401 UNAUTHORIZED — the key is missing, unknown or revoked
    • 402 NOT_PREMIUM — the restaurant has no active subscription or trial
    • 403 FORBIDDEN — the key lacks the scope this endpoint needs
    • 404 NOT_FOUND — no such restaurant in this account, or no such item in it
    • 409 CONFLICT — the category still has products
    • 500 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

    NameInDescription
    ref*pathThe restaurant identifier (the slug in its public URL, e.g. "pizzeria-roma") or its id, as returned by GET /restaurants
    id*pathThe category id, from GET /restaurants/{ref}/categories

    Body (JSON)

    FieldTypeDescription
    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

    FieldTypeDescription
    category_id*string
    order_products*string[]

    Errors

    • 400 VALIDATION — a field is missing or malformed; `issues` names it
    • 401 UNAUTHORIZED — the key is missing, unknown or revoked
    • 402 NOT_PREMIUM — the restaurant has no active subscription or trial
    • 403 FORBIDDEN — the key lacks the scope this endpoint needs
    • 404 NOT_FOUND — no such restaurant in this account, or no such item in it
    • 500 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

    NameInDescription
    ref*pathThe restaurant identifier (the slug in its public URL, e.g. "pizzeria-roma") or its id, as returned by GET /restaurants
    qqueryOnly 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

    FieldTypeDescription
    products*object[]
    id*string
    category_id*string
    name*string
    price*numberMajor units in the restaurant's currency
    original_pricenumberPresent while the product is on promotion
    descriptionstring
    available*booleanfalse renders "Sin stock"
    hidden*booleantrue keeps it off the public menu
    has_image*boolean
    image_urlstring
    variants*object[]
    id*string
    name*string
    min_selections*integermin -9007199254740991 · max 9007199254740991
    max_selections*integer0 is no limitmin -9007199254740991 · max 9007199254740991
    max_per_optionintegermin -9007199254740991 · max 9007199254740991
    options*object[]
    id*string
    name*string
    price*numberExtra charge on top of the product price
    show*boolean
    translationsobjectThe 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`.
    translationsobjectThe 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`.
    esobject
    enobject
    ptobject
    frobject
    deobject
    itobject
    translationsobjectName 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`.
    esobject
    namestring
    descriptionstring
    enobject
    namestring
    descriptionstring
    ptobject
    namestring
    descriptionstring
    frobject
    namestring
    descriptionstring
    deobject
    namestring
    descriptionstring
    itobject
    namestring
    descriptionstring
    count*integermin -9007199254740991 · max 9007199254740991

    Errors

    • 400 VALIDATION — a field is missing or malformed; `issues` names it
    • 401 UNAUTHORIZED — the key is missing, unknown or revoked
    • 402 NOT_PREMIUM — the restaurant has no active subscription or trial
    • 403 FORBIDDEN — the key lacks the scope this endpoint needs
    • 404 NOT_FOUND — no such restaurant in this account, or no such item in it
    • 500 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

    NameInDescription
    ref*pathThe restaurant identifier (the slug in its public URL, e.g. "pizzeria-roma") or its id, as returned by GET /restaurants

    Body (JSON)

    FieldTypeDescription
    category_id*stringThe category the product belongs to (from get_menu)up to 128 characters
    name*stringProduct nameup to 50 characters
    price*numberPrice in the restaurant's currency, in major units (12500 for COP $12.000, 5.99 for USD $5.99). Never cents.min 0
    descriptionstringOptional description. Pass an empty string to remove it.up to 3000 characters
    original_pricenumber | nullThe price before a promotion. Must be greater than price; the menu shows it struck through. Pass null to end the promotion.min 0
    availablebooleanfalse shows the product as "Sin stock": visible but not orderable. Default true.
    hiddenbooleantrue keeps the product off the public menu entirely. Default false.
    variantsobject[]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*stringVariant name, e.g. "Tamaño" or "Adiciones"up to 50 characters
    translationsobjectThe 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.
    esobject
    namestringThe name in that languageup to 50 characters
    enobject
    namestringThe name in that languageup to 50 characters
    ptobject
    namestringThe name in that languageup to 50 characters
    frobject
    namestringThe name in that languageup to 50 characters
    deobject
    namestringThe name in that languageup to 50 characters
    itobject
    namestringThe name in that languageup to 50 characters
    min_selectionsintegerOptions the diner must pick. 0 makes the variant optional.min 0 · max 9007199254740991 · default 0
    max_selectionsintegerOptions the diner may pick. 0 is no limit; 1 renders a single choice.min 0 · max 9007199254740991 · default 0
    max_per_optionintegerHow many times one option may be repeated. Omit or 0 for no limit.min 0 · max 9007199254740991
    options*object[]
    name*stringOption name, e.g. "Grande"up to 50 characters
    translationsobjectThe 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.
    esobject
    enobject
    ptobject
    frobject
    deobject
    itobject
    pricenumberExtra charge for this option on top of the product price. 0 when it costs nothing.min 0 · default 0
    showbooleanfalse hides the option from diners without deleting itdefault true
    translationsobjectThe 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.
    esobject
    namestringThe product name in that languageup to 50 characters
    descriptionstringThe description in that languageup to 3000 characters
    enobject
    namestringThe product name in that languageup to 50 characters
    descriptionstringThe description in that languageup to 3000 characters
    ptobject
    namestringThe product name in that languageup to 50 characters
    descriptionstringThe description in that languageup to 3000 characters
    frobject
    namestringThe product name in that languageup to 50 characters
    descriptionstringThe description in that languageup to 3000 characters
    deobject
    namestringThe product name in that languageup to 50 characters
    descriptionstringThe description in that languageup to 3000 characters
    itobject
    namestringThe product name in that languageup to 50 characters
    descriptionstringThe 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

    FieldTypeDescription
    product*object
    id*string
    category_id*string
    name*string
    price*numberMajor units in the restaurant's currency
    original_pricenumberPresent while the product is on promotion
    descriptionstring
    available*booleanfalse renders "Sin stock"
    hidden*booleantrue keeps it off the public menu
    has_image*boolean
    image_urlstring
    variants*object[]
    id*string
    name*string
    min_selections*integermin -9007199254740991 · max 9007199254740991
    max_selections*integer0 is no limitmin -9007199254740991 · max 9007199254740991
    max_per_optionintegermin -9007199254740991 · max 9007199254740991
    options*object[]
    id*string
    name*string
    price*numberExtra charge on top of the product price
    show*boolean
    translationsobjectThe 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`.
    translationsobjectThe 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`.
    esobject
    enobject
    ptobject
    frobject
    deobject
    itobject
    translationsobjectName 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`.
    esobject
    namestring
    descriptionstring
    enobject
    namestring
    descriptionstring
    ptobject
    namestring
    descriptionstring
    frobject
    namestring
    descriptionstring
    deobject
    namestring
    descriptionstring
    itobject
    namestring
    descriptionstring

    Errors

    • 400 VALIDATION — a field is missing or malformed; `issues` names it
    • 401 UNAUTHORIZED — the key is missing, unknown or revoked
    • 402 NOT_PREMIUM — the restaurant has no active subscription or trial
    • 403 FORBIDDEN — the key lacks the scope this endpoint needs
    • 404 NOT_FOUND — no such restaurant in this account, or no such item in it
    • 500 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

    NameInDescription
    ref*pathThe restaurant identifier (the slug in its public URL, e.g. "pizzeria-roma") or its id, as returned by GET /restaurants

    Body (JSON)

    FieldTypeDescription
    updates*object[]Up to 50 products, each with only the fields to change
    product_id*stringup to 128 characters
    changes*object
    category_idstringMove the product to another categoryup to 128 characters
    namestringProduct nameup to 50 characters
    pricenumberPrice in the restaurant's currency, in major units (12500 for COP $12.000, 5.99 for USD $5.99). Never cents.min 0
    descriptionstringOptional description. Pass an empty string to remove it.up to 3000 characters
    original_pricenumber | nullThe price before a promotion. Must be greater than price; the menu shows it struck through. Pass null to end the promotion.min 0
    availablebooleanfalse shows the product as "Sin stock": visible but not orderable. Default true.
    hiddenbooleantrue keeps the product off the public menu entirely. Default false.
    variantsobject[]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*stringVariant name, e.g. "Tamaño" or "Adiciones"up to 50 characters
    translationsobjectThe 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_selectionsintegerOptions the diner must pick. 0 makes the variant optional.min 0 · max 9007199254740991 · default 0
    max_selectionsintegerOptions the diner may pick. 0 is no limit; 1 renders a single choice.min 0 · max 9007199254740991 · default 0
    max_per_optionintegerHow many times one option may be repeated. Omit or 0 for no limit.min 0 · max 9007199254740991
    options*object[]
    translationsobjectThe 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.
    esobject
    enobject
    ptobject
    frobject
    deobject
    itobject

    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

    FieldTypeDescription
    updated*integermin -9007199254740991 · max 9007199254740991

    Errors

    • 400 VALIDATION — a field is missing or malformed; `issues` names it
    • 401 UNAUTHORIZED — the key is missing, unknown or revoked
    • 402 NOT_PREMIUM — the restaurant has no active subscription or trial
    • 403 FORBIDDEN — the key lacks the scope this endpoint needs
    • 404 NOT_FOUND — no such restaurant in this account, or no such item in it
    • 500 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

    NameInDescription
    ref*pathThe restaurant identifier (the slug in its public URL, e.g. "pizzeria-roma") or its id, as returned by GET /restaurants
    id*pathThe 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

    FieldTypeDescription
    product*object
    id*string
    category_id*string
    name*string
    price*numberMajor units in the restaurant's currency
    original_pricenumberPresent while the product is on promotion
    descriptionstring
    available*booleanfalse renders "Sin stock"
    hidden*booleantrue keeps it off the public menu
    has_image*boolean
    image_urlstring
    variants*object[]
    id*string
    name*string
    min_selections*integermin -9007199254740991 · max 9007199254740991
    max_selections*integer0 is no limitmin -9007199254740991 · max 9007199254740991
    max_per_optionintegermin -9007199254740991 · max 9007199254740991
    options*object[]
    id*string
    name*string
    price*numberExtra charge on top of the product price
    show*boolean
    translationsobjectThe 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`.
    translationsobjectThe 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`.
    esobject
    enobject
    ptobject
    frobject
    deobject
    itobject
    translationsobjectName 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`.
    esobject
    namestring
    descriptionstring
    enobject
    namestring
    descriptionstring
    ptobject
    namestring
    descriptionstring
    frobject
    namestring
    descriptionstring
    deobject
    namestring
    descriptionstring
    itobject
    namestring
    descriptionstring

    Errors

    • 401 UNAUTHORIZED — the key is missing, unknown or revoked
    • 402 NOT_PREMIUM — the restaurant has no active subscription or trial
    • 403 FORBIDDEN — the key lacks the scope this endpoint needs
    • 404 NOT_FOUND — no such restaurant in this account, or no such item in it
    • 500 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

    NameInDescription
    ref*pathThe restaurant identifier (the slug in its public URL, e.g. "pizzeria-roma") or its id, as returned by GET /restaurants
    id*pathThe product id, from GET /restaurants/{ref}/products

    Body (JSON)

    FieldTypeDescription
    category_idstringMove the product to another categoryup to 128 characters
    namestringProduct nameup to 50 characters
    pricenumberPrice in the restaurant's currency, in major units (12500 for COP $12.000, 5.99 for USD $5.99). Never cents.min 0
    descriptionstringOptional description. Pass an empty string to remove it.up to 3000 characters
    original_pricenumber | nullThe price before a promotion. Must be greater than price; the menu shows it struck through. Pass null to end the promotion.min 0
    availablebooleanfalse shows the product as "Sin stock": visible but not orderable. Default true.
    hiddenbooleantrue keeps the product off the public menu entirely. Default false.
    variantsobject[]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*stringVariant name, e.g. "Tamaño" or "Adiciones"up to 50 characters
    translationsobjectThe 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.
    esobject
    namestringThe name in that languageup to 50 characters
    enobject
    namestringThe name in that languageup to 50 characters
    ptobject
    namestringThe name in that languageup to 50 characters
    frobject
    namestringThe name in that languageup to 50 characters
    deobject
    namestringThe name in that languageup to 50 characters
    itobject
    namestringThe name in that languageup to 50 characters
    min_selectionsintegerOptions the diner must pick. 0 makes the variant optional.min 0 · max 9007199254740991 · default 0
    max_selectionsintegerOptions the diner may pick. 0 is no limit; 1 renders a single choice.min 0 · max 9007199254740991 · default 0
    max_per_optionintegerHow many times one option may be repeated. Omit or 0 for no limit.min 0 · max 9007199254740991
    options*object[]
    name*stringOption name, e.g. "Grande"up to 50 characters
    translationsobjectThe 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.
    esobject
    enobject
    ptobject
    frobject
    deobject
    itobject
    pricenumberExtra charge for this option on top of the product price. 0 when it costs nothing.min 0 · default 0
    showbooleanfalse hides the option from diners without deleting itdefault true
    translationsobjectThe 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.
    esobject
    namestringThe product name in that languageup to 50 characters
    descriptionstringThe description in that languageup to 3000 characters
    enobject
    namestringThe product name in that languageup to 50 characters
    descriptionstringThe description in that languageup to 3000 characters
    ptobject
    namestringThe product name in that languageup to 50 characters
    descriptionstringThe description in that languageup to 3000 characters
    frobject
    namestringThe product name in that languageup to 50 characters
    descriptionstringThe description in that languageup to 3000 characters
    deobject
    namestringThe product name in that languageup to 50 characters
    descriptionstringThe description in that languageup to 3000 characters
    itobject
    namestringThe product name in that languageup to 50 characters
    descriptionstringThe 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

    FieldTypeDescription
    product*object
    id*string
    category_id*string
    name*string
    price*numberMajor units in the restaurant's currency
    original_pricenumberPresent while the product is on promotion
    descriptionstring
    available*booleanfalse renders "Sin stock"
    hidden*booleantrue keeps it off the public menu
    has_image*boolean
    image_urlstring
    variants*object[]
    id*string
    name*string
    min_selections*integermin -9007199254740991 · max 9007199254740991
    max_selections*integer0 is no limitmin -9007199254740991 · max 9007199254740991
    max_per_optionintegermin -9007199254740991 · max 9007199254740991
    options*object[]
    id*string
    name*string
    price*numberExtra charge on top of the product price
    show*boolean
    translationsobjectThe 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`.
    translationsobjectThe 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`.
    esobject
    enobject
    ptobject
    frobject
    deobject
    itobject
    translationsobjectName 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`.
    esobject
    namestring
    descriptionstring
    enobject
    namestring
    descriptionstring
    ptobject
    namestring
    descriptionstring
    frobject
    namestring
    descriptionstring
    deobject
    namestring
    descriptionstring
    itobject
    namestring
    descriptionstring

    Errors

    • 400 VALIDATION — a field is missing or malformed; `issues` names it
    • 401 UNAUTHORIZED — the key is missing, unknown or revoked
    • 402 NOT_PREMIUM — the restaurant has no active subscription or trial
    • 403 FORBIDDEN — the key lacks the scope this endpoint needs
    • 404 NOT_FOUND — no such restaurant in this account, or no such item in it
    • 500 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

    NameInDescription
    ref*pathThe restaurant identifier (the slug in its public URL, e.g. "pizzeria-roma") or its id, as returned by GET /restaurants
    id*pathThe 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

    FieldTypeDescription
    deleted*string

    Errors

    • 401 UNAUTHORIZED — the key is missing, unknown or revoked
    • 402 NOT_PREMIUM — the restaurant has no active subscription or trial
    • 403 FORBIDDEN — the key lacks the scope this endpoint needs
    • 404 NOT_FOUND — no such restaurant in this account, or no such item in it
    • 500 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

    NameInDescription
    ref*pathThe 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)

    FieldTypeDescription
    image_url*stringPublic 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

    FieldTypeDescription
    restaurant*object
    id*string
    identifier*string
    name*string
    currency*stringISO 4217 code every price is in
    type*"whatsapp" | "read_only""whatsapp" takes orders by WhatsApp; "read_only" only shows the menu
    phone*string | nullWhatsApp 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 it
    • 401 UNAUTHORIZED — the key is missing, unknown or revoked
    • 402 NOT_PREMIUM — the restaurant has no active subscription or trial
    • 403 FORBIDDEN — the key lacks the scope this endpoint needs
    • 404 NOT_FOUND — no such restaurant in this account, or no such item in it
    • 500 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

    NameInDescription
    ref*pathThe 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

    FieldTypeDescription
    restaurant*object
    id*string
    identifier*string
    name*string
    currency*stringISO 4217 code every price is in
    type*"whatsapp" | "read_only""whatsapp" takes orders by WhatsApp; "read_only" only shows the menu
    phone*string | nullWhatsApp 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 revoked
    • 402 NOT_PREMIUM — the restaurant has no active subscription or trial
    • 403 FORBIDDEN — the key lacks the scope this endpoint needs
    • 404 NOT_FOUND — no such restaurant in this account, or no such item in it
    • 500 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

    NameInDescription
    ref*pathThe restaurant identifier (the slug in its public URL, e.g. "pizzeria-roma") or its id, as returned by GET /restaurants
    id*pathThe product id, from GET /restaurants/{ref}/products

    Body (JSON)

    FieldTypeDescription
    image_url*stringPublic 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

    FieldTypeDescription
    product*object
    id*string
    category_id*string
    name*string
    price*numberMajor units in the restaurant's currency
    original_pricenumberPresent while the product is on promotion
    descriptionstring
    available*booleanfalse renders "Sin stock"
    hidden*booleantrue keeps it off the public menu
    has_image*boolean
    image_urlstring
    variants*object[]
    id*string
    name*string
    min_selections*integermin -9007199254740991 · max 9007199254740991
    max_selections*integer0 is no limitmin -9007199254740991 · max 9007199254740991
    max_per_optionintegermin -9007199254740991 · max 9007199254740991
    options*object[]
    id*string
    name*string
    price*numberExtra charge on top of the product price
    show*boolean
    translationsobjectThe 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`.
    translationsobjectThe 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`.
    esobject
    enobject
    ptobject
    frobject
    deobject
    itobject
    translationsobjectName 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`.
    esobject
    namestring
    descriptionstring
    enobject
    namestring
    descriptionstring
    ptobject
    namestring
    descriptionstring
    frobject
    namestring
    descriptionstring
    deobject
    namestring
    descriptionstring
    itobject
    namestring
    descriptionstring

    Errors

    • 400 VALIDATION — a field is missing or malformed; `issues` names it
    • 401 UNAUTHORIZED — the key is missing, unknown or revoked
    • 402 NOT_PREMIUM — the restaurant has no active subscription or trial
    • 403 FORBIDDEN — the key lacks the scope this endpoint needs
    • 404 NOT_FOUND — no such restaurant in this account, or no such item in it
    • 500 INTERNAL — something failed on our side; retry in a moment
    delete/restaurants/{ref}/products/{id}/imagerequires menu:writeremoveProductImage

    Remove the product photo

    Parameters

    NameInDescription
    ref*pathThe restaurant identifier (the slug in its public URL, e.g. "pizzeria-roma") or its id, as returned by GET /restaurants
    id*pathThe 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

    FieldTypeDescription
    product_id*string
    has_image*false

    Errors

    • 401 UNAUTHORIZED — the key is missing, unknown or revoked
    • 402 NOT_PREMIUM — the restaurant has no active subscription or trial
    • 403 FORBIDDEN — the key lacks the scope this endpoint needs
    • 404 NOT_FOUND — no such restaurant in this account, or no such item in it
    • 500 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

    NameInDescription
    ref*pathThe 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

    FieldTypeDescription
    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 | nullHTTP status of the last attempt; null if it never answeredmin -9007199254740991 · max 9007199254740991

    Errors

    • 401 UNAUTHORIZED — the key is missing, unknown or revoked
    • 402 NOT_PREMIUM — the restaurant has no active subscription or trial
    • 403 FORBIDDEN — the key lacks the scope this endpoint needs
    • 404 NOT_FOUND — no such restaurant in this account, or no such item in it
    • 500 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

    NameInDescription
    ref*pathThe restaurant identifier (the slug in its public URL, e.g. "pizzeria-roma") or its id, as returned by GET /restaurants

    Body (JSON)

    FieldTypeDescription
    url*stringWhere 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

    FieldTypeDescription
    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 | nullHTTP status of the last attempt; null if it never answeredmin -9007199254740991 · max 9007199254740991
    secret*stringThe signing secret. Shown only in this response; store it now

    Errors

    • 400 VALIDATION — a field is missing or malformed; `issues` names it
    • 401 UNAUTHORIZED — the key is missing, unknown or revoked
    • 402 NOT_PREMIUM — the restaurant has no active subscription or trial
    • 403 FORBIDDEN — the key lacks the scope this endpoint needs
    • 404 NOT_FOUND — no such restaurant in this account, or no such item in it
    • 409 CONFLICT — this restaurant already has a webhook with that URL
    • 500 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

    NameInDescription
    ref*pathThe restaurant identifier (the slug in its public URL, e.g. "pizzeria-roma") or its id, as returned by GET /restaurants
    id*pathThe webhook id, from GET /restaurants/{ref}/webhooks

    Body (JSON)

    FieldTypeDescription
    urlstringWhere 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
    enabledbooleanfalse 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

    FieldTypeDescription
    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 | nullHTTP status of the last attempt; null if it never answeredmin -9007199254740991 · max 9007199254740991

    Errors

    • 400 VALIDATION — a field is missing or malformed; `issues` names it
    • 401 UNAUTHORIZED — the key is missing, unknown or revoked
    • 402 NOT_PREMIUM — the restaurant has no active subscription or trial
    • 403 FORBIDDEN — the key lacks the scope this endpoint needs
    • 404 NOT_FOUND — no such restaurant in this account, or no such item in it
    • 409 CONFLICT — this restaurant already has a webhook with that URL
    • 500 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

    NameInDescription
    ref*pathThe restaurant identifier (the slug in its public URL, e.g. "pizzeria-roma") or its id, as returned by GET /restaurants
    id*pathThe 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

    FieldTypeDescription
    deleted*string

    Errors

    • 401 UNAUTHORIZED — the key is missing, unknown or revoked
    • 402 NOT_PREMIUM — the restaurant has no active subscription or trial
    • 403 FORBIDDEN — the key lacks the scope this endpoint needs
    • 404 NOT_FOUND — no such restaurant in this account, or no such item in it
    • 500 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

    NameInDescription
    ref*pathThe restaurant identifier (the slug in its public URL, e.g. "pizzeria-roma") or its id, as returned by GET /restaurants
    id*pathThe 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

    FieldTypeDescription
    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 | nullHTTP status of the last attempt; null if it never answeredmin -9007199254740991 · max 9007199254740991
    secret*stringThe signing secret. Shown only in this response; store it now

    Errors

    • 401 UNAUTHORIZED — the key is missing, unknown or revoked
    • 402 NOT_PREMIUM — the restaurant has no active subscription or trial
    • 403 FORBIDDEN — the key lacks the scope this endpoint needs
    • 404 NOT_FOUND — no such restaurant in this account, or no such item in it
    • 500 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

    NameInDescription
    ref*pathThe restaurant identifier (the slug in its public URL, e.g. "pizzeria-roma") or its id, as returned by GET /restaurants
    id*pathThe 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

    FieldTypeDescription
    delivery*object
    id*string
    event_id*string
    type*string
    attempt*integer1 for the first try, then each retrymin -9007199254740991 · max 9007199254740991
    ok*boolean
    http_status*integer | nullmin -9007199254740991 · max 9007199254740991
    duration_ms*integermin -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 revoked
    • 402 NOT_PREMIUM — the restaurant has no active subscription or trial
    • 403 FORBIDDEN — the key lacks the scope this endpoint needs
    • 404 NOT_FOUND — no such restaurant in this account, or no such item in it
    • 500 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

    NameInDescription
    ref*pathThe restaurant identifier (the slug in its public URL, e.g. "pizzeria-roma") or its id, as returned by GET /restaurants
    id*pathThe 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

    FieldTypeDescription
    deliveries*object[]
    id*string
    event_id*string
    type*string
    attempt*integer1 for the first try, then each retrymin -9007199254740991 · max 9007199254740991
    ok*boolean
    http_status*integer | nullmin -9007199254740991 · max 9007199254740991
    duration_ms*integermin -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 revoked
    • 402 NOT_PREMIUM — the restaurant has no active subscription or trial
    • 403 FORBIDDEN — the key lacks the scope this endpoint needs
    • 404 NOT_FOUND — no such restaurant in this account, or no such item in it
    • 500 INTERNAL — something failed on our side; retry in a moment

    Built by weeeb 🧡

    Delimenu

    • How does it work?
    • Pricing
    • Frequently asked questions
    • Platform status
    • Help center
    • Developers
    • Ideas and feedback
    • Roadmap
    • Changelog

    Legal

    • Terms and conditions
    • Privacy
    • Cookies

    Follow us

    Follow us on social media and keep up with all our news.

    Copyright © 2026 Weeeb LLC. All rights reserved.

    EspañolEnglishPortuguês