# Errors

Every error the realpng API returns, with its status, its code, what it means and what to do about it.

Every failed call answers with an HTTP status and a JSON body whose `code` is stable. Branch on `code`, never on `message`, which is written for people and can change.

## Error shape

**Check an image on a private network**

```bash title="cURL"
curl -X POST "https://realpng.com/api/v1/audit" \
  -H "X-API-Key: $REALPNG_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "image_url": "http://192.168.1.20/photo.png"
  }'
```

```javascript title="JavaScript"
const res = await fetch("https://realpng.com/api/v1/audit", {
  method: "POST",
  headers: {
    "X-API-Key": process.env.REALPNG_API_KEY,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "image_url": "http://192.168.1.20/photo.png"
  }),
});
const data = await res.json();
```

```python title="Python"
import os

import requests

res = requests.post(
    "https://realpng.com/api/v1/audit",
    headers={"X-API-Key": os.environ["REALPNG_API_KEY"]},
    json={
        "image_url": "http://192.168.1.20/photo.png",
    },
)
data = res.json()
```

Response `400 Bad Request`

```json
{
  "code": "invalid_url",
  "message": "Use a public http or https URL on its default port."
}
```

| Field     | Type   | Description                                                                                        |
| --------- | ------ | -------------------------------------------------------------------------------------------------- |
| `code`    | string | Always present. A stable snake_case name for the error.                                            |
| `message` | string | Always present. One sentence for a person. Some messages include a link, such as where to upgrade. |
| `detail`  | object | Only on some errors. See [Error details](#error-details).                                          |

Some 429 and 503 errors also send a `Retry-After` header with the number of seconds to wait. [Run many images](/docs/credits-and-limits#run-many-images) shows a loop that waits for it.

## Errors by status

Each table lists every code that status carries. `GET /optimize/{id}/file` and `GET /optimize/{id}/image` need no key, so they never answer 401, 402 or 429.

The PUT to an `upload_url` goes to storage, not to realpng, so its refusals are not in this shape. [Handle errors](/docs/guides/upload-a-local-file#handle-errors) covers them.

### 400 Bad request

| Code                | What it means                                                                                                                                                                                                                                                                                                                                                                      | What to do                                                                                                                                                           |
| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `invalid_body`      | The body is not a JSON object. On `POST /uploads`, `size` is missing or not a positive whole number, or `content_type` is missing.                                                                                                                                                                                                                                                 | Send a JSON object. For an upload, send the file's exact size in bytes and its `content_type`.                                                                       |
| `invalid_image`     | The body sets none or more than one of `image_url`, `image_base64`, `image_upload_id`, `video_url`, `video_upload_id`, `audio_url` and `audio_upload_id`. Also sent when an upload id is not a UUID or the image is empty.                                                                                                                                                         | Send exactly one file field. An upload id is the `id` from `POST /uploads`.                                                                                          |
| `invalid_url`       | `image_url`, `video_url` or `audio_url` is not http or https, is on a port other than the default, includes a user name or password, or points at a private, loopback or link local address, by IP, through DNS or through a redirect. Also a `callback_url` that is not https, is on a port other than the default, includes a user name or password, or is a private IP address. | Use a URL anyone on the internet can open, or [upload the file](/docs/guides/upload-a-local-file).                                                                   |
| `image_unreachable` | The image URL answered with a status other than 200, took too long to answer, or could not be fetched.                                                                                                                                                                                                                                                                             | Check that the URL opens without a login or cookies. If the host blocks automated requests, [upload the file](/docs/guides/upload-a-local-file) instead.             |
| `video_unreachable` | The video URL answered with a status other than 200, took too long to send the whole file, or could not be fetched.                                                                                                                                                                                                                                                                | Check that the URL opens without a login or cookies. A large video goes faster as an [upload](/docs/guides/upload-a-local-file).                                     |
| `audio_unreachable` | The voice clip's URL answered with a status other than 200, took too long to send the whole file, or could not be fetched.                                                                                                                                                                                                                                                         | Check that the URL opens without a login or cookies, or [upload the file](/docs/guides/upload-a-local-file).                                                         |
| `invalid_callback`  | `POST /optimize` only. `callback_url` is not a string, or its host resolves to no address or to a private one.                                                                                                                                                                                                                                                                     | Pass a public https URL.                                                                                                                                             |
| `batch_unsupported` | MCP only. The request body is a JSON-RPC batch, an array of messages.                                                                                                                                                                                                                                                                                                              | Send one JSON-RPC message per request.                                                                                                                               |
| `invalid_parameter` | `seconds` is not a positive number. Otherwise the engine refused our request to it, which points to a fault on our side, not in your call.                                                                                                                                                                                                                                         | Send a video's length in seconds, or leave `seconds` out. For any other cause, retry once. If it keeps failing, email [hello@realpng.com](mailto:hello@realpng.com). |

### 401 Unauthorized

| Code          | What it means                                          | What to do                                                                                                                                                                   |
| ------------- | ------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `invalid_key` | The API key is missing, malformed, unknown or revoked. | Send a valid key in the `X-API-Key` header or as `Authorization: Bearer`. See [Authentication](/docs/authentication), or create a new key at [/account#keys](/account#keys). |

### 402 Payment required

| Code             | What it means                                                                                                                                 | What to do                                                                                                         |
| ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| `plan_required`  | Your plan has no API access. The message names the plan.                                                                                      | [Upgrade your plan](/account#billing). See [plans without API access](/docs/authentication#when-a-key-is-refused). |
| `payment_failed` | The last payment for your plan failed.                                                                                                        | Update your card at [/account#billing](/account#billing). API access resumes once the payment goes through.        |
| `quota_exceeded` | The credits left this period do not cover this call. Only on `POST /audit` and `POST /optimize`. [`detail`](#quota_exceeded) has the numbers. | Wait until `detail.resets_at`, or [upgrade your plan](/account#billing). The refused call costs nothing.           |

### 404 Not found

| Code               | What it means                                                                                                                                                                                                                                                                                                | What to do                                                                                                                      |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------- |
| `not_found`        | `GET /optimize/{id}` found no optimization with that ID on your account, or it has expired. `GET /audit/{id}` answers the same for a video check. `GET /optimize/{id}/file` and `GET /optimize/{id}/image` answer the same for an unknown or expired optimization, and until the optimization has succeeded. | Check the ID. Poll until its status is `succeeded` before you download.                                                         |
| `unknown_job`      | The optimization or video check has expired. It means the same as `not_found`, so treat both as gone.                                                                                                                                                                                                        | Start a new one. See [how long an optimization lasts](/docs/guides/optimize-an-image#how-an-optimization-runs).                 |
| `upload_not_found` | `POST /audit` or `POST /optimize` with `image_upload_id`, `video_upload_id` or `audio_upload_id`. Nothing was uploaded to that ID, the upload was already used, or it expired.                                                                                                                               | Request a new upload and PUT the file again. See [Use each upload once](/docs/guides/upload-a-local-file#use-each-upload-once). |

### 413 Content too large

| Code              | What it means                                                                                                                                                                                                                                            | What to do                                                                                                                                                                                        |
| ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `image_too_large` | The image is over the size limit for how you sent it. `image_base64` has a lower limit than `image_url` and uploads. Also sent when the `size` you give `POST /uploads` is over the limit, or when a JSON request body is too large, including over MCP. | Send a large file as an [upload](/docs/guides/upload-a-local-file). See [Image limits](/docs/credits-and-limits#image-limits).                                                                    |
| `too_many_pixels` | The image has more pixels, width times height, than the call accepts. `POST /optimize` refuses it at once, never as a failed optimization. [`detail`](#too_many_pixels) has the numbers.                                                                 | Resize the image to fit the [pixel limit](/docs/credits-and-limits#image-limits) and send it again. An uploaded file needs a [new upload](/docs/guides/upload-a-local-file#use-each-upload-once). |
| `video_too_large` | The video is over the size limit, sent by URL or as the `size` you give `POST /uploads`.                                                                                                                                                                 | Shorten or compress the video. See [Video limits](/docs/credits-and-limits#video-limits).                                                                                                         |
| `audio_too_large` | The voice clip is over the size limit, sent by URL.                                                                                                                                                                                                      | Compress the clip, or send it as an MP3. See [Video limits](/docs/credits-and-limits#video-limits).                                                                                               |
| `video_too_long`  | `seconds` is over the length limit. A video that runs past `seconds`, the length limit or the frame limit fails its job with the same code. [`detail`](#video_too_long) has the numbers.                                                                 | Send a shorter video, or the right `seconds`. See [Video limits](/docs/credits-and-limits#video-limits).                                                                                          |

### 415 Unsupported media type

| Code                | What it means                                                                                                                                                                                                                                                                                       | What to do                                                                                                                                                                                                                                       |
| ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `unsupported_media` | On `POST /uploads`, `content_type` is not a supported image, video or audio type. On a check or optimization, the file is not a supported type, is animated, or cannot be decoded. A check also answers it for a video or voice clip sent in an image field, and `POST /optimize` for a voice clip. | Convert the file to a [supported type](/docs/credits-and-limits#image-limits) and send it again. Send a video as `video_url` or `video_upload_id`. An uploaded file needs a [new upload](/docs/guides/upload-a-local-file#use-each-upload-once). |
| `audio_unsupported` | The voice clip has no audio that can be decoded. It fails the clip's job.                                                                                                                                                                                                                           | Export the clip again as an MP3, WAV or M4A, then send it again.                                                                                                                                                                                 |
| `video_unsupported` | The video has no video stream that can be decoded, or no usable frame rate. It fails the video's job.                                                                                                                                                                                               | Export the video again as an MP4 with H.264, then send it again.                                                                                                                                                                                 |

### 429 Too many requests

| Code            | What it means                                                                                                                                                                      | What to do                                                                                                    |
| --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| `rate_limited`  | Your account is over its plan's [requests a minute](/docs/credits-and-limits#requests-a-minute).                                                                                   | Wait the seconds in `Retry-After`, then retry.                                                                |
| `too_many_jobs` | Your account already runs as many checks and optimizations as its plan's [jobs at once](/docs/credits-and-limits#jobs-at-once) allows. Only on `POST /audit` and `POST /optimize`. | Wait the seconds in `Retry-After`, then retry. A slot frees up when a running check or optimization finishes. |

### 500 Internal server error

| Code             | What it means                                                                                       | What to do                                                                                                                            |
| ---------------- | --------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| `internal_error` | Something failed on our side.                                                                       | Retry with a short backoff. A failed check or optimization [gives its credits back](/docs/credits-and-limits#when-credits-come-back). |
| `engine_error`   | The engine failed without naming the error. It usually comes with status 500 but can carry another. | Retry. If it keeps failing, email [hello@realpng.com](mailto:hello@realpng.com).                                                      |

### 503 Service unavailable

| Code                                                  | What it means                                                                                                                               | What to do                                                                                                                    |
| ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| `engine_unavailable`                                  | The engine could not be reached or did not answer in time, usually because it is starting up. Sends `Retry-After`.                          | Wait the seconds in `Retry-After`, then retry.                                                                                |
| `model_unavailable`                                   | The detection models are not ready yet. No `Retry-After`.                                                                                   | Retry after a few seconds. This refusal does not use up an upload.                                                            |
| `optimizations_disabled`                              | Optimizing is paused. Checks still work. Only on `POST /optimize`. Sends `Retry-After`.                                                     | Wait the seconds in `Retry-After`, then retry. The image is not read, so an upload stays usable.                              |
| `tracking_failed`                                     | We could not record the optimization, so it was not charged.                                                                                | Start the optimization again. If you sent `image_upload_id`, [upload the file](/docs/guides/upload-a-local-file) again first. |
| `uploads_unavailable`                                 | Image storage is unavailable. This affects `POST /uploads` and images sent as `image_url` or `image_base64`, which are stored the same way. | Retry later.                                                                                                                  |
| `engine_credentials_missing`, `engine_region_missing` | A configuration fault on our side.                                                                                                          | Retry later. If it keeps failing, email [hello@realpng.com](mailto:hello@realpng.com).                                        |

## Error details

Only these three `detail` shapes are stable. Other errors may include a `detail` object whose fields can change, so do not code against them.

### quota_exceeded

| Field       | Type                       | Description                                                                |
| ----------- | -------------------------- | -------------------------------------------------------------------------- |
| `required`  | integer                    | Credits the call needs.                                                    |
| `available` | integer                    | Credits left this period, after the checks and optimizations in flight.    |
| `resets_at` | string, ISO 8601 date-time | When [credits reset](/docs/credits-and-limits#when-credits-reset), in UTC. |

### too_many_pixels

| Field    | Type    | Description                                                                            |
| -------- | ------- | -------------------------------------------------------------------------------------- |
| `width`  | integer | The image width in pixels.                                                             |
| `height` | integer | The image height in pixels.                                                            |
| `limit`  | integer | The most pixels, width times height, this call accepts. A pixel count, not megapixels. |

### video_too_long

| Field         | Type   | Description                          |
| ------------- | ------ | ------------------------------------ |
| `seconds`     | number | The video's length in seconds.       |
| `max_seconds` | number | The longest video this call accepts. |

## Errors in a failed optimization

An optimization that fails while it runs is not an HTTP error. `GET /optimize/{id}` and the webhook carry its `error`, with the same `code` and `message` fields, as [When an optimization fails](/docs/guides/optimize-an-image#when-an-optimization-fails) shows. A video or voice check's `GET /audit/{id}` carries its `error` the same way.

| Code                | What it means                                                       | What to do                                    |
| ------------------- | ------------------------------------------------------------------- | --------------------------------------------- |
| `job_timeout`       | The optimization did not finish in time.                            | Start a new optimization.                     |
| `stage1_failed`     | The re-render step failed.                                          | Start a new optimization.                     |
| `model_unavailable` | The models could not load for this optimization.                    | Start a new optimization.                     |
| `engine_error`      | The engine failed without naming the error.                         | Start a new optimization.                     |
| `job_failed`        | Any other failure.                                                  | Start a new optimization.                     |
| `video_too_long`    | The video runs past `seconds`, the length limit or the frame limit. | Send a shorter video, or the right `seconds`. |
| `video_unsupported` | The video could not be decoded.                                     | Export it again as an MP4 with H.264.         |
| `audio_too_long`    | The voice clip runs past the length limit.                          | Send a shorter clip.                          |
| `audio_unsupported` | The voice clip could not be decoded.                                | Export it again as an MP3, WAV or M4A.        |

A code this table does not list means the same thing: the optimization failed. Its credits [are given back](/docs/credits-and-limits#when-credits-come-back). A new optimization of an uploaded file needs a [new upload](/docs/guides/upload-a-local-file#use-each-upload-once).

## Errors over MCP

- Refusals before any tool runs answer as HTTP errors in the shape above: 401 `invalid_key`, 402 `plan_required` and `payment_failed`, 429 `rate_limited`, 400 `batch_unsupported`, and 413 `image_too_large` for an oversized request body.
- A request the MCP transport itself cannot read gets a JSON-RPC error object instead of that shape. That covers a missing MCP `Accept` header (406), a `Content-Type` other than JSON (415) and a body that is not a JSON-RPC message (400).
- Every error inside a tool call comes back as a tool result with `isError: true` and the error's message as its text. The `code` is not included.

[Read tool errors](/docs/mcp/tools#read-tool-errors) shows what an agent sees.
