Errors and rate limits
Failed requests return a non-2xx status and a JSON error body with a stable code. Branch your code on the HTTP status and the code, never on the message text.
The error format
Section titled “The error format”Every error has the same shape. code is a stable machine-readable string, message is written for humans in the language of the Accept-Language header (Slovak for sk or cs, otherwise English), and detail is optional extra data such as per-field validation errors or the name of the offending parameter.
{ "error": { "code": "INVALID_PARAMETER", "message": "limit must be between 1 and 100", "detail": { "parameter": "limit" } }}Error codes by HTTP status
Section titled “Error codes by HTTP status”| Status | Codes | Meaning |
|---|---|---|
| 400 | BAD_REQUEST, INVALID_PARAMETER, INVALID_CURSOR, UNSUPPORTED_FILTER | The request is malformed. Fix it before retrying. |
| 401 | UNAUTHORIZED | Key missing, invalid, expired or revoked. |
| 402 | PAYMENT_REQUIRED | The organization subscription is not active. |
| 403 | FORBIDDEN, LEGACY_KEY_NOT_ALLOWED | Missing scope, or a legacy key on a v1-only endpoint. |
| 404 | NOT_FOUND | The resource does not exist in your organization. |
| 409 | CONFLICT, IDEMPOTENCY_IN_PROGRESS, LIMIT_EXCEEDED | State conflict, a request with the same idempotency key is still running, or a plan limit was reached. |
| 422 | VALIDATION_ERROR, IDEMPOTENCY_KEY_REUSED, TOKEN_OWNER_INACTIVE, WEBHOOK_URL_INVALID, WEBHOOK_URL_BLOCKED, UNKNOWN_EVENT | The request is well formed but cannot be processed. |
| 429 | RATE_LIMITED | Too many requests. Wait for Retry-After. |
Which errors to retry
Section titled “Which errors to retry”Retry 429 after the time in Retry-After, and retry 5xx responses and network errors with exponential backoff. Do not retry other 4xx errors unchanged, because the same request will fail the same way. A 409 IDEMPOTENCY_IN_PROGRESS is safe to retry after a short pause. Always send an Idempotency-Key on POST requests you retry; see Idempotency.
Rate limit rules
Section titled “Rate limit rules”Limits are counted per minute, once per API key and once per organization. The defaults are 120 requests per minute for a key and 600 for the whole organization, and they can differ per organization, so read the real values from limits.requests_per_minute and limits.org_requests_per_minute in GET /me. Limits are never keyed by IP address, so several organizations behind one egress address do not throttle each other.
Rate limit headers
Section titled “Rate limit headers”Every response carries X-RateLimit-Limit (the ceiling of the tightest bucket), X-RateLimit-Remaining (requests left in the window) and X-RateLimit-Reset (seconds until the window resets). A 429 response adds Retry-After with the number of seconds to wait. Slow down before you reach zero by checking X-RateLimit-Remaining in a long-running job.
Retry loop in Node.js
Section titled “Retry loop in Node.js”This helper honours Retry-After on 429, backs off exponentially on 5xx and network errors, and gives up after five attempts. It needs Node.js 18 or newer.
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
export async function dakoty(path, init = {}, attempts = 5) { const url = `https://api.dakoty.com/api/v1/public${path}`; const headers = { Authorization: `Bearer ${process.env.DAKOTY_API_KEY}`, ...init.headers }; for (let i = 0; i < attempts; i++) { let res; try { res = await fetch(url, { ...init, headers }); } catch (err) { if (i === attempts - 1) throw err; await sleep(2 ** i * 500); continue; } if (res.status === 429 || res.status >= 500) { if (i === attempts - 1) return res; const wait = Number(res.headers.get("retry-after")) * 1000 || 2 ** i * 500; await sleep(wait); continue; } return res; }}Retry loop in Python
Section titled “Retry loop in Python”The same policy with requests. It re-sends the same Idempotency-Key header if you set it in headers, which is what makes retrying a POST safe.
import osimport timeimport requests
BASE = "https://api.dakoty.com/api/v1/public"
def dakoty(method, path, attempts=5, **kwargs): headers = {"Authorization": f"Bearer {os.environ['DAKOTY_API_KEY']}", **kwargs.pop("headers", {})} for i in range(attempts): try: r = requests.request(method, BASE + path, headers=headers, timeout=30, **kwargs) except requests.RequestException: if i == attempts - 1: raise time.sleep(2**i * 0.5) continue if (r.status_code == 429 or r.status_code >= 500) and i < attempts - 1: time.sleep(float(r.headers.get("Retry-After") or 2**i * 0.5)) continue return r