Skip to content
Aranisdevelopers
Sign in

Errors & rate limits

One error shape everywhere, with a machine code to branch on.

Error response
{
  "error": {
    "code": "validation_failed",
    "message": "One or more fields are invalid.",
    "request_id": "req_01J8Z9K2M4N6P8Q0R2S4T6V8",
    "details": {
      "fields": {
        "size": "Must be one of: small, medium, large, enterprise.",
        "email": "Required."
      }
    }
  }
}
Branch on code, never on message. Messages are written for humans and will be reworded without notice; codes are part of the contract.

Every code

HTTPCodeMeaningRetry?
400invalid_requestMalformed request — unknown filter value, bad cursor, limit out of range.No
401invalid_api_keyMissing or invalid credentials.No
403plan_upgrade_requiredWorkspace is not on Enterprise.No
403insufficient_scopeKey lacks the scope this endpoint needs. `details.required_scope` names it.No
404resource_not_foundNo such resource in this workspace.No
409conflictConflicts with existing state — duplicate domain, or an Idempotency-Key reused with a different body.No
422validation_failedBody well-formed but a field is invalid. `details.fields` names each.No
429rate_limit_exceededToo many requests. `Retry-After` gives the seconds.Yes
500internal_errorSomething failed on our side.Yes

request_id

Present in the body of every error and as the X-Request-Id header on every response, successful ones included. Log it. It is the first thing support asks for, and it is what lets us find your exact request rather than guessing from a timestamp.

Why a bad plan is 403, not 401

A 401 means "I do not know who you are" and sends an integrator hunting for a key problem. When your key is valid but your workspace is not entitled — wrong plan, missing scope — that is a 403: we know exactly who you are, and the answer is still no.

Conversely, every authentication failure returns the same 401, whether the key is unknown, revoked or expired. Distinguishing them would tell an attacker which keys exist.

Rate limits

Per workspace, not per key — issuing more keys does not raise your ceiling.

WindowLimit
1 second60 requests
1 minute600 requests
1 hour5,000 requests
GET /reports/{id}/pdf60 per hour, on top of the above

Every response carries your current budget:

Response headers
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 59
X-RateLimit-Reset: 1785735662
X-Request-Id: req_01J8Z9K2M4N6P8Q0R2S4T6V8

The reported bucket is whichever is closest to its ceiling, so X-RateLimit-Remaining always reflects the constraint that will actually stop you. On a 429, wait Retry-After seconds. The SDK does this automatically.

Idempotency

Every POST requires an Idempotency-Key header. Replaying the same key with the same body returns the original response and creates nothing new; the same key with a different body is a 409.

curl
curl -X POST https://api.aranis.ai/v1/suppliers \
  -H "Authorization: Bearer $ARANIS_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: crm-vendor-8842" \
  -d '{"name":"Acme Cloud Inc","email":"security@acme.com","domain":"acme.com"}'
Derive the key from the record you are syncing — a CRM id, a row id, a job id. A fresh UUID per call defeats the entire mechanism: the retry after a timeout gets a new key and creates a duplicate, which is exactly what the header exists to prevent.

Keys are retained for 24 hours. PUT /v1/assets takes no key: its natural identity (source, cloud_provider, external_id) already makes repeated calls converge on the same state.

What to retry

Retry 429 and 5xx, with backoff. Do not retry 4xx other than 429 — the request will fail the same way every time, and hammering a 422 just burns your rate limit.

Writes are safe to retry because of Idempotency-Key. That is what makes the header worth requiring rather than merely offering.