# Errors & Rate Limits

Error response shapes, the full HTTP status code table, rate limit behavior, and a retry strategy that works with Atlas Cloud.

Source: https://atlascloud.ai/docs/en/errors

## Error response shapes

Atlas Cloud returns three different error shapes depending on which part of the API you call. Check the shape before writing a parser.

<Tabs groupId="error-shape" items={['LLM & media', 'Async job failure', 'Public API']}>
<Tab value="LLM & media">

Used by the LLM protocol endpoints and the media generation endpoints:

```json
{
  "code": 401,
  "msg": "unauthorized",
  "request_id": "…",
  "data": null
}
```

</Tab>
<Tab value="Async job failure">

When a job is accepted but later fails, the HTTP call succeeds and the failure lives inside the prediction:

```json
{
  "code": 200,
  "data": {
    "id": "…",
    "status": "failed",
    "error": "…",
    "error_code": 1039
  }
}
```

Always check `data.status` — a `200` does not mean the generation succeeded.

</Tab>
<Tab value="Public API">

The billing endpoints under `/public/v1` use an OpenAI-style envelope:

```json
{
  "error": {
    "type": "invalid_request_error",
    "code": "invalid_date_range",
    "message": "end_date must be after start_date",
    "param": "end_date"
  },
  "request_id": "req_…"
}
```

</Tab>
</Tabs>

Every response carries an **`X-Request-ID`** header. Log it — it is the fastest way for support to trace a specific call.

## HTTP status codes

| Status | Meaning | What to do |
| --- | --- | --- |
| `400` | Malformed request: unparseable body, missing `model`, wrong content type, invalid retention header, or an invalid `webhook_url` | Fix the request. The `msg` field names the specific problem |
| `401` | Authentication failed — key missing, unknown, or expired | Check the key. Note that a wrong URL path also returns 401, so verify the endpoint too |
| `402` | Insufficient balance, or a Coding Plan allowance that has run out | [Top up](https://www.atlascloud.ai/console/billing) |
| `403` | Account or user is not permitted — includes using a Coding Plan key on a model that does not accept one | Check the key's scope, or contact support |
| `404` | Resource not found. For models this also covers models that are not available to your account | Check the model ID against the catalog |
| `413` | Request body exceeds 50 MB | Send a URL instead of inline Base64, or [upload the file](/upload-files) first |
| `429` | Rate limit reached | Back off and retry — see below |
| `451` | Blocked in your region | Not retryable |
| `500` | Internal error | Retry once, then report with the request ID |
| `503` | Temporarily unavailable | Retry with backoff |
| `504` | A synchronous request exceeded the maximum wait | Switch to the asynchronous flow and poll |

<Callout type="warn">
A `401` does not always mean your key is wrong. The gateway authenticates before routing, so a typo in the **path** also produces `401` rather than `404`. If a key works elsewhere, check the URL first.
</Callout>

## Job-level error codes

When an asynchronous job fails, `data.error_code` carries a numeric platform code that is more specific than the HTTP status. For example, `1039` indicates the input was rejected by content moderation.

`data.error` holds a human-readable description. Log both, along with the prediction ID.

## Rate limits

Rate limits apply per account and per model. Exceeding one returns `429`.

<Callout type="warn">
LLM and media endpoints do **not** return `X-RateLimit-Limit`, `X-RateLimit-Remaining`, or `Retry-After`. You cannot read your remaining quota from response headers — implement backoff on the client instead.

The `/public/v1` billing endpoints are the exception: their `429` responses do include `Retry-After`.
</Callout>

If you need higher limits for a production workload, [contact us](/contact) with your expected request volume and model mix.

## Retry strategy

Retry on `429`, `500`, `503`, and `504`, plus network-level failures. Do **not** retry `400`, `401`, `402`, `403`, `404`, or `451` — they will fail identically.

<Callout type="warn">
Retry **read** requests freely. Be careful with retrying generation submissions: a request that timed out may still have been accepted, and a blind retry can create — and bill for — a second job. Prefer submitting asynchronously and polling, so a lost response never means a lost job.
</Callout>

<Tabs groupId="lang" items={['Python', 'Node.js']}>
<Tab value="Python">
```python
import time, random, requests

RETRYABLE = {429, 500, 503, 504}

def call_with_retry(url, payload, api_key, max_attempts=4):
    for attempt in range(max_attempts):
        response = requests.post(
            url,
            json=payload,
            headers={"Authorization": f"Bearer {api_key}"},
            timeout=60,
        )
        if response.status_code not in RETRYABLE:
            return response

        if attempt == max_attempts - 1:
            break

        # Exponential backoff with jitter, so clients don't retry in lockstep
        delay = min(2 ** attempt, 30) * (0.5 + random.random() / 2)
        time.sleep(delay)

    return response
```
</Tab>
<Tab value="Node.js">
```javascript
const RETRYABLE = new Set([429, 500, 503, 504]);

async function callWithRetry(url, payload, apiKey, maxAttempts = 4) {
  let response;

  for (let attempt = 0; attempt < maxAttempts; attempt++) {
    response = await fetch(url, {
      method: "POST",
      headers: {
        Authorization: `Bearer ${apiKey}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify(payload),
    });

    if (!RETRYABLE.has(response.status)) return response;
    if (attempt === maxAttempts - 1) break;

    // Exponential backoff with jitter
    const delay = Math.min(2 ** attempt, 30) * 1000 * (0.5 + Math.random() / 2);
    await new Promise((resolve) => setTimeout(resolve, delay));
  }

  return response;
}
```
</Tab>
</Tabs>

## Streaming errors

When a streaming request fails **before** the stream opens, you get a normal HTTP error. Once the stream has started, the connection stays open and the error arrives as an event in the stream — so a `200` on a streaming call does not guarantee a complete response. Always handle mid-stream termination.

Streams may also contain SSE comment lines beginning with `:` as keep-alive signals. These are not data and must be ignored — most SSE clients do this for you, but hand-rolled parsers often do not.

## Getting help

When reporting a problem, include:

- The `X-Request-ID` header from the failing response
- The prediction ID, for asynchronous jobs
- The exact model ID and the timestamp

Reach us through [Support](/contact).

## Related

<Cards>
  <Card title="LLM API Protocols" href="/llm-protocols" description="Protocol-specific behavior and gateway normalization." />
  <Card title="Predictions" href="/predictions" description="Job status machine and polling." />
  <Card title="API Keys" href="/api-keys" description="Key scopes and permissions." />
</Cards>
