Read a restaurant's menu, paid orders, loyalty members, stock and daily sales, mark dishes sold out, and get signed webhooks when orders are paid, refunded or the menu changes.
The business makes a key in Back office > Settings > Partner API & webhooks and gives it to you. A key looks like
bkp_ab12cd34_… (45 characters), is shown to them once, and can be revoked at any time. Send it on every call, either way:
X-Api-Key: bkp_xxxxxxxx_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Authorization: Bearer bkp_xxxxxxxx_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
All paths start with /api/partner/v1. A key made for one branch always works on that branch. A key for every branch works on the
first branch unless you add ?branch=CODE (the branch codes are in the business's back office, e.g. DXB01).
Times are UTC in ISO 8601; amounts are in the branch's currency.
| Scope | Allows |
|---|---|
menu:read | The menu, prices and what is sold out |
menu:write | Marking items sold out or back on sale |
orders:read | Paid and refunded orders with their lines and payments |
loyalty:read | Looking up loyalty members and their points |
stock:read | Stock on hand |
reports:read | Daily sales by branch |
Each key has its own number of calls a minute (60 unless the business set another, up to 600). Every answer carries
X-RateLimit-Limit and X-RateLimit-Remaining; past the limit you get 429 with Retry-After in seconds.
Errors are JSON problem details with a plain title:
| 401 | No key, an unknown key, or a revoked or expired one |
| 403 | The key lacks the scope, works for another branch, or the business no longer has the add-on |
| 404 | No such branch, order, item or member |
| 429 | Too many calls this minute; wait Retry-After seconds |
available. menu:read{"available": false} marks it sold out at the branch; true puts it back. Tills and delivery apps hear about it. menu:writemore is true, call again with after set to the returned next. orders:readorders:readloyalty:readstock:readreports:readcurl https://YOUR-BOOKNIP-HOST/api/partner/v1/orders?from=2026-10-22 \
-H "X-Api-Key: $BOOKNIP_KEY"
{ "branch": "DXB01", "more": false, "next": null,
"orders": [ { "id": "6f1c…", "number": 41, "businessDate": "2026-10-22", "type": "DineIn", "status": "Paid",
"paidAt": "2026-10-22T09:14:03Z", "netTotal": 76.19, "vatTotal": 3.81, "grandTotal": 80.00,
"lines": [ { "sku": "MZ-001", "name": "Hummus", "quantity": 2, "unitPrice": 28.00, "taxCode": "SR", "taxRate": 5 } ],
"payments": [ { "tender": "Card", "amount": 80.00, "isRefund": false } ] } ] }
The business adds your https address and picks events. Each event is a POST with a JSON body:
{ "id": "delivery id", "type": "OrderPaid", "subject": "the order (or shift, member…) id", "created": 1792818843, "data": { … } }
| Event | When |
|---|---|
OrderPaid | An order is paid and its tax invoice issued |
OrderRefunded | An order is refunded |
Partner.MenuItemChanged | A menu item is added, changed, deleted, sold out or back |
GoodsReceived | Stock is received from a supplier |
CashShiftClosed | A cash shift is closed |
LoyaltyMemberJoined, LoyaltyEarned, LoyaltyRedeemed, TierChanged | Loyalty changes |
Ping | The Test button in the back office |
Headers: X-Booknip-Event, X-Booknip-Delivery, X-Booknip-Timestamp (Unix seconds),
X-Booknip-Signature and Idempotency-Key. Answer with any 2xx within 10 seconds. Anything else is retried after
1 and 5 minutes, half an hour, 2, 6 and 12 hours, then a day twice; after that it is marked failed and the business can send it again.
Delivery is at least once, so skip an Idempotency-Key you have already handled. Addresses must be public https; private and local
networks are refused.
The signature is sha256= and the hex HMAC-SHA256 of "{timestamp}.{raw body}" with the webhook's signing secret
(whsec_…, shown to the business when the webhook is made). Use the raw body exactly as received, compare in constant time, and
refuse timestamps more than five minutes old.
const crypto = require('crypto');
function isFromBooknip(req, rawBody, secret) {
const ts = req.headers['x-booknip-timestamp'];
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(`${ts}.${rawBody}`).digest('hex');
const got = req.headers['x-booknip-signature'] || '';
return got.length === expected.length && crypto.timingSafeEqual(Buffer.from(got), Buffer.from(expected));
}
using System.Security.Cryptography;
using System.Text;
static bool IsFromBooknip(HttpRequest req, string rawBody, string secret)
{
var ts = req.Headers["X-Booknip-Timestamp"].ToString();
if (!long.TryParse(ts, out var t) || Math.Abs(DateTimeOffset.UtcNow.ToUnixTimeSeconds() - t) > 300) return false;
var hash = HMACSHA256.HashData(Encoding.UTF8.GetBytes(secret), Encoding.UTF8.GetBytes($"{ts}.{rawBody}"));
var expected = "sha256=" + Convert.ToHexString(hash).ToLowerInvariant();
return CryptographicOperations.FixedTimeEquals(Encoding.ASCII.GetBytes(expected),
Encoding.ASCII.GetBytes(req.Headers["X-Booknip-Signature"].ToString()));
}
import hmac, hashlib, time
def is_from_booknip(headers, raw_body: bytes, secret: str) -> bool:
ts = headers.get("X-Booknip-Timestamp", "")
if not ts.isdigit() or abs(time.time() - int(ts)) > 300:
return False
mac = hmac.new(secret.encode(), ts.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest("sha256=" + mac, headers.get("X-Booknip-Signature", ""))
The demo restaurant (Al Bahar, on a Booknip demo server) has a read-only key for every scope except menu:write:
curl https://YOUR-BOOKNIP-HOST/api/partner/v1/menu -H "X-Api-Key: bkp_demoakey_albaharaccountingappreadonly2345"
Booknip Hospitality · Partner API v1. Breaking changes come as a new version; fields may be added to answers at any time.