Vai al contenuto

API di BookNDish

Costruisci su BookNDish

Un'API REST documentata, webhook firmati e una sandbox gratuita. Collega un sito web, un display di cucina, un CRM o uno strumento di contabilità a qualsiasi ristorante su BookNDish, con il permesso del titolare.

Il riferimento dell'API qui sotto è scritto in inglese.

Guida rapida

  1. 01

    Ottieni un token

    Il titolare del ristorante o un responsabile apre Impostazioni, poi Sviluppatori, e crea un token con gli ambiti che servono alla tua integrazione, oppure un token sandbox per iniziare con dati di esempio. Un token appartiene a un solo ristorante e viene mostrato una sola volta.

  2. 02

    Chiama l'API

    Invia il token come token Bearer. Ogni risposta è in JSON con data, meta e links.

  3. 03

    Scrivi in sicurezza

    Dai a ogni POST un Idempotency-Key. Un nuovo tentativo con la stessa chiave riceve la prima risposta e nulla viene eseguito due volte.

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

Ambiti

Un token porta solo gli ambiti che gli sono stati dati, e ogni endpoint ne richiede uno.

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

Riferimento dell'API

Menu

8 endpoint

Menus, categories and dishes.

GET/menus

Every menu with its categories

Ambito: menu:read

Esempio di risposta 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.

Ambito: menu:read

Parametri
NomeInTipoObbligatorio
menupathintegerObbligatorio
Esempio di risposta 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

Ambito: menu:read

Parametri
NomeInTipoObbligatorio
menu_idqueryintegerFacoltativo
Esempio di risposta 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

Ambito: menu:read

Parametri
NomeInTipoObbligatorio
category_idqueryintegerFacoltativo
availablequerystring: "true", "false", "1", "0"Facoltativo
per_pagequeryintegerFacoltativo
cursorquerystringFacoltativo
Esempio di risposta 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.

Ambito: menu:write

Parametri
NomeInTipoObbligatorio
Idempotency-KeyheaderstringFacoltativo
Corpo della richiesta
{
    "category_id": 7,
    "name": "Burger",
    "price_minor": 1100,
    "allergens": [
        "gluten"
    ]
}
Esempio di risposta 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

Ambito: menu:read

Parametri
NomeInTipoObbligatorio
itempathintegerObbligatorio
Esempio di risposta 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.

Ambito: menu:write

Parametri
NomeInTipoObbligatorio
itempathintegerObbligatorio
Corpo della richiesta
{
    "price_minor": 1250
}
Esempio di risposta 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.

Ambito: menu:write

Parametri
NomeInTipoObbligatorio
itempathintegerObbligatorio
Corpo della richiesta
{
    "is_available": false
}
Esempio di risposta 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 endpoint

Orders on any channel, and their status.

GET/orders

Orders, newest first

Ambito: orders:read

Parametri
NomeInTipoObbligatorio
statusquerystringFacoltativo
typequerystring: "dine_in", "pickup", "delivery"Facoltativo
channelquerystringFacoltativo
placed_fromquerystringFacoltativo
placed_toquerystringFacoltativo
updated_sincequerystringFacoltativo
per_pagequeryintegerFacoltativo
cursorquerystringFacoltativo
Esempio di risposta 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.

Ambito: orders:write

Parametri
NomeInTipoObbligatorio
Idempotency-KeyheaderstringFacoltativo
Corpo della richiesta
{
    "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
        }
    ]
}
Esempio di risposta 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

Ambito: orders:read

Parametri
NomeInTipoObbligatorio
orderpathstringObbligatorio
Esempio di risposta 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.

Ambito: orders:write

Parametri
NomeInTipoObbligatorio
orderpathstringObbligatorio
Idempotency-KeyheaderstringFacoltativo
Corpo della richiesta
{
    "status": "accepted"
}
Esempio di risposta 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 endpoint

Bookings and table availability. Needs a plan with reservations.

GET/reservations

Bookings for a day or a range

Ambito: reservations:read

Parametri
NomeInTipoObbligatorio
datequerystringFacoltativo
fromquerystringFacoltativo
toquerystringFacoltativo
statusquerystring: "pending", "confirmed", "declined", "cancelled", "seated", "no_show"Facoltativo
per_pagequeryintegerFacoltativo
cursorquerystringFacoltativo
Esempio di risposta 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.

Ambito: reservations:write

Parametri
NomeInTipoObbligatorio
Idempotency-KeyheaderstringFacoltativo
Corpo della richiesta
{
    "name": "Ada Lovelace",
    "phone": "+15550100",
    "party_size": 2,
    "date": "2026-10-03",
    "time": "19:30"
}
Esempio di risposta 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

Ambito: reservations:read

Parametri
NomeInTipoObbligatorio
datequerystringObbligatorio
party_sizequeryintegerObbligatorio
Esempio di risposta 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

Ambito: reservations:read

Parametri
NomeInTipoObbligatorio
reservationpathstringObbligatorio
Esempio di risposta 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.

Ambito: reservations:write

Parametri
NomeInTipoObbligatorio
reservationpathstringObbligatorio
Idempotency-KeyheaderstringFacoltativo
Esempio di risposta 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 endpoint

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.

Ambito: guests:read

Parametri
NomeInTipoObbligatorio
marketing_opted_inquerystring: "true", "false", "1", "0"Facoltativo
updated_sincequerystringFacoltativo
per_pagequeryintegerFacoltativo
cursorquerystringFacoltativo
Esempio di risposta 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

Ambito: guests:read

Parametri
NomeInTipoObbligatorio
guestpathintegerObbligatorio
Esempio di risposta 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 endpoint

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.

Ambito: reports:read

Parametri
NomeInTipoObbligatorio
fromquerystringFacoltativo
toquerystringFacoltativo
branch_idqueryintegerFacoltativo
Esempio di risposta 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 endpoint

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 della richiesta
{
    "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_..."
}
Esempio di risposta 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 della richiesta
{
    "token": "bnd_ort_q8Lz...",
    "client_id": "bnd_app_4kq8...",
    "client_secret": "bnd_cs_..."
}
Esempio di risposta 200
{}

Webhook

Aggiungi un indirizzo https in Impostazioni, poi Sviluppatori, e scegli i suoi eventi. Ogni consegna è un POST firmato: rispondi con un qualsiasi 2xx entro 10 secondi. Un errore viene ritentato dopo 1 minuto, 5 minuti, 30 minuti, 2 ore e 6 ore, e il registro delle consegne può rieseguirlo.

Eventi

order.created
Viene effettuato un ordine su qualsiasi canale.
order.status_changed
Un ordine passa a un altro stato.
order.paid
I pagamenti coprono l'ordine.
reservation.created
Viene fatta una prenotazione, dall'ospite o dal personale.
reservation.updated
Una prenotazione cambia.
reservation.cancelled
Una prenotazione viene annullata.
menu.item_updated
Un piatto cambia: prezzo, nome, esaurito.
payment.refunded
Un pagamento viene rimborsato.
guest.created
Viene creato il profilo di un ospite.
guest.updated
I dati o il consenso di un ospite cambiano, oppure l'ospite viene cancellato.
giftcard.issued
Una carta regalo viene venduta o emessa.
giftcard.redeemed
Una carta regalo paga un ordine.
review.created
Un ospite invia una valutazione.
stock.below_par
Un ingrediente scende sotto la scorta minima in una sede.
shift.published
Un responsabile pubblica una settimana di turni.
experience.booked
I biglietti per un'esperienza vengono pagati.

Verifica la firma

BookNDish-Signature contiene t, un orario Unix, e v1, l'HMAC-SHA256 esadecimale di t, un punto e il corpo grezzo, con il tuo segreto di firma come chiave. Confronta in tempo costante e rifiuta un t più vecchio di cinque minuti.

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

Una sandbox è un locale di esempio tutto suo, con menu, tavoli, ospiti, una prenotazione e un ordine, dietro un solo token con tutti gli ambiti. Risponde allo stesso indirizzo con un header BookNDish-Sandbox, non tocca mai il ristorante reale e torna ai suoi dati di esempio ogni notte.

Creane una in Impostazioni, poi Sviluppatori, poi Sandbox. Il suo token inizia con bnd_sbx_.

Limiti ed errori

  • Ogni token può effettuare 60 richieste al minuto per impostazione predefinita. Ogni risposta contiene X-RateLimit-Remaining, e un 429 contiene Retry-After.
  • Un errore risponde con error.code, error.message ed error.details. Un token vede sempre e solo il proprio ristorante: qualsiasi altra cosa è un 404.
  • v1 può solo crescere: nuovi endpoint, campi facoltativi ed eventi. Una modifica che romperebbe un client va in v2, e v1 resta al suo fianco.