# Binadit API with JavaScript / TypeScript

[All languages and safety rules](/developers/guides/code-examples?lang=en) · [API reference](/developers/api?lang=en#reference)

Node.js 22+. A server-side ES module that can also be imported from TypeScript. Never include service secrets in browser bundles.

## 1. Reusable client

Save this as `binadit.mjs`. The client follows no redirects and does not retry requests. Supply secrets in the environment through your secret manager; nothing is automatically saved or logged.

```javascript
// Node.js 22+, server-side JavaScript. Never put service secrets in browser code.
import { createHmac, timingSafeEqual } from 'node:crypto';
const ORIGIN = 'https://my.binadit.com';
const LIMIT = 8 * 1024 * 1024;

async function send(method, path, headers, body) {
  if (!(path.startsWith('/api/v1/') || path === '/oauth/token') || /[\\#]/.test(path)) {
    throw new Error('Use a relative Binadit API path');
  }
  let response;
  try {
    response = await fetch(ORIGIN + path, {
      method, headers, body, redirect: 'manual', signal: AbortSignal.timeout(30000),
    });
  } catch {
    throw new Error('Transport outcome unknown; preserve the original request');
  }
  if (!response.ok) {
    await response.body?.cancel();
    throw new Error(`API HTTP ${response.status}; inspect the original operation`);
  }
  const chunks = [];
  let size = 0;
  for await (const chunk of response.body ?? []) {
    size += chunk.length;
    if (size > LIMIT) throw new Error("Response exceeds the example's 8 MiB limit");
    chunks.push(Buffer.from(chunk));
  }
  return { headers: response.headers, bytes: Buffer.concat(chunks) };
}

export async function accessToken() {
  if (process.env.BINADIT_ACCESS_TOKEN) return process.env.BINADIT_ACCESS_TOKEN;
  const { BINADIT_CLIENT_ID: id, BINADIT_CLIENT_SECRET: secret } = process.env;
  if (!id || !secret) throw new Error('Supply service credentials from your secret manager');
  const result = await send('POST', '/oauth/token', {
    Accept: 'application/json', 'Content-Type': 'application/x-www-form-urlencoded',
  }, new URLSearchParams({ grant_type: 'client_credentials', client_id: id, client_secret: secret }));
  const token = JSON.parse(result.bytes.toString('utf8')).access_token;
  if (typeof token !== 'string' || !token) throw new Error('Token response is invalid');
  return token;
}

export async function api(token, method, path, body = undefined, options = {}) {
  if (!path.startsWith('/api/v1/') || !['GET', 'POST', 'PUT', 'PATCH', 'DELETE'].includes(method)) {
    throw new Error('Use a supported API method and path');
  }
  const headers = { Authorization: `Bearer ${token}`, Accept: options.binary ? 'application/pdf' : 'application/json' };
  if (method !== 'GET') {
    if (!/^[A-Za-z0-9_.:-]{16,128}$/.test(options.key ?? '')) throw new Error('Supply the persisted original Idempotency-Key');
    headers['Idempotency-Key'] = options.key;
  }
  if (options.version) headers['If-Match'] = options.version;
  if (body !== undefined) headers['Content-Type'] = 'application/json';
  const result = await send(method, path, headers, body === undefined ? undefined : JSON.stringify(body));
  if (options.binary && result.headers.get('content-type')?.split(';')[0] !== 'application/pdf') throw new Error('Expected an invoice PDF');
  return { etag: result.headers.get('etag'), data: options.binary ? result.bytes : (result.bytes.length ? JSON.parse(result.bytes.toString('utf8')) : null) };
}

export function verifyWebhook(raw, deliveryId, timestamp, signature, expectedKeyId, secret, now = Date.now() / 1000) {
  const uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
  if (raw.length > 1024 * 1024 || secret.length < 32 || !uuid.test(deliveryId) || !uuid.test(expectedKeyId)
      || !/^[1-9][0-9]{8,10}$/.test(timestamp) || Math.abs(now - Number(timestamp)) > 300) return false;
  const digest = createHmac('sha256', secret).update(`${timestamp}.${deliveryId}.`).update(raw).digest('hex');
  const expected = Buffer.from(`v1=${digest},key_id=${expectedKeyId}`);
  const actual = Buffer.from(signature);
  return actual.length === expected.length && timingSafeEqual(actual, expected);
}
```

## 2. Once for this workflow

```javascript
import { accessToken, api } from './binadit.mjs';
const token = await accessToken();
```

Request a token once for this short workflow. A service credential is not human step-up. Stop on 401 and inspect any mutation that might already have been sent. Each code block below is a separate example: do not paste them all into a single sequence. All IDs and example.test addresses are fictional. Write examples with possible effects use dry_run.

## Check the authenticated identity

`GET /api/v1/me` · scope `account:read`

```javascript
const result = await api(token, "GET", "/api/v1/me");
```

## List accessible companies

`GET /api/v1/accounts` · scope `account:read`

```javascript
const result = await api(token, "GET", "/api/v1/accounts");
```

## Check capabilities and scopes

`GET /api/v1/accounts/123/capabilities` · scope `account:read`

```javascript
const result = await api(token, "GET", "/api/v1/accounts/123/capabilities");
```

## Read company details and their ETag

`GET /api/v1/accounts/123/profile` · scope `account:read`

```javascript
const result = await api(token, "GET", "/api/v1/accounts/123/profile");
```

## Update company details

`PATCH /api/v1/accounts/123/profile` · scope `account:write`

First read **GET profile** and preserve its HTTP ETag (or explicitly documented plan version) as `BINADIT_RESOURCE_VERSION`. Versions belonging to different resources are not interchangeable.

For exactly this reviewed request, use a previously durably recorded `BINADIT_REQUEST_KEY` containing 16–128 safe characters. Reuse it only for the same method, URL and JSON, never for the next step.

```javascript
const result = await api(token, "PATCH", "/api/v1/accounts/123/profile", {"city": "Utrecht", "dry_run": true}, { version: process.env.BINADIT_RESOURCE_VERSION, key: process.env.BINADIT_REQUEST_KEY });
```

## Read products and prices

`GET /api/v1/accounts/123/products` · scope `products:read`

```javascript
const result = await api(token, "GET", "/api/v1/accounts/123/products");
```

## List existing domain holders

`GET /api/v1/accounts/123/holders` · scope `holders:read`

```javascript
const result = await api(token, "GET", "/api/v1/accounts/123/holders");
```

## Preview a default holder from company or personal details

`POST /api/v1/accounts/123/holders/default` · scope `holders:read, holders:write`

For exactly this reviewed request, use a previously durably recorded `BINADIT_REQUEST_KEY` containing 16–128 safe characters. Reuse it only for the same method, URL and JSON, never for the next step.

```javascript
const result = await api(token, "POST", "/api/v1/accounts/123/holders/default", {"dry_run": true}, { key: process.env.BINADIT_REQUEST_KEY });
```

## Create a company domain holder

`POST /api/v1/accounts/123/holders` · scope `holders:write`

For exactly this reviewed request, use a previously durably recorded `BINADIT_REQUEST_KEY` containing 16–128 safe characters. Reuse it only for the same method, URL and JSON, never for the next step.

```javascript
const result = await api(token, "POST", "/api/v1/accounts/123/holders", {"initials": "A", "firstname": "Ada", "lastname": "Example", "gender": "F", "street": "Voorbeeldstraat", "number": "1", "zipcode": "1234 AB", "city": "Utrecht", "country": "NL", "email": "ada@example.test", "tel1": "+31", "tel3": "612345678", "dry_run": true}, { key: process.env.BINADIT_REQUEST_KEY });
```

## Check domain availability

`POST /api/v1/accounts/123/domains/check` · scope `domains:read`

For exactly this reviewed request, use a previously durably recorded `BINADIT_REQUEST_KEY` containing 16–128 safe characters. Reuse it only for the same method, URL and JSON, never for the next step.

```javascript
const result = await api(token, "POST", "/api/v1/accounts/123/domains/check", {"domains": ["example.test"]}, { key: process.env.BINADIT_REQUEST_KEY });
```

## Quote a domain registration

`POST /api/v1/accounts/123/order-quotes` · scope `orders:write`

For exactly this reviewed request, use a previously durably recorded `BINADIT_REQUEST_KEY` containing 16–128 safe characters. Reuse it only for the same method, URL and JSON, never for the next step.

```javascript
const result = await api(token, "POST", "/api/v1/accounts/123/order-quotes", {"kind": "domain_register", "input": {"domain": "example.test", "product_id": 9, "holder_id": 17}, "dry_run": true}, { key: process.env.BINADIT_REQUEST_KEY });
```

## Read a saved quote again

`GET /api/v1/accounts/123/order-quotes/11111111-1111-4111-8111-111111111111` · scope `orders:read`

```javascript
const result = await api(token, "GET", "/api/v1/accounts/123/order-quotes/11111111-1111-4111-8111-111111111111");
```

## Preview an order and payment

`POST /api/v1/accounts/123/orders` · scope `orders:commit`

For exactly this reviewed request, use a previously durably recorded `BINADIT_REQUEST_KEY` containing 16–128 safe characters. Reuse it only for the same method, URL and JSON, never for the next step.

```javascript
const result = await api(token, "POST", "/api/v1/accounts/123/orders", {"quote_id": "11111111-1111-4111-8111-111111111111", "dry_run": true}, { key: process.env.BINADIT_REQUEST_KEY });
```

## Read registration renewal and subscription status

`GET /api/v1/accounts/123/domains/31/subscription` · scope `domains:read`

```javascript
const result = await api(token, "GET", "/api/v1/accounts/123/domains/31/subscription");
```

## Disable automatic domain renewal

`POST /api/v1/accounts/123/domains/31/actions/cancel` · scope `domains:write`

First read **GET domains/31** and preserve its HTTP ETag (or explicitly documented plan version) as `BINADIT_RESOURCE_VERSION`. Versions belonging to different resources are not interchangeable.

For exactly this reviewed request, use a previously durably recorded `BINADIT_REQUEST_KEY` containing 16–128 safe characters. Reuse it only for the same method, URL and JSON, never for the next step.

```javascript
const result = await api(token, "POST", "/api/v1/accounts/123/domains/31/actions/cancel", {"confirm_domain": "example.test", "dry_run": true}, { version: process.env.BINADIT_RESOURCE_VERSION, key: process.env.BINADIT_REQUEST_KEY });
```

## List DNS zones

`GET /api/v1/accounts/123/dns/zones` · scope `dns.zones:read`

```javascript
const result = await api(token, "GET", "/api/v1/accounts/123/dns/zones");
```

## Read current DNS records and version

`GET /api/v1/accounts/123/dns/zones/8/records` · scope `dns.records:read`

```javascript
const result = await api(token, "GET", "/api/v1/accounts/123/dns/zones/8/records");
```

## List DNS templates

`GET /api/v1/accounts/123/dns/templates` · scope `dns.templates:read`

```javascript
const result = await api(token, "GET", "/api/v1/accounts/123/dns/templates");
```

## Preview a DNS template

`POST /api/v1/accounts/123/dns/templates` · scope `dns.templates:write`

For exactly this reviewed request, use a previously durably recorded `BINADIT_REQUEST_KEY` containing 16–128 safe characters. Reuse it only for the same method, URL and JSON, never for the next step.

```javascript
const result = await api(token, "POST", "/api/v1/accounts/123/dns/templates", {"name": "Websitevoorbeeld", "replace": "none", "records": [{"name": "@", "type": "A", "value": "192.0.2.50", "ttl": 300}, {"name": "www", "type": "CNAME", "value": "example.test", "ttl": 300}], "dry_run": true}, { key: process.env.BINADIT_REQUEST_KEY });
```

## Propose applying a template

`POST /api/v1/accounts/123/dns/zones/8/template-plans` · scope `dns.records:write`

For exactly this reviewed request, use a previously durably recorded `BINADIT_REQUEST_KEY` containing 16–128 safe characters. Reuse it only for the same method, URL and JSON, never for the next step.

```javascript
const result = await api(token, "POST", "/api/v1/accounts/123/dns/zones/8/template-plans", {"template_id": 11, "dry_run": true}, { key: process.env.BINADIT_REQUEST_KEY });
```

## Preview multiple DNS records in one plan

`POST /api/v1/accounts/123/dns/change-plans` · scope `dns.records:write`

For exactly this reviewed request, use a previously durably recorded `BINADIT_REQUEST_KEY` containing 16–128 safe characters. Reuse it only for the same method, URL and JSON, never for the next step.

```javascript
const result = await api(token, "POST", "/api/v1/accounts/123/dns/change-plans", {"zone": "example.test", "changes": [{"action": "create", "name": "proef", "type": "A", "value": "192.0.2.50", "ttl": 300}, {"action": "create", "name": "_verify", "type": "TXT", "value": "binadit-example", "ttl": 300}], "dry_run": true}, { key: process.env.BINADIT_REQUEST_KEY });
```

## Apply a reviewed DNS plan

`POST /api/v1/accounts/123/dns/change-plans/11111111-1111-4111-8111-111111111111/apply` · scope `dns.records:write`

First read **the version of the original reviewed DNS plan** and preserve its HTTP ETag (or explicitly documented plan version) as `BINADIT_RESOURCE_VERSION`. Versions belonging to different resources are not interchangeable.

For exactly this reviewed request, use a previously durably recorded `BINADIT_REQUEST_KEY` containing 16–128 safe characters. Reuse it only for the same method, URL and JSON, never for the next step.

```javascript
const result = await api(token, "POST", "/api/v1/accounts/123/dns/change-plans/11111111-1111-4111-8111-111111111111/apply", {"dry_run": true}, { version: process.env.BINADIT_RESOURCE_VERSION, key: process.env.BINADIT_REQUEST_KEY });
```

## Follow the original operation

`GET /api/v1/accounts/123/operations/22222222-2222-4222-8222-222222222222` · scope `operations:read`

```javascript
const result = await api(token, "GET", "/api/v1/accounts/123/operations/22222222-2222-4222-8222-222222222222");
```

## List invoices

`GET /api/v1/accounts/123/billing/invoices` · scope `billing.invoices:read`

```javascript
const result = await api(token, "GET", "/api/v1/accounts/123/billing/invoices");
```

## Download an invoice PDF

`GET /api/v1/accounts/123/billing/invoices/45/pdf` · scope `billing.invoices:read`

```javascript
const result = await api(token, "GET", "/api/v1/accounts/123/billing/invoices/45/pdf", undefined, { binary: true });
```

`result.data` contains only PDF bytes. Use the separate private-file examples below; do not print PDF content or personal data.

## List Workspaces

`GET /api/v1/accounts/123/workspaces` · scope `workspace:read`

```javascript
const result = await api(token, "GET", "/api/v1/accounts/123/workspaces");
```

## List mailboxes

`GET /api/v1/accounts/123/workspaces/7/mailboxes` · scope `workspace.mailboxes:read`

```javascript
const result = await api(token, "GET", "/api/v1/accounts/123/workspaces/7/mailboxes");
```

## Read delegates and user-configured shares

`GET /api/v1/accounts/123/workspaces/7/mailboxes/23/delegates` · scope `workspace.mailboxes:read`

```javascript
const result = await api(token, "GET", "/api/v1/accounts/123/workspaces/7/mailboxes/23/delegates");
```

## Grant full mail access and calendar editing

`PUT /api/v1/accounts/123/workspaces/7/mailboxes/23/delegates/24` · scope `workspace.mailboxes:write`

First read **GET workspaces/7/mailboxes/23** and preserve its HTTP ETag (or explicitly documented plan version) as `BINADIT_RESOURCE_VERSION`. Versions belonging to different resources are not interchangeable.

For exactly this reviewed request, use a previously durably recorded `BINADIT_REQUEST_KEY` containing 16–128 safe characters. Reuse it only for the same method, URL and JSON, never for the next step.

```javascript
const result = await api(token, "PUT", "/api/v1/accounts/123/workspaces/7/mailboxes/23/delegates/24", {"level": "full", "send_as": false, "calendar_level": "write", "dry_run": true}, { version: process.env.BINADIT_RESOURCE_VERSION, key: process.env.BINADIT_REQUEST_KEY });
```

## Preview a free mailing list

`POST /api/v1/accounts/123/workspaces/7/mailing-lists` · scope `workspace.mailboxes:write`

First read **GET workspaces/7** and preserve its HTTP ETag (or explicitly documented plan version) as `BINADIT_RESOURCE_VERSION`. Versions belonging to different resources are not interchangeable.

For exactly this reviewed request, use a previously durably recorded `BINADIT_REQUEST_KEY` containing 16–128 safe characters. Reuse it only for the same method, URL and JSON, never for the next step.

```javascript
const result = await api(token, "POST", "/api/v1/accounts/123/workspaces/7/mailing-lists", {"domain_id": 12, "local_part": "team", "display_name": "Team", "members": [{"kind": "mailbox", "id": 23}, {"kind": "mailbox", "id": 24}], "dry_run": true}, { version: process.env.BINADIT_RESOURCE_VERSION, key: process.env.BINADIT_REQUEST_KEY });
```

## Add one mailing-list member

`PUT /api/v1/accounts/123/workspaces/7/mailing-lists/10/members/mailbox-24` · scope `workspace.mailboxes:write`

First read **GET workspaces/7/mailing-lists/10** and preserve its HTTP ETag (or explicitly documented plan version) as `BINADIT_RESOURCE_VERSION`. Versions belonging to different resources are not interchangeable.

For exactly this reviewed request, use a previously durably recorded `BINADIT_REQUEST_KEY` containing 16–128 safe characters. Reuse it only for the same method, URL and JSON, never for the next step.

```javascript
const result = await api(token, "PUT", "/api/v1/accounts/123/workspaces/7/mailing-lists/10/members/mailbox-24", {"dry_run": true}, { version: process.env.BINADIT_RESOURCE_VERSION, key: process.env.BINADIT_REQUEST_KEY });
```

## Remove one mailing-list member

`DELETE /api/v1/accounts/123/workspaces/7/mailing-lists/10/members/mailbox-24` · scope `workspace.mailboxes:write`

First read **GET workspaces/7/mailing-lists/10** and preserve its HTTP ETag (or explicitly documented plan version) as `BINADIT_RESOURCE_VERSION`. Versions belonging to different resources are not interchangeable.

For exactly this reviewed request, use a previously durably recorded `BINADIT_REQUEST_KEY` containing 16–128 safe characters. Reuse it only for the same method, URL and JSON, never for the next step.

```javascript
const result = await api(token, "DELETE", "/api/v1/accounts/123/workspaces/7/mailing-lists/10/members/mailbox-24", {"dry_run": true}, { version: process.env.BINADIT_RESOURCE_VERSION, key: process.env.BINADIT_REQUEST_KEY });
```

## Preview a free room

`POST /api/v1/accounts/123/workspaces/7/rooms` · scope `workspace.mailboxes:write`

First read **GET workspaces/7** and preserve its HTTP ETag (or explicitly documented plan version) as `BINADIT_RESOURCE_VERSION`. Versions belonging to different resources are not interchangeable.

For exactly this reviewed request, use a previously durably recorded `BINADIT_REQUEST_KEY` containing 16–128 safe characters. Reuse it only for the same method, URL and JSON, never for the next step.

```javascript
const result = await api(token, "POST", "/api/v1/accounts/123/workspaces/7/rooms", {"domain_id": 12, "local_part": "vergaderzaal", "display_name": "Vergaderzaal 1", "kind": "room", "capacity": 8, "location": "Eerste verdieping", "manager_ids": [23], "dry_run": true}, { version: process.env.BINADIT_RESOURCE_VERSION, key: process.env.BINADIT_REQUEST_KEY });
```

## Add a room manager

`PUT /api/v1/accounts/123/workspaces/7/rooms/11/managers/24` · scope `workspace.mailboxes:write`

First read **GET workspaces/7/rooms/11** and preserve its HTTP ETag (or explicitly documented plan version) as `BINADIT_RESOURCE_VERSION`. Versions belonging to different resources are not interchangeable.

For exactly this reviewed request, use a previously durably recorded `BINADIT_REQUEST_KEY` containing 16–128 safe characters. Reuse it only for the same method, URL and JSON, never for the next step.

```javascript
const result = await api(token, "PUT", "/api/v1/accounts/123/workspaces/7/rooms/11/managers/24", {"dry_run": true}, { version: process.env.BINADIT_RESOURCE_VERSION, key: process.env.BINADIT_REQUEST_KEY });
```

## Delete a room with personal verification

`DELETE /api/v1/accounts/123/workspaces/7/rooms/11` · scope `workspace.mailboxes:write`

First read **GET workspaces/7/rooms/11** and preserve its HTTP ETag (or explicitly documented plan version) as `BINADIT_RESOURCE_VERSION`. Versions belonging to different resources are not interchangeable.

For exactly this reviewed request, use a previously durably recorded `BINADIT_REQUEST_KEY` containing 16–128 safe characters. Reuse it only for the same method, URL and JSON, never for the next step.

Also requires recent strong human verification, current privileges and enabled sensitive operations. A service credential cannot supply that verification.

```javascript
const result = await api(token, "DELETE", "/api/v1/accounts/123/workspaces/7/rooms/11", {"confirm_address": "vergaderzaal@example.test", "dry_run": true}, { version: process.env.BINADIT_RESOURCE_VERSION, key: process.env.BINADIT_REQUEST_KEY });
```

## List hosting subscriptions

`GET /api/v1/accounts/123/hosting/subscriptions` · scope `hosting:read`

```javascript
const result = await api(token, "GET", "/api/v1/accounts/123/hosting/subscriptions");
```

## Preview hosting cancellation

`POST /api/v1/accounts/123/hosting/subscriptions/41/actions/cancel` · scope `hosting:write`

First read **GET hosting/subscriptions/41** and preserve its HTTP ETag (or explicitly documented plan version) as `BINADIT_RESOURCE_VERSION`. Versions belonging to different resources are not interchangeable.

For exactly this reviewed request, use a previously durably recorded `BINADIT_REQUEST_KEY` containing 16–128 safe characters. Reuse it only for the same method, URL and JSON, never for the next step.

```javascript
const result = await api(token, "POST", "/api/v1/accounts/123/hosting/subscriptions/41/actions/cancel", {"intent_id": "33333333-3333-4333-8333-333333333333", "dry_run": true}, { version: process.env.BINADIT_RESOURCE_VERSION, key: process.env.BINADIT_REQUEST_KEY });
```

## List spam filters

`GET /api/v1/accounts/123/spam/filters` · scope `spam:read`

```javascript
const result = await api(token, "GET", "/api/v1/accounts/123/spam/filters");
```

## Invite a colleague with limited permissions

`POST /api/v1/accounts/123/team/invitations` · scope `team:write`

First read **GET profile** and preserve its HTTP ETag (or explicitly documented plan version) as `BINADIT_RESOURCE_VERSION`. Versions belonging to different resources are not interchangeable.

For exactly this reviewed request, use a previously durably recorded `BINADIT_REQUEST_KEY` containing 16–128 safe characters. Reuse it only for the same method, URL and JSON, never for the next step.

Also requires recent strong human verification, current privileges and enabled sensitive operations. A service credential cannot supply that verification.

```javascript
const result = await api(token, "POST", "/api/v1/accounts/123/team/invitations", {"email": "collega@example.test", "role": "member", "permissions": [{"module": "module_dns", "level": "read"}], "dry_run": true}, { version: process.env.BINADIT_RESOURCE_VERSION, key: process.env.BINADIT_REQUEST_KEY });
```

## Preview a paused webhook

`POST /api/v1/accounts/123/webhooks` · scope `webhooks:write`

For exactly this reviewed request, use a previously durably recorded `BINADIT_REQUEST_KEY` containing 16–128 safe characters. Reuse it only for the same method, URL and JSON, never for the next step.

Also requires recent strong human verification, current privileges and enabled sensitive operations. A service credential cannot supply that verification.

```javascript
const result = await api(token, "POST", "/api/v1/accounts/123/webhooks", {"name": "Orderstatus", "url": "https://receiver.example.test/binadit", "integration_id": "44444444-4444-4444-8444-444444444444", "dry_run": true}, { key: process.env.BINADIT_REQUEST_KEY });
```

## Inspect webhook deliveries

`GET /api/v1/accounts/123/webhooks/55555555-5555-4555-8555-555555555555/deliveries` · scope `webhooks:read`

```javascript
const result = await api(token, "GET", "/api/v1/accounts/123/webhooks/55555555-5555-4555-8555-555555555555/deliveries");
```

## Read the current product version

`GET /api/v1/accounts/123/products/9` · scope `products:read`

```javascript
const result = await api(token, "GET", "/api/v1/accounts/123/products/9");
```

## Quote a paid personal mailbox

`POST /api/v1/accounts/123/order-quotes` · scope `orders:write`

For exactly this reviewed request, use a previously durably recorded `BINADIT_REQUEST_KEY` containing 16–128 safe characters. Reuse it only for the same method, URL and JSON, never for the next step.

```javascript
const result = await api(token, "POST", "/api/v1/accounts/123/order-quotes", {"kind": "workspace_mailbox", "input": {"workspace_id": 7, "product_id": 9, "domain_id": 12, "local_part": "ada", "display_name": "Ada", "drive": false}, "dry_run": true}, { key: process.env.BINADIT_REQUEST_KEY });
```

## Preview a paid shared mailbox

`POST /api/v1/accounts/123/workspaces/7/shared-mailboxes` · scope `workspace.mailboxes:write, orders:commit`

First read **GET workspaces/7** and preserve its HTTP ETag (or explicitly documented plan version) as `BINADIT_RESOURCE_VERSION`. Versions belonging to different resources are not interchangeable.

For exactly this reviewed request, use a previously durably recorded `BINADIT_REQUEST_KEY` containing 16–128 safe characters. Reuse it only for the same method, URL and JSON, never for the next step.

Also read `GET products/9` and save its unquoted `data.version` as `BINADIT_PRODUCT_VERSION`. This paid shared mailbox requires `orders:commit` and current ordering/spending privileges in addition to mail access. A product version is not a Workspace version.

```javascript
const payload = {"domain_id": 12, "product_id": 9, "local_part": "support", "display_name": "Support", "member_ids": [23, 24], "dry_run": true};
payload["product_version"] = process.env.BINADIT_PRODUCT_VERSION;
const result = await api(token, "POST", "/api/v1/accounts/123/workspaces/7/shared-mailboxes", payload, { version: process.env.BINADIT_RESOURCE_VERSION, key: process.env.BINADIT_REQUEST_KEY });
```

## Enable Send As separately

`PUT /api/v1/accounts/123/workspaces/7/mailboxes/23/delegates/24` · scope `workspace.mailboxes:write`

First read **GET workspaces/7/mailboxes/23** and preserve its HTTP ETag (or explicitly documented plan version) as `BINADIT_RESOURCE_VERSION`. Versions belonging to different resources are not interchangeable.

For exactly this reviewed request, use a previously durably recorded `BINADIT_REQUEST_KEY` containing 16–128 safe characters. Reuse it only for the same method, URL and JSON, never for the next step.

Enabling Send As requires recent strong personal verification and full mail access. A service credential with only the stated scope is insufficient.

```javascript
const result = await api(token, "PUT", "/api/v1/accounts/123/workspaces/7/mailboxes/23/delegates/24", {"level": "full", "send_as": true, "calendar_level": "write", "dry_run": true}, { version: process.env.BINADIT_RESOURCE_VERSION, key: process.env.BINADIT_REQUEST_KEY });
```

## Revoke mail and calendar access together

`DELETE /api/v1/accounts/123/workspaces/7/mailboxes/23/delegates/24` · scope `workspace.mailboxes:write`

First read **GET workspaces/7/mailboxes/23** and preserve its HTTP ETag (or explicitly documented plan version) as `BINADIT_RESOURCE_VERSION`. Versions belonging to different resources are not interchangeable.

For exactly this reviewed request, use a previously durably recorded `BINADIT_REQUEST_KEY` containing 16–128 safe characters. Reuse it only for the same method, URL and JSON, never for the next step.

```javascript
const result = await api(token, "DELETE", "/api/v1/accounts/123/workspaces/7/mailboxes/23/delegates/24", {"dry_run": true}, { version: process.env.BINADIT_RESOURCE_VERSION, key: process.env.BINADIT_REQUEST_KEY });
```

## Further workflows

### Paginate without skipping resources

Check `meta.next_cursor` even when `data` is empty. Do not follow an external next-page URL with a token.

```javascript
let cursor = null;
const seen = new Set();
for (let page = 0; page < 20; page++) {
  const query = new URLSearchParams({ limit: '25' });
  if (cursor !== null) query.set('cursor', String(cursor));
  const result = await api(token, 'GET', `/api/v1/accounts/123/domains?${query}`);
  for (const domain of result.data.data) console.log(domain.id);
  cursor = result.data.meta?.next_cursor ?? null;
  if (cursor === null) break;
  if (seen.has(String(cursor))) throw new Error('Repeated pagination cursor');
  seen.add(String(cursor));
  if (page === 19) throw new Error('Page limit reached; explicitly continue from the saved cursor');
}
```

### Save a PDF without overwriting files

```javascript
import { open } from 'node:fs/promises';
const result = await api(token, 'GET', '/api/v1/accounts/123/billing/invoices/45/pdf', undefined, { binary: true });
const file = await open('factuur-45.pdf', 'wx', 0o600);
try { await file.writeFile(result.data); } finally { await file.close(); }
```

On Windows, use a private directory with appropriate ACLs. Do not log PDF bytes.

### Verify a webhook signature

```javascript
import { verifyWebhook } from './binadit.mjs';
// rawBody must be the unmodified Buffer, before JSON parsing middleware.
const valid = verifyWebhook(rawBody, deliveryIdHeader, timestampHeader, signatureHeader,
  process.env.BINADIT_WEBHOOK_KEY_ID, process.env.BINADIT_WEBHOOK_SECRET);
if (!valid) throw new Error('Reject this webhook');
// Next: atomically insert deliveryIdHeader (UNIQUE) and enqueue work in your DB.
```

Check that the headers and secrets are present before calling the function. Set body limits and reject duplicate headers. HMAC verification uses a constant-time comparison and accepts at most five minutes of clock difference. It does not create a work item or prevent replay by itself. Atomically record the unique delivery ID and work item before returning 2xx. For TypeScript, you can include this server-side ES module in an existing Node project with `allowJs`; the examples are not a browser SDK.
