Booknip

Partner API v1

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.

Keys

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.

Scopes

ScopeAllows
menu:readThe menu, prices and what is sold out
menu:writeMarking items sold out or back on sale
orders:readPaid and refunded orders with their lines and payments
loyalty:readLooking up loyalty members and their points
stock:readStock on hand
reports:readDaily sales by branch

Limits and errors

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:

401No key, an unknown key, or a revoked or expired one
403The key lacks the scope, works for another branch, or the business no longer has the add-on
404No such branch, order, item or member
429Too many calls this minute; wait Retry-After seconds

Endpoints

GET/me
The business, the key's name, scopes, limit and branch. Any scope.
GET/menu
Categories and items with price, VAT, SKU, barcode and available. menu:read
PUT/menu/items/{id}/availability
Body {"available": false} marks it sold out at the branch; true puts it back. Tills and delivery apps hear about it. menu:write
GET/orders?from=2026-10-01&to=2026-10-31&limit=50&after=…
Paid and refunded orders, oldest first, up to 200 a page. When more is true, call again with after set to the returned next. orders:read
GET/orders/{id}
One order with lines (SKU, quantity, price, tax code) and payments. orders:read
GET/loyalty/members?phone=0501234567
Member number, tier, points, visits and whether they agreed to offers. loyalty:read
GET/stock
Stock on hand by location with reorder levels. stock:read
GET/sales/daily?date=2026-10-22
Orders, net, VAT and gross for a business day, by tender. reports:read
curl 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 } ] } ] }

Webhooks

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": { … } }
EventWhen
OrderPaidAn order is paid and its tax invoice issued
OrderRefundedAn order is refunded
Partner.MenuItemChangedA menu item is added, changed, deleted, sold out or back
GoodsReceivedStock is received from a supplier
CashShiftClosedA cash shift is closed
LoyaltyMemberJoined, LoyaltyEarned, LoyaltyRedeemed, TierChangedLoyalty changes
PingThe 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.

Checking signatures

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", ""))

Try it

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"
Works only where the demo data is loaded.

  

Booknip Hospitality · Partner API v1. Breaking changes come as a new version; fields may be added to answers at any time.