# Delimenu API

> Gestiona un menú de Delimenu desde tu propio sistema, o muestra la carta de un restaurante en un frontend hecho a tu medida. Referencia completa de la API REST v1, generada del mismo OpenAPI que sirve `https://delimenu.co/api/v1/openapi.json`. Versión HTML: https://delimenu.co/developers.

## Resumen para agentes

- URL base: `https://delimenu.co/api/v1`. Respuestas JSON en `snake_case`.
- Autenticación: cabecera `Authorization: Bearer dmk_…`. El dueño del restaurante crea la clave en el panel de Delimenu, en **Configuración › API para desarrolladores**; se muestra una sola vez. Una clave cubre todos los restaurantes de su cuenta; hasta 10 claves activas por cuenta.
- Permisos por clave: `menu:read` (leer) y `menu:write` (editar). Una clave de solo lectura recibe `403` en cualquier escritura.
- El menú público de cualquier restaurante se lee sin clave: `GET https://delimenu.co/api/v1/menus/{identifier}`, donde `identifier` es el mismo de `https://delimenu.co/{identifier}`. CORS abierto, cacheado en el borde, se refresca con cada cambio.
- **Los precios son unidades enteras de la moneda del restaurante, nunca centavos**: `12500` son $ 12.000 pesos colombianos, `5.99` son US$ 5,99. La moneda viene en `restaurant.currency` (ISO 4217).
- Los cambios se aplican al menú público al instante. No hay borrador ni "publicar". Confirma con el dueño antes de borrar algo.
- Flujo típico: `GET /restaurants` para obtener el identificador, `GET /restaurants/{ref}/menu` para ver categorías y productos con sus ids, y luego las escrituras.
- Un restaurante sin suscripción ni prueba activa responde `402` en los endpoints con clave y `403` en el menú público.
- Editar muchos productos a la vez: `PATCH /restaurants/{ref}/products`, hasta 50 por llamada.
- Las imágenes se envían como URL https pública (JPG, PNG o WEBP, hasta 5 MB); la llamada tarda unos segundos mientras se optimiza.
- Si lo que quieres es que un agente gestione el menú conversando, el servidor MCP en `https://delimenu.co/api/mcp` expone las mismas operaciones con OAuth.

## Errores

Todo error responde `{ "error", "message" }`: `error` es un código estable en inglés para decidir en código; `message` es un texto en español que se puede mostrar al dueño tal cual. Las validaciones añaden `issues: [{ path, message }]`. Los `401` y `403` incluyen `docs` con la URL de esta documentación.

| HTTP | error | Cuándo |
|---|---|---|
| 400 | `VALIDATION` | Falta un campo o tiene un formato inválido; `issues` dice cuál. |
| 401 | `UNAUTHORIZED` | La clave falta, no existe o fue revocada. |
| 402 | `NOT_PREMIUM` | El restaurante no tiene suscripción ni prueba activa. |
| 403 | `FORBIDDEN` | La clave no tiene el permiso que pide el endpoint. |
| 404 | `NOT_FOUND` | El restaurante no está en tu cuenta, o el producto o la categoría no existen en él. |
| 409 | `CONFLICT` | Nombre de categoría repetido, identificador ocupado o categoría con productos. |
| 422 | `LIMIT` | Se alcanzó un tope. |
| 500 | `INTERNAL` | Falló algo de nuestro lado. Reintenta en un momento. |

## Referencia

Los campos marcados con `*` son obligatorios. Las claves de los ejemplos son ficticias.

## Public menu

No key needed. What delimenu.co/{identifier} shows.

### GET /menus/{identifier}

**Public menu of a restaurant** · `operationId: getPublicMenu` · público, sin clave

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. Cached at the edge and refreshed on every change the owner makes; a differently-cased identifier is redirected to the lowercase one.

**Parámetros**

| Nombre | Dónde | Descripción |
|---|---|---|
| `identifier` * | ruta | The slug in the public URL: delimenu.co/{identifier} |

**Ejemplo**

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

**Respuesta 200**

```json
{
  "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
  },
  "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
                }
              ]
            }
          ]
        }
      ]
    }
  ]
}
```

**Errores**

- `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 /restaurants

**List the restaurants of the account** · `operationId: listRestaurants` · requiere `menu:read`

Call this first; every other endpoint takes one of these by identifier or id.

**Ejemplo**

```bash
curl -X GET "https://delimenu.co/api/v1/restaurants" \
  -H "Authorization: Bearer dmk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
```

**Respuesta 200**

```json
{
  "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,
      "trial_active": false
    }
  ]
}
```

**Errores**

- `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}

**Get a restaurant** · `operationId: getRestaurant` · requiere `menu:read`

**Parámetros**

| Nombre | Dónde | Descripción |
|---|---|---|
| `ref` * | ruta | The restaurant identifier (the slug in its public URL, e.g. "pizzeria-roma") or its id, as returned by GET /restaurants |

**Ejemplo**

```bash
curl -X GET "https://delimenu.co/api/v1/restaurants/pizzeria-roma" \
  -H "Authorization: Bearer dmk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
```

**Respuesta 200**

```json
{
  "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,
    "trial_active": false
  }
}
```

**Errores**

- `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}

**Update a restaurant** · `operationId: updateRestaurant` · requiere `menu:write`

Changes only the fields given: name, WhatsApp phone, address, currency or type. Changing the currency converts no prices. The identifier has its own endpoint.

**Parámetros**

| Nombre | Dónde | Descripción |
|---|---|---|
| `ref` * | ruta | The restaurant identifier (the slug in its public URL, e.g. "pizzeria-roma") or its id, as returned by GET /restaurants |

**Cuerpo (JSON)**

| Campo | Tipo | Descripción |
|---|---|---|
| `name` | `string` | hasta 50 caracteres |
| `phone` | `string` | WhatsApp number that receives orders, in E.164 format (+573001234567) |
| `address` | `string` | Street address shown on the menu. Empty string removes it. — hasta 65 caracteres |
| `currency` | `string` | ISO 4217 code every price is shown in. Changing it converts nothing. |
| `type` | `"whatsapp" \| "read_only"` | "whatsapp" takes orders by WhatsApp; "read_only" only shows the menu |

**Ejemplo**

```bash
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á"}'
```

**Respuesta 200**

```json
{
  "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,
    "trial_active": false
  }
}
```

**Errores**

- `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}/identifier

**Change the public URL** · `operationId: updateRestaurantIdentifier` · requiere `menu:write`

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.

**Parámetros**

| Nombre | Dónde | Descripción |
|---|---|---|
| `ref` * | ruta | The restaurant identifier (the slug in its public URL, e.g. "pizzeria-roma") or its id, as returned by GET /restaurants |

**Cuerpo (JSON)**

| Campo | Tipo | Descripción |
|---|---|---|
| `identifier` * | `string` | The new public slug: lowercase letters, digits, dots, hyphens and underscores, e.g. "pizzeria-roma" — hasta 50 caracteres |

**Ejemplo**

```bash
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"}'
```

**Respuesta 200**

```json
{
  "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,
    "trial_active": false
  }
}
```

**Errores**

- `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}/menu

**Get the whole menu** · `operationId: getMenu` · requiere `menu:read`

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.

**Parámetros**

| Nombre | Dónde | Descripción |
|---|---|---|
| `ref` * | ruta | The restaurant identifier (the slug in its public URL, e.g. "pizzeria-roma") or its id, as returned by GET /restaurants |
| `include_hidden` | query | Also return the products hidden from the public menu |

**Ejemplo**

```bash
curl -X GET "https://delimenu.co/api/v1/restaurants/pizzeria-roma/menu?include_hidden=true" \
  -H "Authorization: Bearer dmk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
```

**Respuesta 200**

```json
{
  "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,
    "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."
}
```

**Errores**

- `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}/categories

**List the categories** · `operationId: listCategories` · requiere `menu:read`

**Parámetros**

| Nombre | Dónde | Descripción |
|---|---|---|
| `ref` * | ruta | The restaurant identifier (the slug in its public URL, e.g. "pizzeria-roma") or its id, as returned by GET /restaurants |

**Ejemplo**

```bash
curl -X GET "https://delimenu.co/api/v1/restaurants/pizzeria-roma/categories" \
  -H "Authorization: Bearer dmk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
```

**Respuesta 200**

```json
{
  "categories": [
    {
      "id": "k3Qm8vXb2LpN7wRt1YaZ",
      "name": "Pizzas",
      "position": 1
    },
    {
      "id": "c7Wd2Bn5Kq9Xr4Ms8Lt1",
      "name": "Bebidas",
      "position": 2
    }
  ]
}
```

**Errores**

- `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}/categories

**Create a category** · `operationId: createCategory` · requiere `menu:write`

Adds a category at the end of the menu.

**Parámetros**

| Nombre | Dónde | Descripción |
|---|---|---|
| `ref` * | ruta | The restaurant identifier (the slug in its public URL, e.g. "pizzeria-roma") or its id, as returned by GET /restaurants |

**Cuerpo (JSON)**

| Campo | Tipo | Descripción |
|---|---|---|
| `name` * | `string` | Category name — hasta 50 caracteres |

**Ejemplo**

```bash
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"}'
```

**Respuesta 201**

```json
{
  "category": {
    "id": "d4Fg6Hj8Kl0Zx2Cv4Bn6",
    "name": "Postres"
  }
}
```

**Errores**

- `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-order

**Reorder the categories** · `operationId: reorderCategories` · requiere `menu:write`

Sets the display order. Pass every category id exactly once, in the new order.

**Parámetros**

| Nombre | Dónde | Descripción |
|---|---|---|
| `ref` * | ruta | The restaurant identifier (the slug in its public URL, e.g. "pizzeria-roma") or its id, as returned by GET /restaurants |

**Cuerpo (JSON)**

| Campo | Tipo | Descripción |
|---|---|---|
| `category_ids` * | `string[]` | Every category id exactly once, in the new display order |

**Ejemplo**

```bash
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"]}'
```

**Respuesta 200**

```json
{
  "order_categories": [
    "c7Wd2Bn5Kq9Xr4Ms8Lt1",
    "k3Qm8vXb2LpN7wRt1YaZ"
  ]
}
```

**Errores**

- `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}

**Rename a category** · `operationId: updateCategory` · requiere `menu:write`

**Parámetros**

| Nombre | Dónde | Descripción |
|---|---|---|
| `ref` * | ruta | The restaurant identifier (the slug in its public URL, e.g. "pizzeria-roma") or its id, as returned by GET /restaurants |
| `id` * | ruta | The category id, from GET /restaurants/{ref}/categories |

**Cuerpo (JSON)**

| Campo | Tipo | Descripción |
|---|---|---|
| `name` * | `string` | Category name — hasta 50 caracteres |

**Ejemplo**

```bash
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"}'
```

**Respuesta 200**

```json
{
  "category": {
    "id": "k3Qm8vXb2LpN7wRt1YaZ",
    "name": "Pizzas artesanales"
  }
}
```

**Errores**

- `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}

**Delete a category** · `operationId: deleteCategory` · requiere `menu:write`

Refused with 409 while the category still has products, unless delete_products is true, in which case the products are deleted too.

**Parámetros**

| Nombre | Dónde | Descripción |
|---|---|---|
| `ref` * | ruta | The restaurant identifier (the slug in its public URL, e.g. "pizzeria-roma") or its id, as returned by GET /restaurants |
| `id` * | ruta | The category id, from GET /restaurants/{ref}/categories |
| `delete_products` | query | Delete the products in the category too. Without it a non-empty category is refused with 409. |

**Ejemplo**

```bash
curl -X DELETE "https://delimenu.co/api/v1/restaurants/pizzeria-roma/categories/k3Qm8vXb2LpN7wRt1YaZ?delete_products=true" \
  -H "Authorization: Bearer dmk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
```

**Respuesta 200**

```json
{
  "deleted": "k3Qm8vXb2LpN7wRt1YaZ",
  "deleted_products": 3
}
```

**Errores**

- `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-order

**Reorder the products of a category** · `operationId: reorderProducts` · requiere `menu:write`

Pass every product id of that category exactly once, in the new order.

**Parámetros**

| Nombre | Dónde | Descripción |
|---|---|---|
| `ref` * | ruta | The restaurant identifier (the slug in its public URL, e.g. "pizzeria-roma") or its id, as returned by GET /restaurants |
| `id` * | ruta | The category id, from GET /restaurants/{ref}/categories |

**Cuerpo (JSON)**

| Campo | Tipo | Descripción |
|---|---|---|
| `product_ids` * | `string[]` | Every product id of the category exactly once, in the new display order |

**Ejemplo**

```bash
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"]}'
```

**Respuesta 200**

```json
{
  "category_id": "k3Qm8vXb2LpN7wRt1YaZ",
  "order_products": [
    "q2Jk5Lm8Np1Qr4St7Uv0",
    "p9Hs4TqL2mNc8VbX6Rdy"
  ]
}
```

**Errores**

- `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}/products

**List or search the products** · `operationId: listProducts` · requiere `menu:read`

Every product, hidden ones included and flagged. With q, only those whose name or description contains the text.

**Parámetros**

| Nombre | Dónde | Descripción |
|---|---|---|
| `ref` * | ruta | The restaurant identifier (the slug in its public URL, e.g. "pizzeria-roma") or its id, as returned by GET /restaurants |
| `q` | query | Only products whose name or description contains this text (accent- and case-insensitive) — hasta 100 caracteres |

**Ejemplo**

```bash
curl -X GET "https://delimenu.co/api/v1/restaurants/pizzeria-roma/products?q=margarita" \
  -H "Authorization: Bearer dmk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
```

**Respuesta 200**

```json
{
  "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
}
```

**Errores**

- `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}/products

**Create a product** · `operationId: createProduct` · requiere `menu:write`

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.

**Parámetros**

| Nombre | Dónde | Descripción |
|---|---|---|
| `ref` * | ruta | The restaurant identifier (the slug in its public URL, e.g. "pizzeria-roma") or its id, as returned by GET /restaurants |

**Cuerpo (JSON)**

| Campo | Tipo | Descripción |
|---|---|---|
| `category_id` * | `string` | The category the product belongs to (from get_menu) — hasta 128 caracteres |
| `name` * | `string` | Product name — hasta 50 caracteres |
| `price` * | `number` | Price in the restaurant's currency, in major units (12500 for COP $12.000, 5.99 for USD $5.99). Never cents. — min 0 |
| `description` | `string` | Optional description. Pass an empty string to remove it. — hasta 3000 caracteres |
| `original_price` | `number \| null` | The price before a promotion. Must be greater than price; the menu shows it struck through. Pass null to end the promotion. — min 0 |
| `available` | `boolean` | false shows the product as "Sin stock": visible but not orderable. Default true. |
| `hidden` | `boolean` | true keeps the product off the public menu entirely. Default false. |
| `variants` | `object[]` | Choices the diner makes (sizes, extras). Replaces the whole list when provided. |
|   `name` * | `string` | Variant name, e.g. "Tamaño" or "Adiciones" — hasta 50 caracteres |
|   `min_selections` | `integer` | Options the diner must pick. 0 makes the variant optional. — min 0 · max 9007199254740991 · por defecto 0 |
|   `max_selections` | `integer` | Options the diner may pick. 0 is no limit; 1 renders a single choice. — min 0 · max 9007199254740991 · por defecto 0 |
|   `max_per_option` | `integer` | How many times one option may be repeated. Omit or 0 for no limit. — min 0 · max 9007199254740991 |
|   `options` * | `object[]` |  |
|     `name` * | `string` | Option name, e.g. "Grande" — hasta 50 caracteres |
|     `price` | `number` | Extra charge for this option on top of the product price. 0 when it costs nothing. — min 0 · por defecto 0 |
|     `show` | `boolean` | false hides the option from diners without deleting it — por defecto true |

**Ejemplo**

```bash
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}]}]}'
```

**Respuesta 201**

```json
{
  "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
          }
        ]
      }
    ]
  }
}
```

**Errores**

- `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}/products

**Update many products** · `operationId: bulkUpdateProducts` · requiere `menu:write`

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.

**Parámetros**

| Nombre | Dónde | Descripción |
|---|---|---|
| `ref` * | ruta | The restaurant identifier (the slug in its public URL, e.g. "pizzeria-roma") or its id, as returned by GET /restaurants |

**Cuerpo (JSON)**

| Campo | Tipo | Descripción |
|---|---|---|
| `updates` * | `object[]` | Up to 50 products, each with only the fields to change |
|   `product_id` * | `string` | hasta 128 caracteres |
|   `changes` * | `object` |  |
|     `category_id` | `string` | Move the product to another category — hasta 128 caracteres |
|     `name` | `string` | Product name — hasta 50 caracteres |
|     `price` | `number` | Price in the restaurant's currency, in major units (12500 for COP $12.000, 5.99 for USD $5.99). Never cents. — min 0 |
|     `description` | `string` | Optional description. Pass an empty string to remove it. — hasta 3000 caracteres |
|     `original_price` | `number \| null` | The price before a promotion. Must be greater than price; the menu shows it struck through. Pass null to end the promotion. — min 0 |
|     `available` | `boolean` | false shows the product as "Sin stock": visible but not orderable. Default true. |
|     `hidden` | `boolean` | true keeps the product off the public menu entirely. Default false. |
|     `variants` | `object[]` | Choices the diner makes (sizes, extras). Replaces the whole list when provided. |
|       `name` * | `string` | Variant name, e.g. "Tamaño" or "Adiciones" — hasta 50 caracteres |
|       `min_selections` | `integer` | Options the diner must pick. 0 makes the variant optional. — min 0 · max 9007199254740991 · por defecto 0 |
|       `max_selections` | `integer` | Options the diner may pick. 0 is no limit; 1 renders a single choice. — min 0 · max 9007199254740991 · por defecto 0 |
|       `max_per_option` | `integer` | How many times one option may be repeated. Omit or 0 for no limit. — min 0 · max 9007199254740991 |
|       `options` * | `object[]` |  |

**Ejemplo**

```bash
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}}]}'
```

**Respuesta 200**

```json
{
  "updated": 2
}
```

**Errores**

- `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}

**Get a product** · `operationId: getProduct` · requiere `menu:read`

**Parámetros**

| Nombre | Dónde | Descripción |
|---|---|---|
| `ref` * | ruta | The restaurant identifier (the slug in its public URL, e.g. "pizzeria-roma") or its id, as returned by GET /restaurants |
| `id` * | ruta | The product id, from GET /restaurants/{ref}/products |

**Ejemplo**

```bash
curl -X GET "https://delimenu.co/api/v1/restaurants/pizzeria-roma/products/p9Hs4TqL2mNc8VbX6Rdy" \
  -H "Authorization: Bearer dmk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
```

**Respuesta 200**

```json
{
  "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
          }
        ]
      }
    ]
  }
}
```

**Errores**

- `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}

**Update a product** · `operationId: updateProduct` · requiere `menu:write`

Changes only the fields given: name, price, description, original_price (promotion), available, hidden, variants (replaces all) or category_id (moves it). Raising price above an existing original_price ends the promotion.

**Parámetros**

| Nombre | Dónde | Descripción |
|---|---|---|
| `ref` * | ruta | The restaurant identifier (the slug in its public URL, e.g. "pizzeria-roma") or its id, as returned by GET /restaurants |
| `id` * | ruta | The product id, from GET /restaurants/{ref}/products |

**Cuerpo (JSON)**

| Campo | Tipo | Descripción |
|---|---|---|
| `category_id` | `string` | Move the product to another category — hasta 128 caracteres |
| `name` | `string` | Product name — hasta 50 caracteres |
| `price` | `number` | Price in the restaurant's currency, in major units (12500 for COP $12.000, 5.99 for USD $5.99). Never cents. — min 0 |
| `description` | `string` | Optional description. Pass an empty string to remove it. — hasta 3000 caracteres |
| `original_price` | `number \| null` | The price before a promotion. Must be greater than price; the menu shows it struck through. Pass null to end the promotion. — min 0 |
| `available` | `boolean` | false shows the product as "Sin stock": visible but not orderable. Default true. |
| `hidden` | `boolean` | true keeps the product off the public menu entirely. Default false. |
| `variants` | `object[]` | Choices the diner makes (sizes, extras). Replaces the whole list when provided. |
|   `name` * | `string` | Variant name, e.g. "Tamaño" or "Adiciones" — hasta 50 caracteres |
|   `min_selections` | `integer` | Options the diner must pick. 0 makes the variant optional. — min 0 · max 9007199254740991 · por defecto 0 |
|   `max_selections` | `integer` | Options the diner may pick. 0 is no limit; 1 renders a single choice. — min 0 · max 9007199254740991 · por defecto 0 |
|   `max_per_option` | `integer` | How many times one option may be repeated. Omit or 0 for no limit. — min 0 · max 9007199254740991 |
|   `options` * | `object[]` |  |
|     `name` * | `string` | Option name, e.g. "Grande" — hasta 50 caracteres |
|     `price` | `number` | Extra charge for this option on top of the product price. 0 when it costs nothing. — min 0 · por defecto 0 |
|     `show` | `boolean` | false hides the option from diners without deleting it — por defecto true |

**Ejemplo**

```bash
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,"available":true}'
```

**Respuesta 200**

```json
{
  "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
          }
        ]
      }
    ]
  }
}
```

**Errores**

- `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}

**Delete a product** · `operationId: deleteProduct` · requiere `menu:write`

Permanent, photo included. Prefer hidden: true when the owner may want it back.

**Parámetros**

| Nombre | Dónde | Descripción |
|---|---|---|
| `ref` * | ruta | The restaurant identifier (the slug in its public URL, e.g. "pizzeria-roma") or its id, as returned by GET /restaurants |
| `id` * | ruta | The product id, from GET /restaurants/{ref}/products |

**Ejemplo**

```bash
curl -X DELETE "https://delimenu.co/api/v1/restaurants/pizzeria-roma/products/p9Hs4TqL2mNc8VbX6Rdy" \
  -H "Authorization: Bearer dmk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
```

**Respuesta 200**

```json
{
  "deleted": "p9Hs4TqL2mNc8VbX6Rdy"
}
```

**Errores**

- `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}

**Set the logo or the banner** · `operationId: setRestaurantImage` · requiere `menu:write`

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.

**Parámetros**

| Nombre | Dónde | Descripción |
|---|---|---|
| `ref` * | ruta | The restaurant identifier (the slug in its public URL, e.g. "pizzeria-roma") or its id, as returned by GET /restaurants |
| `kind` * | ruta | "logo" is shown at 512×512; "banner" is the header image, 1080×1080 max — valores: "logo", "banner" |

**Cuerpo (JSON)**

| Campo | Tipo | Descripción |
|---|---|---|
| `image_url` * | `string` | Public https URL of a JPG, PNG or WEBP image up to 5 MB — hasta 2048 caracteres |

**Ejemplo**

```bash
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"}'
```

**Respuesta 200**

```json
{
  "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,
    "trial_active": false
  }
}
```

**Errores**

- `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}

**Remove the logo or the banner** · `operationId: removeRestaurantImage` · requiere `menu:write`

**Parámetros**

| Nombre | Dónde | Descripción |
|---|---|---|
| `ref` * | ruta | The restaurant identifier (the slug in its public URL, e.g. "pizzeria-roma") or its id, as returned by GET /restaurants |
| `kind` * | ruta | "logo" is shown at 512×512; "banner" is the header image, 1080×1080 max — valores: "logo", "banner" |

**Ejemplo**

```bash
curl -X DELETE "https://delimenu.co/api/v1/restaurants/pizzeria-roma/images/logo" \
  -H "Authorization: Bearer dmk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
```

**Respuesta 200**

```json
{
  "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,
    "trial_active": false
  }
}
```

**Errores**

- `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}/image

**Set the product photo** · `operationId: setProductImage` · requiere `menu:write`

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.

**Parámetros**

| Nombre | Dónde | Descripción |
|---|---|---|
| `ref` * | ruta | The restaurant identifier (the slug in its public URL, e.g. "pizzeria-roma") or its id, as returned by GET /restaurants |
| `id` * | ruta | The product id, from GET /restaurants/{ref}/products |

**Cuerpo (JSON)**

| Campo | Tipo | Descripción |
|---|---|---|
| `image_url` * | `string` | Public https URL of a JPG, PNG or WEBP image up to 5 MB — hasta 2048 caracteres |

**Ejemplo**

```bash
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"}'
```

**Respuesta 200**

```json
{
  "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
          }
        ]
      }
    ]
  }
}
```

**Errores**

- `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}/image

**Remove the product photo** · `operationId: removeProductImage` · requiere `menu:write`

**Parámetros**

| Nombre | Dónde | Descripción |
|---|---|---|
| `ref` * | ruta | The restaurant identifier (the slug in its public URL, e.g. "pizzeria-roma") or its id, as returned by GET /restaurants |
| `id` * | ruta | The product id, from GET /restaurants/{ref}/products |

**Ejemplo**

```bash
curl -X DELETE "https://delimenu.co/api/v1/restaurants/pizzeria-roma/products/p9Hs4TqL2mNc8VbX6Rdy/image" \
  -H "Authorization: Bearer dmk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
```

**Respuesta 200**

```json
{
  "product_id": "p9Hs4TqL2mNc8VbX6Rdy",
  "has_image": false
}
```

**Errores**

- `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
