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 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
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
result = api(token, 'GET', '/api/v1/me')
List accessible companies
GET /api/v1/accounts · scope account:read
result = api(token, 'GET', '/api/v1/accounts')
Check capabilities and scopes
GET /api/v1/accounts/123/capabilities · scope account:read
result = api(token, 'GET', '/api/v1/accounts/123/capabilities')
Read company details and their ETag
GET /api/v1/accounts/123/profile · scope account:read
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.
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
result = api(token, 'GET', '/api/v1/accounts/123/products')
List existing domain holders
GET /api/v1/accounts/123/holders · scope holders:read
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.
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.
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.
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.
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
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.
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
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.
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
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
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
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.
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.
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.
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.
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
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
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
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
result = api(token, 'GET', '/api/v1/accounts/123/workspaces')
List mailboxes
GET /api/v1/accounts/123/workspaces/7/mailboxes · scope workspace.mailboxes:read
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
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.
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.
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.
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.
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.
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.
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.
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
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.
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
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.
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.
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
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
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.
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.
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.
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.
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.
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
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
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.