Ir para o conteúdo

API da BookNDish

Construa sobre a BookNDish

Uma API REST documentada, webhooks assinados e uma sandbox gratuita. Ligue um site, um ecrã de cozinha, um CRM ou uma ferramenta de contabilidade a qualquer restaurante na BookNDish, com a autorização do proprietário.

A referência da API abaixo está escrita em inglês.

Início rápido

  1. 01

    Obtenha um token

    O proprietário do restaurante ou um gerente abre Definições, depois Programadores, e cria um token com as permissões de que a sua integração precisa, ou um token de sandbox para começar com dados de exemplo. Um token pertence a um só restaurante e é mostrado uma única vez.

  2. 02

    Chame a API

    Envie o token como token Bearer. Cada resposta é JSON com data, meta e links.

  3. 03

    Escreva com segurança

    Dê a cada POST um Idempotency-Key. Uma nova tentativa com a mesma chave recebe a primeira resposta de volta, e nada é executado duas vezes.

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

Permissões

Um token só leva as permissões que lhe foram dadas, e cada endpoint exige uma.

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

Referência da API

Menu

8 endpoints

Menus, categories and dishes.

GET/menus

Every menu with its categories

Permissão: menu:read

Exemplo de resposta 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.

Permissão: menu:read

Parâmetros
NomeEmTipoObrigatório
menupathintegerObrigatório
Exemplo de resposta 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

Permissão: menu:read

Parâmetros
NomeEmTipoObrigatório
menu_idqueryintegerOpcional
Exemplo de resposta 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

Permissão: menu:read

Parâmetros
NomeEmTipoObrigatório
category_idqueryintegerOpcional
availablequerystring: "true", "false", "1", "0"Opcional
per_pagequeryintegerOpcional
cursorquerystringOpcional
Exemplo de resposta 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.

Permissão: menu:write

Parâmetros
NomeEmTipoObrigatório
Idempotency-KeyheaderstringOpcional
Corpo do pedido
{
    "category_id": 7,
    "name": "Burger",
    "price_minor": 1100,
    "allergens": [
        "gluten"
    ]
}
Exemplo de resposta 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

Permissão: menu:read

Parâmetros
NomeEmTipoObrigatório
itempathintegerObrigatório
Exemplo de resposta 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.

Permissão: menu:write

Parâmetros
NomeEmTipoObrigatório
itempathintegerObrigatório
Corpo do pedido
{
    "price_minor": 1250
}
Exemplo de resposta 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.

Permissão: menu:write

Parâmetros
NomeEmTipoObrigatório
itempathintegerObrigatório
Corpo do pedido
{
    "is_available": false
}
Exemplo de resposta 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

Permissão: orders:read

Parâmetros
NomeEmTipoObrigatório
statusquerystringOpcional
typequerystring: "dine_in", "pickup", "delivery"Opcional
channelquerystringOpcional
placed_fromquerystringOpcional
placed_toquerystringOpcional
updated_sincequerystringOpcional
per_pagequeryintegerOpcional
cursorquerystringOpcional
Exemplo de resposta 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.

Permissão: orders:write

Parâmetros
NomeEmTipoObrigatório
Idempotency-KeyheaderstringOpcional
Corpo do pedido
{
    "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
        }
    ]
}
Exemplo de resposta 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

Permissão: orders:read

Parâmetros
NomeEmTipoObrigatório
orderpathstringObrigatório
Exemplo de resposta 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.

Permissão: orders:write

Parâmetros
NomeEmTipoObrigatório
orderpathstringObrigatório
Idempotency-KeyheaderstringOpcional
Corpo do pedido
{
    "status": "accepted"
}
Exemplo de resposta 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

Permissão: reservations:read

Parâmetros
NomeEmTipoObrigatório
datequerystringOpcional
fromquerystringOpcional
toquerystringOpcional
statusquerystring: "pending", "confirmed", "declined", "cancelled", "seated", "no_show"Opcional
per_pagequeryintegerOpcional
cursorquerystringOpcional
Exemplo de resposta 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.

Permissão: reservations:write

Parâmetros
NomeEmTipoObrigatório
Idempotency-KeyheaderstringOpcional
Corpo do pedido
{
    "name": "Ada Lovelace",
    "phone": "+15550100",
    "party_size": 2,
    "date": "2026-10-03",
    "time": "19:30"
}
Exemplo de resposta 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

Permissão: reservations:read

Parâmetros
NomeEmTipoObrigatório
datequerystringObrigatório
party_sizequeryintegerObrigatório
Exemplo de resposta 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

Permissão: reservations:read

Parâmetros
NomeEmTipoObrigatório
reservationpathstringObrigatório
Exemplo de resposta 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.

Permissão: reservations:write

Parâmetros
NomeEmTipoObrigatório
reservationpathstringObrigatório
Idempotency-KeyheaderstringOpcional
Exemplo de resposta 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.

Permissão: guests:read

Parâmetros
NomeEmTipoObrigatório
marketing_opted_inquerystring: "true", "false", "1", "0"Opcional
updated_sincequerystringOpcional
per_pagequeryintegerOpcional
cursorquerystringOpcional
Exemplo de resposta 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

Permissão: guests:read

Parâmetros
NomeEmTipoObrigatório
guestpathintegerObrigatório
Exemplo de resposta 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.

Permissão: reports:read

Parâmetros
NomeEmTipoObrigatório
fromquerystringOpcional
toquerystringOpcional
branch_idqueryintegerOpcional
Exemplo de resposta 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.

Corpo do pedido
{
    "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_..."
}
Exemplo de resposta 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.

Corpo do pedido
{
    "token": "bnd_ort_q8Lz...",
    "client_id": "bnd_app_4kq8...",
    "client_secret": "bnd_cs_..."
}
Exemplo de resposta 200
{}

Webhooks

Adicione um endereço https em Definições, depois Programadores, e escolha os seus eventos. Cada entrega é um POST assinado: responda com qualquer 2xx em 10 segundos. Uma falha é repetida após 1 minuto, 5 minutos, 30 minutos, 2 horas e 6 horas, e o registo de entregas pode reenviá-la.

Eventos

order.created
É feito um pedido em qualquer canal.
order.status_changed
Um pedido passa para outro estado.
order.paid
Os pagamentos cobrem o pedido.
reservation.created
É feita uma reserva, pelo cliente ou pela equipa.
reservation.updated
Uma reserva muda.
reservation.cancelled
Uma reserva é cancelada.
menu.item_updated
Um prato muda: preço, nome, esgotado.
payment.refunded
Um pagamento é reembolsado.
guest.created
É criado um perfil de cliente.
guest.updated
Os dados ou o consentimento de um cliente mudam, ou o cliente é apagado.
giftcard.issued
Um cartão-presente é vendido ou emitido.
giftcard.redeemed
Um cartão-presente paga um pedido.
review.created
Um cliente envia uma avaliação.
stock.below_par
Um ingrediente desce abaixo do seu nível mínimo numa unidade.
shift.published
Um gerente publica uma semana de turnos.
experience.booked
Os bilhetes de uma experiência são pagos.

Verifique a assinatura

BookNDish-Signature traz t, uma hora Unix, e v1, o HMAC-SHA256 hexadecimal de t, um ponto e o corpo em bruto, com o seu segredo de assinatura como chave. Compare em tempo constante e recuse um t com mais 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

Uma sandbox é um espaço de exemplo próprio, com menu, mesas, clientes, uma reserva e um pedido, atrás de um token com todas as permissões. Responde no mesmo endereço com um cabeçalho BookNDish-Sandbox, nunca toca no restaurante real e volta aos seus dados de exemplo todas as noites.

Crie uma em Definições, depois Programadores, depois Sandbox. O seu token começa por bnd_sbx_.

Limites e erros

  • Cada token pode fazer 60 pedidos por minuto por predefinição. Cada resposta inclui X-RateLimit-Remaining, e um 429 inclui Retry-After.
  • Uma falha responde com error.code, error.message e error.details. Um token só vê o seu próprio restaurante: tudo o resto é um 404.
  • v1 só cresce: novos endpoints, campos opcionais e eventos. Uma alteração que quebraria um cliente vai para v2, mantendo-se v1 ao lado.