Ir al contenido

API de BookNDish

Construye sobre BookNDish

Una API REST documentada, webhooks firmados y un sandbox gratuito. Conecta una web, una pantalla de cocina, un CRM o una herramienta de contabilidad a cualquier restaurante de BookNDish, con el permiso del propietario.

La referencia de la API que aparece abajo está escrita en inglés.

Inicio rápido

  1. 01

    Obtén un token

    El propietario del restaurante o un encargado abre Ajustes, luego Desarrolladores, y crea un token con los permisos que necesita tu integración, o un token de sandbox para empezar con datos de ejemplo. Un token pertenece a un solo restaurante y se muestra una vez.

  2. 02

    Llama a la API

    Envía el token como token Bearer. Cada respuesta es JSON con data, meta y links.

  3. 03

    Escribe con seguridad

    Da a cada POST un Idempotency-Key. Un reintento con la misma clave recibe la primera respuesta y nada se ejecuta dos veces.

URL base: https://restaurant-saas-19qm.onrender.com/api/v1

curl https://restaurant-saas-19qm.onrender.com/api/v1/menus \
  -H "Authorization: Bearer $BOOKNDISH_TOKEN" \
  -H "Accept: application/json"
curl -X POST https://restaurant-saas-19qm.onrender.com/api/v1/orders \
  -H "Authorization: Bearer $BOOKNDISH_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 5b0e1f4e-6a8c-4e0e-9a55-0c1f2d3e4f5a" \
  -d '{"type":"pickup","customer_name":"Ada","customer_phone":"+15550100","lines":[{"item_id":42,"quantity":2}]}'

Permisos

Un token solo lleva los permisos que se le dieron, y cada endpoint pide uno.

  • menu:read
  • menu:write
  • orders:read
  • orders:write
  • reservations:read
  • reservations:write
  • guests:read
  • reports:read

Referencia de la API

Menu

8 endpoints

Menus, categories and dishes.

GET/menus

Every menu with its categories

Permiso: menu:read

Ejemplo de respuesta 200
{
    "data": [
        {
            "id": 3,
            "name": {
                "en": "Dinner"
            },
            "is_active": true,
            "sort_order": 0,
            "categories": [
                {
                    "id": 7,
                    "menu_id": 3,
                    "name": {
                        "en": "Mains"
                    },
                    "is_active": true,
                    "sort_order": 0
                }
            ]
        }
    ],
    "meta": {
        "count": 1,
        "currency": "EUR"
    },
    "links": {}
}
GET/menus/{menu}

One menu, its categories and their dishes

meta.now says what menus by time of day and happy hours make of the menu right now, on the default branch's clock: whether it and each category are open, and each dish or size a happy hour has cut, at the price charged now.

Permiso: menu:read

Parámetros
NombreEnTipoObligatorio
menupathintegerObligatorio
Ejemplo de respuesta 200
{
    "data": {
        "id": 3,
        "name": {
            "en": "Dinner"
        },
        "is_active": true,
        "sort_order": 0,
        "categories": [
            {
                "id": 7,
                "menu_id": 3,
                "name": {
                    "en": "Mains"
                },
                "is_active": true,
                "sort_order": 0,
                "items": [
                    {
                        "id": 42,
                        "category_id": 7,
                        "name": {
                            "en": "Burger"
                        },
                        "description": {
                            "en": "Beef, brioche"
                        },
                        "price_minor": 1100,
                        "is_available": true,
                        "show_online": true,
                        "dietary": "none",
                        "allergens": [
                            "gluten",
                            "milk"
                        ],
                        "sort_order": 1,
                        "variants": [],
                        "modifier_groups": [],
                        "updated_at": "2026-09-29T10:12:00+00:00"
                    }
                ]
            }
        ]
    },
    "meta": {
        "currency": "EUR",
        "now": {
            "available": true,
            "closed_category_ids": [],
            "prices": []
        }
    },
    "links": {}
}
GET/categories

Every category, or one menu's

Permiso: menu:read

Parámetros
NombreEnTipoObligatorio
menu_idqueryintegerOpcional
Ejemplo de respuesta 200
{
    "data": [
        {
            "id": 7,
            "menu_id": 3,
            "name": {
                "en": "Mains"
            },
            "is_active": true,
            "sort_order": 0
        }
    ],
    "meta": {
        "count": 1
    },
    "links": {}
}
GET/items

Dishes, a page at a time

Permiso: menu:read

Parámetros
NombreEnTipoObligatorio
category_idqueryintegerOpcional
availablequerystring: "true", "false", "1", "0"Opcional
per_pagequeryintegerOpcional
cursorquerystringOpcional
Ejemplo de respuesta 200
{
    "data": [
        {
            "id": 42,
            "category_id": 7,
            "name": {
                "en": "Burger"
            },
            "description": null,
            "price_minor": 1100,
            "is_available": true,
            "show_online": true,
            "dietary": "none",
            "allergens": null,
            "sort_order": 1,
            "variants": [],
            "modifier_groups": [],
            "updated_at": "2026-09-29T10:12:00+00:00"
        }
    ],
    "meta": {
        "currency": "EUR",
        "per_page": 25,
        "next_cursor": null,
        "prev_cursor": null
    },
    "links": {
        "next": null,
        "prev": null
    }
}
POST/items

Add a dish

name and description are a string in the restaurant's default language, or a translation map. A dish is priced by price_minor, or per size by variants.

Permiso: menu:write

Parámetros
NombreEnTipoObligatorio
Idempotency-KeyheaderstringOpcional
Cuerpo de la solicitud
{
    "category_id": 7,
    "name": "Burger",
    "price_minor": 1100,
    "allergens": [
        "gluten"
    ]
}
Ejemplo de respuesta 201
{
    "data": {
        "id": 42,
        "category_id": 7,
        "name": {
            "en": "Burger"
        },
        "description": {
            "en": "Beef, brioche"
        },
        "price_minor": 1100,
        "is_available": true,
        "show_online": true,
        "dietary": "none",
        "allergens": [
            "gluten",
            "milk"
        ],
        "sort_order": 1,
        "variants": [],
        "modifier_groups": [
            {
                "id": 5,
                "name": {
                    "en": "Extras"
                },
                "min_select": 0,
                "max_select": null,
                "options": [
                    {
                        "id": 9,
                        "name": {
                            "en": "Cheese"
                        },
                        "price_minor": 100,
                        "is_available": true
                    }
                ]
            }
        ],
        "updated_at": "2026-09-29T10:12:00+00:00"
    },
    "meta": {
        "currency": "USD"
    },
    "links": {}
}
GET/items/{item}

One dish

Permiso: menu:read

Parámetros
NombreEnTipoObligatorio
itempathintegerObligatorio
Ejemplo de respuesta 200
{
    "data": {
        "id": 42,
        "category_id": 7,
        "name": {
            "en": "Burger"
        },
        "description": {
            "en": "Beef, brioche"
        },
        "price_minor": 1100,
        "is_available": true,
        "show_online": true,
        "dietary": "none",
        "allergens": [
            "gluten",
            "milk"
        ],
        "sort_order": 1,
        "variants": [],
        "modifier_groups": [
            {
                "id": 5,
                "name": {
                    "en": "Extras"
                },
                "min_select": 0,
                "max_select": null,
                "options": [
                    {
                        "id": 9,
                        "name": {
                            "en": "Cheese"
                        },
                        "price_minor": 100,
                        "is_available": true
                    }
                ]
            }
        ],
        "updated_at": "2026-09-29T10:12:00+00:00"
    },
    "meta": {
        "currency": "USD"
    },
    "links": {}
}
PATCH/items/{item}

Change a dish

Only the fields sent change; variants and modifier groups are kept. A price change is recorded in the activity log. A dish priced per size refuses price_minor.

Permiso: menu:write

Parámetros
NombreEnTipoObligatorio
itempathintegerObligatorio
Cuerpo de la solicitud
{
    "price_minor": 1250
}
Ejemplo de respuesta 200
{
    "data": {
        "id": 42,
        "category_id": 7,
        "name": {
            "en": "Burger"
        },
        "description": {
            "en": "Beef, brioche"
        },
        "price_minor": 1100,
        "is_available": true,
        "show_online": true,
        "dietary": "none",
        "allergens": [
            "gluten",
            "milk"
        ],
        "sort_order": 1,
        "variants": [],
        "modifier_groups": [
            {
                "id": 5,
                "name": {
                    "en": "Extras"
                },
                "min_select": 0,
                "max_select": null,
                "options": [
                    {
                        "id": 9,
                        "name": {
                            "en": "Cheese"
                        },
                        "price_minor": 100,
                        "is_available": true
                    }
                ]
            }
        ],
        "updated_at": "2026-09-29T10:12:00+00:00"
    },
    "meta": {
        "currency": "USD"
    },
    "links": {}
}
PUT/items/{item}/availability

Sold out, and back

Recorded like a flip on the till.

Permiso: menu:write

Parámetros
NombreEnTipoObligatorio
itempathintegerObligatorio
Cuerpo de la solicitud
{
    "is_available": false
}
Ejemplo de respuesta 200
{
    "data": {
        "id": 42,
        "category_id": 7,
        "name": {
            "en": "Burger"
        },
        "description": {
            "en": "Beef, brioche"
        },
        "price_minor": 1100,
        "is_available": true,
        "show_online": true,
        "dietary": "none",
        "allergens": [
            "gluten",
            "milk"
        ],
        "sort_order": 1,
        "variants": [],
        "modifier_groups": [
            {
                "id": 5,
                "name": {
                    "en": "Extras"
                },
                "min_select": 0,
                "max_select": null,
                "options": [
                    {
                        "id": 9,
                        "name": {
                            "en": "Cheese"
                        },
                        "price_minor": 100,
                        "is_available": true
                    }
                ]
            }
        ],
        "updated_at": "2026-09-29T10:12:00+00:00"
    },
    "meta": {
        "currency": "USD"
    },
    "links": {}
}

Orders

4 endpoints

Orders on any channel, and their status.

GET/orders

Orders, newest first

Permiso: orders:read

Parámetros
NombreEnTipoObligatorio
statusquerystringOpcional
typequerystring: "dine_in", "pickup", "delivery"Opcional
channelquerystringOpcional
placed_fromquerystringOpcional
placed_toquerystringOpcional
updated_sincequerystringOpcional
per_pagequeryintegerOpcional
cursorquerystringOpcional
Ejemplo de respuesta 200
{
    "data": [],
    "meta": {
        "per_page": 25,
        "next_cursor": null,
        "prev_cursor": null
    },
    "links": {
        "next": null,
        "prev": null
    }
}
POST/orders

Place an order

The order is priced from the menu (any price in the body is ignored), the owner's ordering rules apply (types offered, minimum order, delivery zones, opening hours; an order outside hours is held for the next opening), the kitchen gets its tickets and a connected POS gets the order. Dine-in orders need table_code.

Permiso: orders:write

Parámetros
NombreEnTipoObligatorio
Idempotency-KeyheaderstringOpcional
Cuerpo de la solicitud
{
    "type": "delivery",
    "channel": "web",
    "customer_name": "Ada Lovelace",
    "customer_phone": "+15550100",
    "customer_email": "ada@example.com",
    "note": "Ring twice",
    "delivery_address": {
        "line1": "12 Main St",
        "city": "Springfield"
    },
    "lines": [
        {
            "item_id": 42,
            "quantity": 2,
            "option_ids": [
                9
            ]
        },
        {
            "item_id": 43,
            "variant_id": 11,
            "quantity": 1
        }
    ]
}
Ejemplo de respuesta 201
{
    "data": {
        "id": "9d1c7a52-8f0e-4b8c-a1f5-3a1a5b1f0c2d",
        "status": "placed",
        "type": "delivery",
        "channel": "web",
        "branch_id": 1,
        "table": null,
        "customer": {
            "name": "Ada Lovelace",
            "phone": "+15550100",
            "email": "ada@example.com"
        },
        "note": "Ring twice",
        "delivery_address": {
            "line1": "12 Main St",
            "city": "Springfield"
        },
        "currency": "USD",
        "subtotal_minor": 2300,
        "discount_minor": 0,
        "service_charge_minor": 0,
        "tax_added_minor": 0,
        "delivery_fee_minor": 300,
        "total_minor": 2600,
        "tax_breakdown": null,
        "cancel_reason": null,
        "items": [
            {
                "id": 1,
                "item_id": 42,
                "name": {
                    "en": "Burger"
                },
                "variant_name": null,
                "quantity": 2,
                "unit_price_minor": 1200,
                "line_total_minor": 2400,
                "voided": false,
                "modifiers": [
                    {
                        "name": {
                            "en": "Cheese"
                        },
                        "price_minor": 100
                    }
                ]
            }
        ],
        "placed_at": "2026-09-29T10:15:00+00:00",
        "scheduled_for": null,
        "paid_at": null,
        "updated_at": "2026-09-29T10:15:00+00:00"
    },
    "meta": {},
    "links": {}
}
GET/orders/{order}

One order

Permiso: orders:read

Parámetros
NombreEnTipoObligatorio
orderpathstringObligatorio
Ejemplo de respuesta 200
{
    "data": {
        "id": "9d1c7a52-8f0e-4b8c-a1f5-3a1a5b1f0c2d",
        "status": "placed",
        "type": "delivery",
        "channel": "web",
        "branch_id": 1,
        "table": null,
        "customer": {
            "name": "Ada Lovelace",
            "phone": "+15550100",
            "email": "ada@example.com"
        },
        "note": "Ring twice",
        "delivery_address": {
            "line1": "12 Main St",
            "city": "Springfield"
        },
        "currency": "USD",
        "subtotal_minor": 2300,
        "discount_minor": 0,
        "service_charge_minor": 0,
        "tax_added_minor": 0,
        "delivery_fee_minor": 300,
        "total_minor": 2600,
        "tax_breakdown": null,
        "cancel_reason": null,
        "items": [
            {
                "id": 1,
                "item_id": 42,
                "name": {
                    "en": "Burger"
                },
                "variant_name": null,
                "quantity": 2,
                "unit_price_minor": 1200,
                "line_total_minor": 2400,
                "voided": false,
                "modifiers": [
                    {
                        "name": {
                            "en": "Cheese"
                        },
                        "price_minor": 100
                    }
                ]
            }
        ],
        "placed_at": "2026-09-29T10:15:00+00:00",
        "scheduled_for": null,
        "paid_at": null,
        "updated_at": "2026-09-29T10:15:00+00:00"
    },
    "meta": {},
    "links": {}
}
POST/orders/{order}/status

Move an order one step

placed to accepted or rejected, accepted to ready, ready to completed, and anything not finished to cancelled. Accepting fires the kitchen. A cancel with a reason (3 to 200 characters) also voids the kitchen tickets and keeps the reason.

Permiso: orders:write

Parámetros
NombreEnTipoObligatorio
orderpathstringObligatorio
Idempotency-KeyheaderstringOpcional
Cuerpo de la solicitud
{
    "status": "accepted"
}
Ejemplo de respuesta 200
{
    "data": {
        "id": "9d1c7a52-8f0e-4b8c-a1f5-3a1a5b1f0c2d",
        "status": "placed",
        "type": "delivery",
        "channel": "web",
        "branch_id": 1,
        "table": null,
        "customer": {
            "name": "Ada Lovelace",
            "phone": "+15550100",
            "email": "ada@example.com"
        },
        "note": "Ring twice",
        "delivery_address": {
            "line1": "12 Main St",
            "city": "Springfield"
        },
        "currency": "USD",
        "subtotal_minor": 2300,
        "discount_minor": 0,
        "service_charge_minor": 0,
        "tax_added_minor": 0,
        "delivery_fee_minor": 300,
        "total_minor": 2600,
        "tax_breakdown": null,
        "cancel_reason": null,
        "items": [
            {
                "id": 1,
                "item_id": 42,
                "name": {
                    "en": "Burger"
                },
                "variant_name": null,
                "quantity": 2,
                "unit_price_minor": 1200,
                "line_total_minor": 2400,
                "voided": false,
                "modifiers": [
                    {
                        "name": {
                            "en": "Cheese"
                        },
                        "price_minor": 100
                    }
                ]
            }
        ],
        "placed_at": "2026-09-29T10:15:00+00:00",
        "scheduled_for": null,
        "paid_at": null,
        "updated_at": "2026-09-29T10:15:00+00:00"
    },
    "meta": {},
    "links": {}
}

Reservations

5 endpoints

Bookings and table availability. Needs a plan with reservations.

GET/reservations

Bookings for a day or a range

Permiso: reservations:read

Parámetros
NombreEnTipoObligatorio
datequerystringOpcional
fromquerystringOpcional
toquerystringOpcional
statusquerystring: "pending", "confirmed", "declined", "cancelled", "seated", "no_show"Opcional
per_pagequeryintegerOpcional
cursorquerystringOpcional
Ejemplo de respuesta 200
{
    "data": [],
    "meta": {
        "per_page": 25,
        "next_cursor": null,
        "prev_cursor": null
    },
    "links": {
        "next": null,
        "prev": null
    }
}
POST/reservations

Write a booking down

The booking rules apply (party size, lead time, closing days); the guest gets the same confirmation as for a phone booking.

Permiso: reservations:write

Parámetros
NombreEnTipoObligatorio
Idempotency-KeyheaderstringOpcional
Cuerpo de la solicitud
{
    "name": "Ada Lovelace",
    "phone": "+15550100",
    "party_size": 2,
    "date": "2026-10-03",
    "time": "19:30"
}
Ejemplo de respuesta 201
{
    "data": {
        "id": "1b6e1c1e-2f4f-4c43-9e0e-4b9d6f1a2c3d",
        "status": "confirmed",
        "name": "Ada Lovelace",
        "phone": "+15550100",
        "email": null,
        "party_size": 2,
        "date": "2026-10-03",
        "time": "19:30",
        "table_id": null,
        "note": null,
        "deposit_minor": null,
        "deposit_status": null,
        "created_at": "2026-10-01T09:00:00+00:00",
        "updated_at": "2026-10-01T09:00:00+00:00"
    },
    "meta": {},
    "links": {}
}
GET/availability

Bookable times for a day and a party

Permiso: reservations:read

Parámetros
NombreEnTipoObligatorio
datequerystringObligatorio
party_sizequeryintegerObligatorio
Ejemplo de respuesta 200
{
    "data": {
        "date": "2026-10-03",
        "party_size": 2,
        "engine": true,
        "slots": [
            {
                "time": "19:00",
                "available": true,
                "guarantee": false
            },
            {
                "time": "19:30",
                "available": false,
                "guarantee": false
            }
        ],
        "guarantee": null,
        "notify": true
    },
    "meta": {},
    "links": {}
}
GET/reservations/{reservation}

One booking

Permiso: reservations:read

Parámetros
NombreEnTipoObligatorio
reservationpathstringObligatorio
Ejemplo de respuesta 200
{
    "data": {
        "id": "1b6e1c1e-2f4f-4c43-9e0e-4b9d6f1a2c3d",
        "status": "confirmed",
        "name": "Ada Lovelace",
        "phone": "+15550100",
        "email": null,
        "party_size": 2,
        "date": "2026-10-03",
        "time": "19:30",
        "table_id": null,
        "note": null,
        "deposit_minor": null,
        "deposit_status": null,
        "created_at": "2026-10-01T09:00:00+00:00",
        "updated_at": "2026-10-01T09:00:00+00:00"
    },
    "meta": {},
    "links": {}
}
POST/reservations/{reservation}/cancel

Cancel a booking

Cancelled by the restaurant; the guest is told.

Permiso: reservations:write

Parámetros
NombreEnTipoObligatorio
reservationpathstringObligatorio
Idempotency-KeyheaderstringOpcional
Ejemplo de respuesta 200
{
    "data": {
        "id": "1b6e1c1e-2f4f-4c43-9e0e-4b9d6f1a2c3d",
        "status": "confirmed",
        "name": "Ada Lovelace",
        "phone": "+15550100",
        "email": null,
        "party_size": 2,
        "date": "2026-10-03",
        "time": "19:30",
        "table_id": null,
        "note": null,
        "deposit_minor": null,
        "deposit_status": null,
        "created_at": "2026-10-01T09:00:00+00:00",
        "updated_at": "2026-10-01T09:00:00+00:00"
    },
    "meta": {},
    "links": {}
}

Guests

2 endpoints

Guest profiles, with contact details only for guests who agreed to share them.

GET/guests

Guest profiles, a page at a time

Guests who asked to be forgotten never appear. Phone and email are included only for guests who agreed to hear from the restaurant. Staff notes are never included.

Permiso: guests:read

Parámetros
NombreEnTipoObligatorio
marketing_opted_inquerystring: "true", "false", "1", "0"Opcional
updated_sincequerystringOpcional
per_pagequeryintegerOpcional
cursorquerystringOpcional
Ejemplo de respuesta 200
{
    "data": [
        {
            "id": 5,
            "name": "Ada Lovelace",
            "phone": "+15550100",
            "email": "ada@example.com",
            "contact_shared": true,
            "locale": "en",
            "marketing_opted_in": true,
            "marketing_opted_in_at": "2026-09-01T12:00:00+00:00",
            "visits_count": 4,
            "total_spend_minor": 9200,
            "last_visit_at": "2026-09-28T20:15:00+00:00",
            "created_at": "2026-06-02T19:40:00+00:00"
        }
    ],
    "meta": {
        "per_page": 25,
        "next_cursor": null,
        "prev_cursor": null
    },
    "links": {
        "next": null,
        "prev": null
    }
}
GET/guests/{guest}

One guest profile

Permiso: guests:read

Parámetros
NombreEnTipoObligatorio
guestpathintegerObligatorio
Ejemplo de respuesta 200
{
    "data": {
        "id": 6,
        "name": "Alan Turing",
        "phone": null,
        "email": null,
        "contact_shared": false,
        "locale": null,
        "marketing_opted_in": false,
        "marketing_opted_in_at": null,
        "visits_count": 1,
        "total_spend_minor": 2400,
        "last_visit_at": null,
        "created_at": "2026-09-30T11:00:00+00:00"
    },
    "meta": {},
    "links": {}
}

Reports

1 endpoints

Sales figures. Needs a plan with reporting.

GET/reports/sales-summary

Revenue, orders and average order for a period

With the preceding period of the same length. Defaults to today on the restaurant's clock; at most 366 days.

Permiso: reports:read

Parámetros
NombreEnTipoObligatorio
fromquerystringOpcional
toquerystringOpcional
branch_idqueryintegerOpcional
Ejemplo de respuesta 200
{
    "data": {
        "from": "2026-09-01",
        "to": "2026-09-30",
        "branch_id": null,
        "currency": "EUR",
        "orders": 812,
        "revenue_minor": 1843200,
        "average_order_minor": 2270,
        "previous": {
            "orders": 760,
            "revenue_minor": 1690000,
            "average_order_minor": 2223
        },
        "change": {
            "revenue_pct": 9.1,
            "orders_pct": 6.8
        }
    },
    "meta": {},
    "links": {}
}

OAuth

2 endpoints

Apps from the marketplace: a venue owner allows an app on the consent screen, and the app exchanges the code for an access token (used as the bearer token of every operation above) and a refresh token. Authorization code with PKCE (S256).

POST/oauth/token

Exchange a code, or refresh, for tokens

authorization_code: redeem the code the venue's consent sent to your redirect address, with the PKCE verifier. refresh_token: get a new access token and a new refresh token; the old pair stops working. Authenticate with your client id and secret, as HTTP Basic or as client_id and client_secret in the body. Answers are never cached.

Cuerpo de la solicitud
{
    "grant_type": "authorization_code",
    "code": "bnd_ac_9fK2...",
    "redirect_uri": "https://app.example.com/callback",
    "code_verifier": "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk",
    "client_id": "bnd_app_4kq8...",
    "client_secret": "bnd_cs_..."
}
Ejemplo de respuesta 200
{
    "access_token": "812|bnd_oat_Xf3k...",
    "token_type": "Bearer",
    "expires_in": 3600,
    "refresh_token": "bnd_ort_q8Lz...",
    "scope": "menu:read orders:read"
}
POST/oauth/revoke

Revoke a token

Gives back a refresh token (with its access token) or an access token (RFC 7009). An unknown token is not an error. Authenticate with your client id and secret, as HTTP Basic or as client_id and client_secret in the body. To end the whole grant, the venue uninstalls the app.

Cuerpo de la solicitud
{
    "token": "bnd_ort_q8Lz...",
    "client_id": "bnd_app_4kq8...",
    "client_secret": "bnd_cs_..."
}
Ejemplo de respuesta 200
{}

Webhooks

Añade una dirección https en Ajustes, luego Desarrolladores, y elige sus eventos. Cada envío es un POST firmado: responde con cualquier 2xx en menos de 10 segundos. Un fallo se reintenta tras 1 minuto, 5 minutos, 30 minutos, 2 horas y 6 horas, y el registro de envíos puede reenviarlo.

Eventos

order.created
Se hace un pedido por cualquier canal.
order.status_changed
Un pedido pasa a otro estado.
order.paid
Los pagos cubren el pedido.
reservation.created
Se hace una reserva, por el cliente o por el personal.
reservation.updated
Cambia una reserva.
reservation.cancelled
Se cancela una reserva.
menu.item_updated
Cambia un plato: precio, nombre, agotado.
payment.refunded
Se reembolsa un pago.
guest.created
Se crea un perfil de cliente.
guest.updated
Cambian los datos o el consentimiento de un cliente, o se borra al cliente.
giftcard.issued
Se vende o se emite una tarjeta regalo.
giftcard.redeemed
Una tarjeta regalo paga un pedido.
review.created
Un cliente envía una valoración.
stock.below_par
Un ingrediente baja de su nivel mínimo en un local.
shift.published
Un encargado publica una semana de turnos.
experience.booked
Se pagan las entradas de una experiencia.

Verifica la firma

BookNDish-Signature lleva t, una hora Unix, y v1, el HMAC-SHA256 hexadecimal de t, un punto y el cuerpo sin procesar, con tu secreto de firma como clave. Compara en tiempo constante y rechaza un t de más de cinco minutos.

function bookndishSignatureIsValid(string $payload, string $header, string $secret, int $tolerance = 300): bool
{
    $parts = [];
    foreach (explode(',', $header) as $pair) {
        [$key, $value] = array_pad(explode('=', trim($pair), 2), 2, '');
        $parts[$key][] = $value;
    }

    $timestamp = (int) ($parts['t'][0] ?? 0);
    if ($timestamp === 0 || abs(time() - $timestamp) > $tolerance) {
        return false;
    }

    $expected = hash_hmac('sha256', $timestamp.'.'.$payload, $secret);
    foreach ($parts['v1'] ?? [] as $signature) {
        if (hash_equals($expected, $signature)) {
            return true;
        }
    }

    return false;
}

Sandbox

Un sandbox es un local de ejemplo propio, con carta, mesas, clientes, una reserva y un pedido, detrás de un token con todos los permisos. Responde en la misma dirección con una cabecera BookNDish-Sandbox, nunca toca el restaurante real y vuelve a sus datos de ejemplo cada noche.

Crea uno en Ajustes, luego Desarrolladores, luego Sandbox. Su token empieza por bnd_sbx_.

Límites y errores

  • Cada token puede hacer 60 solicitudes por minuto por defecto. Cada respuesta incluye X-RateLimit-Remaining, y un 429 incluye Retry-After.
  • Un fallo responde con error.code, error.message y error.details. Un token solo ve su propio restaurante: cualquier otra cosa es un 404.
  • v1 solo crece: nuevos endpoints, campos opcionales y eventos. Un cambio que rompería a un cliente va a v2, y v1 se mantiene a su lado.