Errors & rate limits
One error shape everywhere, with a machine code to branch on.
{
"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."
}
}
}
}code, never on message. Messages are written for humans and will be reworded without notice; codes are part of the contract.Every code
| HTTP | Code | Meaning | Retry? |
|---|---|---|---|
400 | invalid_request | Malformed request — unknown filter value, bad cursor, limit out of range. | No |
401 | invalid_api_key | Missing or invalid credentials. | No |
403 | plan_upgrade_required | Workspace is not on Enterprise. | No |
403 | insufficient_scope | Key lacks the scope this endpoint needs. `details.required_scope` names it. | No |
404 | resource_not_found | No such resource in this workspace. | No |
409 | conflict | Conflicts with existing state — duplicate domain, or an Idempotency-Key reused with a different body. | No |
422 | validation_failed | Body well-formed but a field is invalid. `details.fields` names each. | No |
429 | rate_limit_exceeded | Too many requests. `Retry-After` gives the seconds. | Yes |
500 | internal_error | Something 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.
| Window | Limit |
|---|---|
| 1 second | 60 requests |
| 1 minute | 600 requests |
| 1 hour | 5,000 requests |
GET /reports/{id}/pdf | 60 per hour, on top of the above |
Every response carries your current budget:
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 59
X-RateLimit-Reset: 1785735662
X-Request-Id: req_01J8Z9K2M4N6P8Q0R2S4T6V8The 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 -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"}'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.