Skip to content
Beta: the Biskèt Partner API may still change before v1 is final. Sandbox access is by request:partners@bisket.ai.

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).

  1. Install the SDK (Node 20+). It is a private beta package on GitHub Packages; the SDK page has the three install steps (a read:packages token and an .npmrc).

    Terminal window
    npm install @bisket-ai/partner-sdk
  2. Optional: receive webhooks. Run the receiver below on a public https URL (a tunnel such as cloudflared or ngrok works) and pass that URL as BISKET_WEBHOOK_URL. The quickstart registers it and prints nothing secret; keep the endpoint’s secret it returns as BISKET_WEBHOOK_SECRET.

  3. 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.ts
// 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.ts
import { 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:

webhook-receiver.ts
// A webhook receiver: verify the signature on the RAW body, answer 2xx fast, work after.
// BISKET_WEBHOOK_SECRET=whsec_… npx tsx webhook-receiver.ts
import { 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);
}
  • POST /v1/locations/{location_id}/orders answered 201 with status: pending. The order reached the restaurant’s system a moment later (order.received), which is why the sample retries accept until 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}/pickup with the token) moved it to picked_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.