Quickstart
You need a sandbox key (bk_test_…) and the id of a sandbox location you are connected to.
Both come from Biskèt with your sandbox access (partners@bisket.ai).
-
Install the SDK (Node 20+). It is a private beta package on GitHub Packages; the SDK page has the three install steps (a
read:packagestoken and an.npmrc).Terminal window npm install @bisket-ai/partner-sdk -
Optional: receive webhooks. Run the receiver below on a public
httpsURL (a tunnel such ascloudflaredorngrokworks) and pass that URL asBISKET_WEBHOOK_URL. The quickstart registers it and prints nothing secret; keep the endpoint’ssecretit returns asBISKET_WEBHOOK_SECRET. -
Run the quickstart.
Terminal window BISKET_API_KEY=bk_test_… BISKET_LOCATION_ID=… BISKET_WEBHOOK_URL=https://… npx tsx quickstart.ts
It reads your account, lists your locations, reads a menu, places an order, plays the restaurant through the simulator (accept, ready, print the pickup ticket), and records the pickup with the ticket’s token.
// Quickstart: from a test key to a picked-up order, in the sandbox.// npm install @bisket-ai/partner-sdk// BISKET_API_KEY=bk_test_… BISKET_LOCATION_ID=… BISKET_WEBHOOK_URL=https://… npx tsx quickstart.tsimport { createBisket, paginate } from '@bisket-ai/partner-sdk';
export interface QuickstartEnv { apiKey: string; locationId: string; webhookUrl?: string }
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
export async function main(env: QuickstartEnv) { const bisket = createBisket({ apiKey: env.apiKey }); // bk_test_ → https://sandbox.api.bisket.ai
// 1. Who am I? const { data: me } = await bisket.GET('/v1/me'); console.log(`partner ${me!.name}, livemode ${me!.livemode}`);
// 2. Optional: a webhook endpoint for order events. Its secret is shown ONCE: store it. let webhookSecret: string | undefined; if (env.webhookUrl) { const { data: endpoint } = await bisket.POST('/v1/webhook_endpoints', { body: { url: env.webhookUrl, enabled_events: ['order.accepted', 'order.ready', 'order.picked_up', 'order.closed'], description: 'quickstart' }, }); webhookSecret = endpoint!.secret; }
// 3. The locations you are connected to. const locations = []; for await (const l of paginate((query) => bisket.GET('/v1/locations', { params: { query } }))) locations.push(l); console.log(`${locations.length} location(s)`); const location_id = env.locationId;
// 4. The menu, and one simple item (no sizes, no sides) to order. const { data: menu } = await bisket.GET('/v1/locations/{location_id}/menu', { params: { path: { location_id } } }); const item = menu!.items.find((i) => i.available && i.variants.length === 0 && i.modifier_groups.length === 0 && i.base_price)!; console.log(`menu version ${menu!.menu_version}: ordering 2 × ${item.name}`);
// 5. Place an order (the partner collects payment). The SDK sends the Idempotency-Key. const unit = item.base_price!.amount; const { data: order } = await bisket.POST('/v1/locations/{location_id}/orders', { params: { path: { location_id } }, body: { external_id: `QS-${Date.now()}`, partner_order_number: String(Date.now()).slice(-6), customer: { first_name: 'Ana' }, items: [{ item_id: item.id, variant_id: null, quantity: 2, unit_price: { amount: unit, currency: 'HTG' }, modifiers: [] }], subtotal: { amount: 2 * unit, currency: 'HTG' }, delivery_fee: { amount: 15000, currency: 'HTG' }, collected_by: 'partner', payment: { method: 'card', status: 'paid', amount: { amount: 2 * unit + 15000, currency: 'HTG' } }, }, }); const order_id = order!.id; console.log(`order ${order_id}: ${order!.status}`);
// 6. Sandbox only: play the restaurant. Accept (once the order reached it), then mark it ready. const simulate = (action: 'accept' | 'mark_ready' | 'issue_pickup_token') => bisket.POST('/v1/sandbox/orders/{order_id}/simulations', { params: { path: { order_id } }, body: { action } }); // Each step reaches the restaurant's system asynchronously: wait for the order to show it. const waitFor = async (status: string) => { for (let i = 0; ; i++) { const { data } = await bisket.GET('/v1/orders/{order_id}', { params: { path: { order_id } } }); if (data!.status === status) return; if (i > 90) throw new Error(`still ${data!.status}, expected ${status}`); await sleep(1000); } }; for (let i = 0; ; i++) { try { await simulate('accept'); break; } catch (e) { if (i > 90) throw e; await sleep(1000); } // the order is still on its way } await waitFor('accepted'); await simulate('mark_ready'); await waitFor('ready');
// 7. The driver scans the QR on the pickup ticket. In the sandbox, ask for the token. const { data: ticket } = await simulate('issue_pickup_token'); const { data: picked } = await bisket.POST('/v1/orders/{order_id}/pickup', { params: { path: { order_id } }, body: { token: (ticket as { pickup_token: string }).pickup_token }, }); console.log(`order ${order_id}: ${picked!.status}`); return { orderId: order_id, status: picked!.status, webhookSecret, locations: locations.length, menuVersion: menu!.menu_version };}
if (process.argv[1] && import.meta.url.endsWith(process.argv[1].split('/').pop()!)) { main({ apiKey: process.env.BISKET_API_KEY!, locationId: process.env.BISKET_LOCATION_ID!, webhookUrl: process.env.BISKET_WEBHOOK_URL }) .catch((e) => { console.error(e); process.exit(1); });}The receiver verifies every delivery’s Bisket-Signature before trusting it:
// A webhook receiver: verify the signature on the RAW body, answer 2xx fast, work after.// BISKET_WEBHOOK_SECRET=whsec_… npx tsx webhook-receiver.tsimport { createServer, type Server } from 'node:http';import { verifyWebhook, WebhookVerificationError } from '@bisket-ai/partner-sdk';
export function receiver(secret: string, onEvent: (type: string, orderId?: string) => void): Server { return createServer(async (req, res) => { const chunks: Buffer[] = []; for await (const c of req) chunks.push(c as Buffer); const raw = Buffer.concat(chunks).toString('utf8'); // never JSON.parse before verifying try { const event = await verifyWebhook(raw, req.headers as Record<string, string>, secret); res.writeHead(204).end(); // acknowledge within 10 s; process asynchronously // Delivery is at least once: de-duplicate on event.id, order by event.sequence per object. if (event.type.startsWith('order.')) onEvent(event.type, (event.data.object as { id: string }).id); else onEvent(event.type); } catch (e) { res.writeHead(e instanceof WebhookVerificationError ? 400 : 500).end(); } });}
if (process.argv[1] && import.meta.url.endsWith(process.argv[1].split('/').pop()!)) { receiver(process.env.BISKET_WEBHOOK_SECRET!, (type, id) => console.log(type, id ?? '')).listen(3000);}What happened
Section titled “What happened”POST /v1/locations/{location_id}/ordersanswered201withstatus: pending. The order reached the restaurant’s system a moment later (order.received), which is why the sample retriesacceptuntil it is there.- The restaurant accepted (
order.accepted), marked it ready (order.ready) and printed the pickup ticket. - The driver’s scan (
POST /v1/orders/{order_id}/pickupwith the token) moved it topicked_up(order.picked_up). At the restaurant’s day close the order closes (order.closed) and posts to the ledger.
Every snippet on this site is a file that runs against the sandbox in our CI, so it works as printed.