Docs menu
Conventions
Every /api/v1 endpoint shares the same rules for auth, errors, rate limits, and pagination. Learn them once here; they hold everywhere.
Authentication
Authenticate with an API key as a Bearer token: Authorization: Bearer dk_.... Keys are never accepted as query parameters. A missing or invalid key returns a uniform 401 unauthorized — the same response whether the key is absent, malformed, or revoked (no oracle). Test keys are prefixed dk_test_ and live keysdk_live_; the prefix decides which quota, mode, and billing a request draws.
curl -sS "$DARB_API_BASE/api/v1/usage" \
-H "Authorization: Bearer $DARB_KEY"The exact security scheme is published in theAPI reference.
Errors
Every non-2xx response is one envelope: a nested error object with a machinecode, a human message, and optional structured details. Thecode is the contract you branch on; the message is advisory and may change without notice.
{
"error": {
"code": "quota_exceeded",
"message": "Monthly route quota exhausted.",
"details": { "metric": "routes", "used": 100, "limit": 100 }
}
}The ErrorEnvelope schema is documented on every operation in theAPI reference — this table lists the discriminators each status can carry:
| Status | code | Meaning |
|---|---|---|
401 | unauthorized | Missing or invalid API key. |
402 | quota_exceeded, subscription_locked, subscription_canceled | Quota or subscription refusal. |
403 | test_mode_unavailable, email_unverified, paid_plan_required, live_key_browser_forbidden | Refused (paid-plan gate, unverified email, or a live key used from a browser). |
404 | not_found | Unknown or foreign resource id — no cross-tenant oracle. |
405 | method_not_allowed | Wrong HTTP method for the path. |
409 | idempotency_key_conflict, webhook_limit_reached | A reused Idempotency-Key with a different body, or the per-tenant webhook cap. |
422 | validation_error | Request failed validation; field errors under details.errors. |
429 | rate_limited | Rate limit exceeded; honest Retry-After + X-RateLimit-* headers. |
500 | internal_error | Unexpected server error; no internals leak. |
503 | service_unavailable | Rate-limit backend unavailable; retry after Retry-After. |
Rate limits
Each API key is limited to 60 requests per minute. Every successful response carries X-RateLimit-Limit and X-RateLimit-Remaining; a throttled request returns 429 rate_limited with an honest Retry-After alongside theX-RateLimit-* headers. A 429 reserves no quota — being rate-limited never spends a route, WhatsApp, or SMS unit (a 429 is not a402).
HTTP/1.1 429 Too Many Requests
Retry-After: 30
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0Asynchronous jobs (202 + poll)
Spend endpoints are asynchronous. POST /api/v1/routes andPOST /api/v1/campaigns return 202 Accepted immediately with anid and an in-progress status (optimizing /collecting). Poll the matching GET until the status reaches a terminal value (optimized / failed for routes;completed / expired for campaigns). There is no callback requirement — though webhooks can push the same completion events.
Quotas & billing
Each spend reserves a unit before the work runs and refunds it on failure. When a metric is exhausted the API returns 402 quota_exceeded withdetails.metric/used/limit; a locked or canceled subscription returns 402 subscription_locked / subscription_canceled. Test-mode and live-mode quotas are entirely separate: a dk_test_ key draws the test caps (100 routes, 200 WhatsApp, 100 SMS per month) and spends no money, while adk_live_ key draws your plan's live quota. Read either throughGET /api/v1/usage — its livemode flag tells you which namespace the numbers describe.
Not found
An unknown id and a resource that belongs to another tenant both return the same404 not_found — the API never distinguishes "does not exist" from "not yours", so a key can't probe for foreign resources. Cross-mode reads follow the same rule: adk_test_ key polling a live route (or vice versa) gets 404 not_found.
Idempotency
Send an Idempotency-Key header on a spend POST to make retries safe. A replay with the same key and body echoes the original resource (with anIdempotent-Replay: true response header) and reserves no new quota; reusing a key with a different body returns 409 idempotency_key_conflict. The key is optional and additive — omit it and the create behaves exactly as before.
Pagination
The public API does not paginate yet. Its only list endpoint,GET /api/v1/webhooks, returns the full set of your subscriptions as a bare JSON array (newest first), bounded by the per-tenant subscription cap — there is nonext_cursor, no cursor query parameter, and no wrapping envelope. Read the array directly.
Should a future list endpoint need paging, it will add keyset ("Load more") paginationadditively — an optional cursor query parameter and anext_cursor field alongside the existing data, under the same additive-onlyv1 promise described below. Until then, don't write cursor-handling code against/api/v1 responses.
Versioning
The API is versioned in the path (/api/v1). Changes within v1 areadditive only — new optional request fields, new response fields, new endpoints — and are enforced by a contract-diff gate on every release, so an integration built againstv1 keeps working. A breaking change would ship under a new version path, never insidev1.