# Binadit API with PHP

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

PHP 8.2+ with the cURL extension.

## 1. Reusable client

Save this as `binadit.php`. 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.

```php
<?php

/** PHP 8.2+ with cURL. No requests or secret output when included. */
final class BinaditExample
{
    private const ORIGIN = 'https://my.binadit.com';

    private static function send(string $method, string $path, array $headers, ?string $body = null): array
    {
        if ((! str_starts_with($path, '/api/v1/') && $path !== '/oauth/token') || strpbrk($path, '\\#') !== false) {
            throw new RuntimeException('Use a relative Binadit API path');
        }
        $responseHeaders = [];
        $raw = '';
        $curl = curl_init(self::ORIGIN.$path);
        curl_setopt_array($curl, [CURLOPT_CUSTOMREQUEST => $method, CURLOPT_HTTPHEADER => $headers,
            CURLOPT_FOLLOWLOCATION => false, CURLOPT_PROTOCOLS => CURLPROTO_HTTPS,
            CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2,
            CURLOPT_CONNECTTIMEOUT => 10, CURLOPT_TIMEOUT => 30,
            CURLOPT_HEADERFUNCTION => static function ($ch, $line) use (&$responseHeaders) {
                if (str_contains($line, ':')) {
                    [$k, $v] = explode(':', $line, 2);
                    $responseHeaders[strtolower(trim($k))] = trim($v);
                }

                return strlen($line);
            },
            CURLOPT_WRITEFUNCTION => static function ($ch, $part) use (&$raw) {
                if (strlen($raw) + strlen($part) > 8 * 1024 * 1024) {
                    return 0;
                }
                $raw .= $part;

                return strlen($part);
            }]);
        if ($body !== null) {
            curl_setopt($curl, CURLOPT_POSTFIELDS, $body);
        }
        $ok = curl_exec($curl);
        $status = curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
        curl_close($curl);
        if ($ok === false) {
            throw new RuntimeException('Transport outcome unknown or response too large; preserve the original request');
        }
        if ($status < 200 || $status >= 300) {
            throw new RuntimeException('API HTTP '.$status.'; inspect the original operation');
        }

        return [$responseHeaders, $raw];
    }

    public static function accessToken(): string
    {
        if ($token = getenv('BINADIT_ACCESS_TOKEN')) {
            return $token;
        }
        $id = getenv('BINADIT_CLIENT_ID');
        $secret = getenv('BINADIT_CLIENT_SECRET');
        if (! $id || ! $secret) {
            throw new RuntimeException('Supply service credentials from your secret manager');
        }
        [, $raw] = self::send('POST', '/oauth/token', ['Accept: application/json', 'Content-Type: application/x-www-form-urlencoded'],
            http_build_query(['grant_type' => 'client_credentials', 'client_id' => $id, 'client_secret' => $secret]));
        $token = json_decode($raw, true, 512, JSON_THROW_ON_ERROR)['access_token'] ?? null;
        if (! is_string($token) || $token === '') {
            throw new RuntimeException('Token response is invalid');
        }

        return $token;
    }

    public static function api(string $token, string $method, string $path, ?array $body = null, ?string $version = null, ?string $key = null, bool $binary = false): array
    {
        if (! str_starts_with($path, '/api/v1/') || ! in_array($method, ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'], true)) {
            throw new RuntimeException('Use a supported API method and path');
        }
        $headers = ['Authorization: Bearer '.$token, 'Accept: '.($binary ? 'application/pdf' : 'application/json')];
        if ($method !== 'GET') {
            if (! $key || ! preg_match('/\A[A-Za-z0-9_.:-]{16,128}\z/D', $key)) {
                throw new RuntimeException('Supply the persisted original Idempotency-Key');
            }
            $headers[] = 'Idempotency-Key: '.$key;
        }
        if ($version !== null) {
            $headers[] = 'If-Match: '.$version;
        }
        if ($body !== null) {
            $headers[] = 'Content-Type: application/json';
        }
        // Empty request objects are JSON {}, never the PHP empty-array encoding [].
        [$headers, $raw] = self::send($method, $path, $headers, $body === null ? null : json_encode($body === [] ? (object) [] : $body, JSON_THROW_ON_ERROR));
        if ($binary && explode(';', $headers['content-type'] ?? '')[0] !== 'application/pdf') {
            throw new RuntimeException('Expected an invoice PDF');
        }

        return ['etag' => $headers['etag'] ?? null, 'data' => $binary ? $raw : ($raw === '' ? null : json_decode($raw, true, 512, JSON_THROW_ON_ERROR))];
    }

    public static function verifyWebhook(string $raw, string $deliveryId, string $timestamp, string $signature, string $expectedKeyId, string $secret, ?int $now = null): bool
    {
        $uuid = '/\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/D';
        if (strlen($raw) > 1024 * 1024 || strlen($secret) < 32 || ! preg_match($uuid, $deliveryId) || ! preg_match($uuid, $expectedKeyId)
            || ! preg_match('/\A[1-9][0-9]{8,10}\z/D', $timestamp) || abs(($now ?? time()) - (int) $timestamp) > 300) {
            return false;
        }
        $digest = hash_hmac('sha256', $timestamp.'.'.$deliveryId.'.'.$raw, $secret);

        return hash_equals('v1='.$digest.',key_id='.$expectedKeyId, $signature);
    }
}
```

## 2. Once for this workflow

```php
require 'binadit.php';
$token = BinaditExample::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`

```php
$result = BinaditExample::api($token, 'GET', '/api/v1/me');
```

## List accessible companies

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

```php
$result = BinaditExample::api($token, 'GET', '/api/v1/accounts');
```

## Check capabilities and scopes

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

```php
$result = BinaditExample::api($token, 'GET', '/api/v1/accounts/123/capabilities');
```

## Read company details and their ETag

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

```php
$result = BinaditExample::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.

```php
$result = BinaditExample::api($token, 'PATCH', '/api/v1/accounts/123/profile', ['city' => 'Utrecht', 'dry_run' => true], version: getenv('BINADIT_RESOURCE_VERSION') ?: throw new RuntimeException('Read the current ETag first'), key: getenv('BINADIT_REQUEST_KEY') ?: null);
```

## Read products and prices

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

```php
$result = BinaditExample::api($token, 'GET', '/api/v1/accounts/123/products');
```

## List existing domain holders

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

```php
$result = BinaditExample::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.

```php
$result = BinaditExample::api($token, 'POST', '/api/v1/accounts/123/holders/default', ['dry_run' => true], key: getenv('BINADIT_REQUEST_KEY') ?: null);
```

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

```php
$result = BinaditExample::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: getenv('BINADIT_REQUEST_KEY') ?: null);
```

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

```php
$result = BinaditExample::api($token, 'POST', '/api/v1/accounts/123/domains/check', ['domains' => ['example.test']], key: getenv('BINADIT_REQUEST_KEY') ?: null);
```

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

```php
$result = BinaditExample::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: getenv('BINADIT_REQUEST_KEY') ?: null);
```

## Read a saved quote again

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

```php
$result = BinaditExample::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.

```php
$result = BinaditExample::api($token, 'POST', '/api/v1/accounts/123/orders', ['quote_id' => '11111111-1111-4111-8111-111111111111', 'dry_run' => true], key: getenv('BINADIT_REQUEST_KEY') ?: null);
```

## Read registration renewal and subscription status

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

```php
$result = BinaditExample::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.

```php
$result = BinaditExample::api($token, 'POST', '/api/v1/accounts/123/domains/31/actions/cancel', ['confirm_domain' => 'example.test', 'dry_run' => true], version: getenv('BINADIT_RESOURCE_VERSION') ?: throw new RuntimeException('Read the current ETag first'), key: getenv('BINADIT_REQUEST_KEY') ?: null);
```

## List DNS zones

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

```php
$result = BinaditExample::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`

```php
$result = BinaditExample::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`

```php
$result = BinaditExample::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.

```php
$result = BinaditExample::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: getenv('BINADIT_REQUEST_KEY') ?: null);
```

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

```php
$result = BinaditExample::api($token, 'POST', '/api/v1/accounts/123/dns/zones/8/template-plans', ['template_id' => 11, 'dry_run' => true], key: getenv('BINADIT_REQUEST_KEY') ?: null);
```

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

```php
$result = BinaditExample::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: getenv('BINADIT_REQUEST_KEY') ?: null);
```

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

```php
$result = BinaditExample::api($token, 'POST', '/api/v1/accounts/123/dns/change-plans/11111111-1111-4111-8111-111111111111/apply', ['dry_run' => true], version: getenv('BINADIT_RESOURCE_VERSION') ?: throw new RuntimeException('Read the current ETag first'), key: getenv('BINADIT_REQUEST_KEY') ?: null);
```

## Follow the original operation

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

```php
$result = BinaditExample::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`

```php
$result = BinaditExample::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`

```php
$result = BinaditExample::api($token, 'GET', '/api/v1/accounts/123/billing/invoices/45/pdf', 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`

```php
$result = BinaditExample::api($token, 'GET', '/api/v1/accounts/123/workspaces');
```

## List mailboxes

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

```php
$result = BinaditExample::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`

```php
$result = BinaditExample::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.

```php
$result = BinaditExample::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: getenv('BINADIT_RESOURCE_VERSION') ?: throw new RuntimeException('Read the current ETag first'), key: getenv('BINADIT_REQUEST_KEY') ?: null);
```

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

```php
$result = BinaditExample::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: getenv('BINADIT_RESOURCE_VERSION') ?: throw new RuntimeException('Read the current ETag first'), key: getenv('BINADIT_REQUEST_KEY') ?: null);
```

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

```php
$result = BinaditExample::api($token, 'PUT', '/api/v1/accounts/123/workspaces/7/mailing-lists/10/members/mailbox-24', ['dry_run' => true], version: getenv('BINADIT_RESOURCE_VERSION') ?: throw new RuntimeException('Read the current ETag first'), key: getenv('BINADIT_REQUEST_KEY') ?: null);
```

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

```php
$result = BinaditExample::api($token, 'DELETE', '/api/v1/accounts/123/workspaces/7/mailing-lists/10/members/mailbox-24', ['dry_run' => true], version: getenv('BINADIT_RESOURCE_VERSION') ?: throw new RuntimeException('Read the current ETag first'), key: getenv('BINADIT_REQUEST_KEY') ?: null);
```

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

```php
$result = BinaditExample::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: getenv('BINADIT_RESOURCE_VERSION') ?: throw new RuntimeException('Read the current ETag first'), key: getenv('BINADIT_REQUEST_KEY') ?: null);
```

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

```php
$result = BinaditExample::api($token, 'PUT', '/api/v1/accounts/123/workspaces/7/rooms/11/managers/24', ['dry_run' => true], version: getenv('BINADIT_RESOURCE_VERSION') ?: throw new RuntimeException('Read the current ETag first'), key: getenv('BINADIT_REQUEST_KEY') ?: null);
```

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

```php
$result = BinaditExample::api($token, 'DELETE', '/api/v1/accounts/123/workspaces/7/rooms/11', ['confirm_address' => 'vergaderzaal@example.test', 'dry_run' => true], version: getenv('BINADIT_RESOURCE_VERSION') ?: throw new RuntimeException('Read the current ETag first'), key: getenv('BINADIT_REQUEST_KEY') ?: null);
```

## List hosting subscriptions

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

```php
$result = BinaditExample::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.

```php
$result = BinaditExample::api($token, 'POST', '/api/v1/accounts/123/hosting/subscriptions/41/actions/cancel', ['intent_id' => '33333333-3333-4333-8333-333333333333', 'dry_run' => true], version: getenv('BINADIT_RESOURCE_VERSION') ?: throw new RuntimeException('Read the current ETag first'), key: getenv('BINADIT_REQUEST_KEY') ?: null);
```

## List spam filters

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

```php
$result = BinaditExample::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.

```php
$result = BinaditExample::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: getenv('BINADIT_RESOURCE_VERSION') ?: throw new RuntimeException('Read the current ETag first'), key: getenv('BINADIT_REQUEST_KEY') ?: null);
```

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

```php
$result = BinaditExample::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: getenv('BINADIT_REQUEST_KEY') ?: null);
```

## Inspect webhook deliveries

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

```php
$result = BinaditExample::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`

```php
$result = BinaditExample::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.

```php
$result = BinaditExample::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: getenv('BINADIT_REQUEST_KEY') ?: null);
```

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

```php
$payload = ['domain_id' => 12, 'product_id' => 9, 'local_part' => 'support', 'display_name' => 'Support', 'member_ids' => [23, 24], 'dry_run' => true];
$payload['product_version'] = getenv('BINADIT_PRODUCT_VERSION') ?: throw new RuntimeException('Read the product version first');
$result = BinaditExample::api($token, 'POST', '/api/v1/accounts/123/workspaces/7/shared-mailboxes', $payload, version: getenv('BINADIT_RESOURCE_VERSION') ?: throw new RuntimeException('Read the current ETag first'), key: getenv('BINADIT_REQUEST_KEY') ?: null);
```

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

```php
$result = BinaditExample::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: getenv('BINADIT_RESOURCE_VERSION') ?: throw new RuntimeException('Read the current ETag first'), key: getenv('BINADIT_REQUEST_KEY') ?: null);
```

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

```php
$result = BinaditExample::api($token, 'DELETE', '/api/v1/accounts/123/workspaces/7/mailboxes/23/delegates/24', ['dry_run' => true], version: getenv('BINADIT_RESOURCE_VERSION') ?: throw new RuntimeException('Read the current ETag first'), key: getenv('BINADIT_REQUEST_KEY') ?: null);
```

## Further workflows

### Paginate without skipping resources

```php
$cursor = null;
$seen = [];
for ($page = 0; $page < 20; $page++) {
    $query = ['limit' => 25];
    if ($cursor !== null) $query['cursor'] = $cursor;
    $result = BinaditExample::api($token, 'GET', '/api/v1/accounts/123/domains?'.http_build_query($query));
    foreach ($result['data']['data'] as $domain) echo $domain['id'].PHP_EOL;
    $cursor = $result['data']['meta']['next_cursor'] ?? null;
    if ($cursor === null) break;
    if (isset($seen[(string)$cursor])) throw new RuntimeException('Repeated pagination cursor');
    $seen[(string)$cursor] = true;
    if ($page === 19) throw new RuntimeException('Page limit reached; explicitly continue from the saved cursor');
}
```

An empty `data` array does not prove that there is no next page. Use the cursor, not an arbitrary external next-page URL.

### Save a PDF in a new private file

```php
$result = BinaditExample::api($token, 'GET', '/api/v1/accounts/123/billing/invoices/45/pdf', binary: true);
$previousMask = umask(0077);
try { $file = fopen('factuur-45.pdf', 'xb'); } finally { umask($previousMask); }
if ($file === false) throw new RuntimeException('Choose a new private file');
try {
    $offset = 0;
    while ($offset < strlen($result['data'])) {
        $written = fwrite($file, substr($result['data'], $offset));
        if (!$written) throw new RuntimeException('PDF write failed');
        $offset += $written;
    }
} finally { fclose($file); }
```

On Windows, use a directory with suitable private ACLs. After a partial failure, leave the original new file for review; do not automatically overwrite it with a later response.

### Verify a webhook signature

```php
$raw = file_get_contents('php://input');  // Never re-encode parsed JSON for HMAC.
$valid = BinaditExample::verifyWebhook($raw, $deliveryIdHeader, $timestampHeader, $signatureHeader,
    getenv('BINADIT_WEBHOOK_KEY_ID') ?: '', getenv('BINADIT_WEBHOOK_SECRET') ?: '');
if (!$valid) { http_response_code(401); exit; }
// Next: atomically insert the delivery ID (UNIQUE) and enqueue work in your DB.
// Return 2xx only after that transaction commits; a valid durable duplicate gets 2xx too.
```

Read and validate the three headers in your own framework, including missing and duplicate headers, and set a body limit. The function verifies time and HMAC; the database constraint prevents replay. Do not assume that a valid signature means the work has not already been performed.
