Skip to documentation

Exchange will be rebuilt separately and is unavailable through the API or CLI. Availability in this release

API examples with Python

Workflow guides

Markdown

On this page

All languages and safety rules · API reference

Python 3.11+, standard library only.

1. Reusable client

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

Python
"""Python 3.11+, standard library only. No network calls when imported."""
import json
import hashlib
import hmac
import os
import re
import time
import urllib.error
import urllib.parse
import urllib.request

ORIGIN = "https://my.binadit.com"
LIMIT = 8 * 1024 * 1024


class NoRedirect(urllib.request.HTTPRedirectHandler):
    def redirect_request(self, req, fp, code, msg, headers, newurl):
        return None


def send(method, path, headers, content=None):
    if not (path.startswith("/api/v1/") or path == "/oauth/token") or "\\" in path or "#" in path:
        raise ValueError("Use a relative Binadit API path")
    request = urllib.request.Request(ORIGIN + path, content, headers, method=method)
    try:
        response = urllib.request.build_opener(NoRedirect()).open(request, timeout=30)
    except urllib.error.HTTPError as error:
        response = error
    except (OSError, urllib.error.URLError):
        raise RuntimeError("Transport outcome unknown; preserve the original request") from None
    with response:
        raw = response.read(LIMIT + 1)
        if len(raw) > LIMIT:
            raise RuntimeError("Response exceeds the example's 8 MiB limit")
        if not 200 <= response.status < 300:
            raise RuntimeError(f"API HTTP {response.status}; inspect the original operation")
        return response.headers, raw


def access_token():
    token = os.environ.get("BINADIT_ACCESS_TOKEN")
    if token:
        return token
    body = urllib.parse.urlencode({"grant_type": "client_credentials",
        "client_id": os.environ["BINADIT_CLIENT_ID"],
        "client_secret": os.environ["BINADIT_CLIENT_SECRET"]}).encode()
    _, raw = send("POST", "/oauth/token", {"Accept": "application/json",
        "Content-Type": "application/x-www-form-urlencoded"}, body)
    token = json.loads(raw)["access_token"]
    if not isinstance(token, str) or not token:
        raise RuntimeError("Token response is invalid")
    return token


def api(token, method, path, body=None, *, version=None, key=None, binary=False):
    if not path.startswith("/api/v1/") or method not in {"GET", "POST", "PUT", "PATCH", "DELETE"}:
        raise ValueError("Use a supported API method and path")
    headers = {"Authorization": f"Bearer {token}", "Accept": "application/pdf" if binary else "application/json"}
    if method != "GET":
        if not key or not re.fullmatch(r"[A-Za-z0-9_.:-]{16,128}", key):
            raise ValueError("Supply the persisted original Idempotency-Key")
        headers["Idempotency-Key"] = key
    if version:
        headers["If-Match"] = version
    encoded = None if body is None else json.dumps(body, ensure_ascii=False).encode()
    if encoded is not None:
        headers["Content-Type"] = "application/json"
    response, raw = send(method, path, headers, encoded)
    if binary and response.get_content_type() != "application/pdf":
        raise RuntimeError("Expected an invoice PDF")
    return {"etag": response.get("ETag"), "data": raw if binary else (json.loads(raw) if raw else None)}


def verify_webhook(raw, delivery_id, timestamp, signature, expected_key_id, secret, now=None):
    """Signature/time check only; separately deduplicate in a durable transaction."""
    uuid = r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"
    if (len(raw) > 1024 * 1024 or len(secret) < 32
            or not re.fullmatch(uuid, delivery_id) or not re.fullmatch(uuid, expected_key_id)
            or not re.fullmatch(r"[1-9][0-9]{8,10}", timestamp)):
        return False
    if abs((time.time() if now is None else now) - int(timestamp)) > 300:
        return False
    digest = hmac.new(secret.encode(), timestamp.encode() + b"." + delivery_id.encode() + b"." + raw, hashlib.sha256).hexdigest()
    expected = f"v1={digest},key_id={expected_key_id}"
    return signature.isascii() and hmac.compare_digest(expected, signature)

2. Once for this workflow

Python
from binadit import access_token, api
import os
token = access_token()

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

Python
result = api(token, 'GET', '/api/v1/me')

List accessible companies

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

Python
result = api(token, 'GET', '/api/v1/accounts')

Check capabilities and scopes

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

Python
result = api(token, 'GET', '/api/v1/accounts/123/capabilities')

Read company details and their ETag

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

Python
result = 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.

Python
result = api(token, 'PATCH', '/api/v1/accounts/123/profile', {'city': 'Utrecht', 'dry_run': True}, version=os.environ["BINADIT_RESOURCE_VERSION"], key=os.environ["BINADIT_REQUEST_KEY"])

Read products and prices

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

Python
result = api(token, 'GET', '/api/v1/accounts/123/products')

List existing domain holders

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

Python
result = 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.

Python
result = api(token, 'POST', '/api/v1/accounts/123/holders/default', {'dry_run': True}, key=os.environ["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.

Python
result = 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=os.environ["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.

Python
result = api(token, 'POST', '/api/v1/accounts/123/domains/check', {'domains': ['example.test']}, key=os.environ["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.

Python
result = 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=os.environ["BINADIT_REQUEST_KEY"])

Read a saved quote again

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

Python
result = 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.

Python
result = api(token, 'POST', '/api/v1/accounts/123/orders', {'quote_id': '11111111-1111-4111-8111-111111111111', 'dry_run': True}, key=os.environ["BINADIT_REQUEST_KEY"])

Read registration renewal and subscription status

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

Python
result = 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.

Python
result = api(token, 'POST', '/api/v1/accounts/123/domains/31/actions/cancel', {'confirm_domain': 'example.test', 'dry_run': True}, version=os.environ["BINADIT_RESOURCE_VERSION"], key=os.environ["BINADIT_REQUEST_KEY"])

List DNS zones

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

Python
result = 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

Python
result = 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

Python
result = 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.

Python
result = 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=os.environ["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.

Python
result = api(token, 'POST', '/api/v1/accounts/123/dns/zones/8/template-plans', {'template_id': 11, 'dry_run': True}, key=os.environ["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.

Python
result = 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=os.environ["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.

Python
result = api(token, 'POST', '/api/v1/accounts/123/dns/change-plans/11111111-1111-4111-8111-111111111111/apply', {'dry_run': True}, version=os.environ["BINADIT_RESOURCE_VERSION"], key=os.environ["BINADIT_REQUEST_KEY"])

Follow the original operation

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

Python
result = 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

Python
result = 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

Python
result = 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

Python
result = api(token, 'GET', '/api/v1/accounts/123/workspaces')

List mailboxes

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

Python
result = 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

Python
result = 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.

Python
result = 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=os.environ["BINADIT_RESOURCE_VERSION"], key=os.environ["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.

Python
result = 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=os.environ["BINADIT_RESOURCE_VERSION"], key=os.environ["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.

Python
result = api(token, 'PUT', '/api/v1/accounts/123/workspaces/7/mailing-lists/10/members/mailbox-24', {'dry_run': True}, version=os.environ["BINADIT_RESOURCE_VERSION"], key=os.environ["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.

Python
result = api(token, 'DELETE', '/api/v1/accounts/123/workspaces/7/mailing-lists/10/members/mailbox-24', {'dry_run': True}, version=os.environ["BINADIT_RESOURCE_VERSION"], key=os.environ["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.

Python
result = 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=os.environ["BINADIT_RESOURCE_VERSION"], key=os.environ["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.

Python
result = api(token, 'PUT', '/api/v1/accounts/123/workspaces/7/rooms/11/managers/24', {'dry_run': True}, version=os.environ["BINADIT_RESOURCE_VERSION"], key=os.environ["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.

Python
result = api(token, 'DELETE', '/api/v1/accounts/123/workspaces/7/rooms/11', {'confirm_address': 'vergaderzaal@example.test', 'dry_run': True}, version=os.environ["BINADIT_RESOURCE_VERSION"], key=os.environ["BINADIT_REQUEST_KEY"])

List hosting subscriptions

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

Python
result = 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.

Python
result = api(token, 'POST', '/api/v1/accounts/123/hosting/subscriptions/41/actions/cancel', {'intent_id': '33333333-3333-4333-8333-333333333333', 'dry_run': True}, version=os.environ["BINADIT_RESOURCE_VERSION"], key=os.environ["BINADIT_REQUEST_KEY"])

List spam filters

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

Python
result = 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.

Python
result = 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=os.environ["BINADIT_RESOURCE_VERSION"], key=os.environ["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.

Python
result = 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=os.environ["BINADIT_REQUEST_KEY"])

Inspect webhook deliveries

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

Python
result = 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

Python
result = 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.

Python
result = 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=os.environ["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.

Python
payload = {'domain_id': 12, 'product_id': 9, 'local_part': 'support', 'display_name': 'Support', 'member_ids': [23, 24], 'dry_run': True}
payload['product_version'] = os.environ['BINADIT_PRODUCT_VERSION']
result = api(token, 'POST', '/api/v1/accounts/123/workspaces/7/shared-mailboxes', payload, version=os.environ["BINADIT_RESOURCE_VERSION"], key=os.environ["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.

Python
result = 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=os.environ["BINADIT_RESOURCE_VERSION"], key=os.environ["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.

Python
result = api(token, 'DELETE', '/api/v1/accounts/123/workspaces/7/mailboxes/23/delegates/24', {'dry_run': True}, version=os.environ["BINADIT_RESOURCE_VERSION"], key=os.environ["BINADIT_REQUEST_KEY"])

Further workflows

Paginate without skipping resources

Read meta.next_cursor even when the filtered data array is empty. Do not follow an arbitrary next URL with your Bearer token. The loop below fails explicitly if it reaches its page limit.

Python
import urllib.parse
cursor = None
seen = set()
for page in range(20):
    path = "/api/v1/accounts/123/domains?limit=25"
    if cursor is not None:
        path += "&cursor=" + urllib.parse.quote(str(cursor), safe="")
    result = api(token, "GET", path)
    for domain in result["data"]["data"]:
        print(domain["id"])  # Only the chosen safe field.
    cursor = result["data"].get("meta", {}).get("next_cursor")
    if cursor is None:
        break
    if str(cursor) in seen:
        raise RuntimeError("Repeated pagination cursor")
    seen.add(str(cursor))
else:
    raise RuntimeError("Page limit reached; explicitly continue from the saved cursor")

Save a PDF without overwriting an existing file

Python
import os
result = api(token, "GET", "/api/v1/accounts/123/billing/invoices/45/pdf", binary=True)
fd = os.open("factuur-45.pdf", os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
with os.fdopen(fd, "wb") as output:
    output.write(result["data"])

On Windows, use a directory whose ACL grants access only to the intended user. An error does not authorise overwriting an existing file.

Verify a webhook signature

Python
from binadit import verify_webhook
# Obtain raw bytes and the headers from your server framework before parsing JSON.
valid = verify_webhook(raw_body, delivery_id_header, timestamp_header, signature_header,
                       os.environ["BINADIT_WEBHOOK_KEY_ID"], os.environ["BINADIT_WEBHOOK_SECRET"])
if not valid:
    raise ValueError("Reject this webhook")
# Next: atomically insert the delivery ID (UNIQUE) and enqueue the work in your DB.

Configure an appropriate body limit and check for missing or duplicate headers in your framework. The function rejects bodies larger than 1 MiB, an invalid time window, an unknown key ID and an incorrect HMAC. It does not prevent replay itself: insert the unique delivery ID and work item in the same durable transaction before returning 2xx. Read the rules shared by all languages.