Errors & Rate Limits
ACP uses standard HTTP status codes and returns structured JSON error bodies. Rate limits protect the service from abuse and ensure fair access for all clients.
HTTP Status Codes
All endpoints return standard HTTP status codes. Successful responses use 200 or 202. Error responses include a JSON body with additional context.
| Code | Status | Meaning |
|---|---|---|
200 | OK | Request succeeded. Response body contains the result. |
202 | Accepted | Async task created. Poll the returned ID for the result. |
400 | Bad Request | Missing or invalid parameters. Check the error message for specifics. |
401 | Unauthorized | Invalid or missing API key. Provide a valid key via x-api-key or Authorization header. |
404 | Not Found | The requested resource (e.g., consensus task ID) does not exist. |
429 | Too Many Requests | Rate limit exceeded. Wait and retry after the Retry-After period. |
500 | Internal Server Error | Unexpected server error. If persistent, check /health and report the issue. |
503 | Service Unavailable | Vectorize or an upstream LLM provider is temporarily unavailable. |
Error Response Structure
All error responses follow a consistent JSON structure. The error field contains a machine-readable error type, and message provides a human-readable description.
{
"error": "bad_request",
"message": "Missing required field: query",
"status": 400
}Error Fields
| Field | Type | Description |
|---|---|---|
error | string | Machine-readable error type (e.g., "bad_request", "unauthorized", "rate_limited", "internal_error") |
message | string | Human-readable description of the error |
status | integer | HTTP status code (mirrors the response status) |
Error Examples
400 Bad Request
Returned when the request is malformed or missing required fields.
{
"error": "bad_request",
"message": "Missing required field: query",
"status": 400
}401 Unauthorized
Returned when the API key is invalid, expired, or not provided.
{
"error": "unauthorized",
"message": "Invalid API key",
"status": 401
}429 Too Many Requests
Returned when the client exceeds the rate limit. The response includes a Retry-After header indicating how many seconds to wait.
{
"error": "rate_limited",
"message": "Rate limit exceeded. Retry after 12 seconds.",
"status": 429
}500 Internal Server Error
Returned for unexpected server failures. These are typically transient and can be retried.
{
"error": "internal_error",
"message": "An unexpected error occurred. Please try again.",
"status": 500
}Rate Limits
Rate limits are enforced per IP address and per API key. Exceeding these limits results in a 429 response.
| Limit | Value | Scope |
|---|---|---|
| Per-minute limit | 100 requests / minute | Per IP address |
| Per-hour limit | 1,000 requests / hour | Per API key |
| Burst limit | 10 requests / second | Per IP address |
Rate limit headers
Every response includes rate-limit headers so your client can proactively throttle:
X-RateLimit-Limit -- Maximum requests in the current window.
X-RateLimit-Remaining -- Requests remaining in the current window.
X-RateLimit-Reset -- Unix timestamp when the window resets.
Retry-After -- Seconds to wait (only on 429 responses).
Retry Strategy
When you receive a rate-limit or transient error, use exponential backoff with jitter to avoid thundering herd problems.
| Status Code | Retryable | Strategy |
|---|---|---|
400 | No | Fix the request and resend. Do not retry the same payload. |
401 | No | Check your API key. Do not retry without fixing credentials. |
429 | Yes | Wait for the Retry-After duration, then retry. |
500 | Yes | Retry with exponential backoff (1s, 2s, 4s, ...). Max 3 retries. |
503 | Yes | Upstream is down. Retry after 5-10 seconds. Check /health. |
import time
import random
import requests
def request_with_backoff(url, payload, max_retries=3):
for attempt in range(max_retries):
response = requests.post(url, json=payload)
if response.status_code == 200:
return response.json()
if response.status_code == 429:
retry_after = int(response.headers.get("Retry-After", 10))
time.sleep(retry_after)
continue
if response.status_code >= 500:
wait = (2 ** attempt) + random.uniform(0, 1)
time.sleep(wait)
continue
# Non-retryable error
response.raise_for_status()
raise Exception("Max retries exceeded")Consensus endpoint latency
The /consensus-iterative and /consensus/sync endpoints can take 10-60 seconds to complete due to multiple LLM round-trips. Do not confuse a slow response with a timeout or error. Set your HTTP client timeout to at least 120 seconds for consensus requests.