---
title: "Errors"
description: "Every IntoSQL API error code with its HTTP status, cause and fix, plus rate limits, idempotency keys and how to retry safely."
canonical: https://intosql.com/docs/api/errors
last_updated: 2026-10-15
---

# Errors

When the API can't do what a request asks, it answers with a 4xx or 5xx status and an error object. The `code` is stable between versions, so your code can branch on it. The `message` is for people and can change.

A conversion that runs and then fails is different: the request succeeded, and the conversion ends with `status: "failed"` and the reason in `error`. See [Failed conversions](#failed-conversions).

## The error object

Every error has the same shape:

```json
{
    "error": {
        "code": "too_many_active",
        "message": "5 conversions are already queued or running on this account. Start this one when one of them ends.",
        "request_id": "req_01k7r3j9qf8m2v6x4t0c5n1h8d"
    }
}
```

When the request has missing or invalid fields, the code is `invalid_request` and `fields` lists the problems per field:

```http
HTTP/1.1 422 Unprocessable Content
Content-Type: application/json

{
    "error": {
        "code": "invalid_request",
        "message": "Some fields are missing or invalid.",
        "fields": {
            "spec": ["The spec field is required."]
        },
        "request_id": "req_01k7r3jb2c4e6g8j0m2p4r6t8v"
    }
}
```

| Field        | Type   | Meaning                                                        |
| ------------ | ------ | -------------------------------------------------------------- |
| `code`       | string | Stable identifier. Branch on this.                             |
| `message`    | string | What went wrong, for people. Don't parse it.                   |
| `fields`     | object | Only with `invalid_request`: field name to a list of messages. |
| `request_id` | string | The answer's `X-Request-Id`. Quote it when you write to us.    |

Every answer, error or not, carries an `X-Request-Id` header. The [TypeScript SDK](/docs/sdks/typescript) throws errors as `IntoSQLError` with `status`, `code`, `message`, `fields` and `requestId`.

## Error codes

| Code                   | Status | Cause                                                                                                            | Fix                                                                                                             |
| ---------------------- | ------ | ---------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
| `unauthenticated`      | 401    | No `Authorization` header, or the key is wrong or revoked.                                                       | Send `Authorization: Bearer <key>` with a key from [Settings, API keys](https://intosql.com/settings/api-keys). |
| `forbidden`            | 403    | The token wasn't made for the API, so it can't use conversions.                                                  | Use a key made in Settings, API keys.                                                                           |
| `suspended`            | 403    | The account is suspended.                                                                                        | Contact support.                                                                                                |
| `needs_pro`            | 402    | The account has no IntoSQL Pro plan.                                                                             | Subscribe to Pro on [/pricing](/pricing).                                                                       |
| `over_quota`           | 402    | The file is larger than what the plan has left this month.                                                       | Check [usage](/docs/api/usage), wait for next month, or split the file.                                         |
| `too_big`              | 413    | `bytes` is over 5 GiB, the largest upload (one `PUT` to storage), or a file sent by `url` is over 50 GiB.        | Send a large file by `url`, or split it.                                                                        |
| `too_many_active`      | 429    | Five conversions are already queued or running on the account. Sent when you start a conversion or send a `url`. | Wait `Retry-After` seconds, or for a [webhook](/docs/api/webhooks) that one ended, then try again.              |
| `invalid_request`      | 422    | Missing or invalid fields, listed in `fields`. Also an `Idempotency-Key` over 255 characters.                    | Fix the fields. Send either `name` and `bytes`, or `url`, never both.                                           |
| `invalid_spec`         | 422    | `spec` isn't a valid conversion spec. The message names the problem.                                             | Check it against [Conversion spec](/docs/api/spec), or validate it with `intosql validate`.                     |
| `invalid_url`          | 422    | `url` isn't HTTPS, or its host has a private, local or reserved address.                                         | Use an `https://` address reachable from the internet, on port 443 or 8443.                                     |
| `idempotency_mismatch` | 422    | The `Idempotency-Key` was already used in the last 24 hours for a different request.                             | Use a new key for each new request.                                                                             |
| `request_in_progress`  | 409    | A request with the same `Idempotency-Key` is still running after 10 seconds.                                     | Retry with the same key in a moment.                                                                            |
| `not_uploaded`         | 409    | You started a conversion before its file arrived whole, or the uploaded file isn't `bytes` long.                 | Finish the `PUT` to `upload.url`, then start it again. If the size was wrong, open a new conversion.            |
| `already_started`      | 409    | The conversion is already queued, running or finished.                                                           | Read it with `GET /conversions/{id}` instead of starting it again.                                              |
| `already_finished`     | 409    | You canceled a conversion that already ended as `done` or `failed`.                                              | Delete it instead, if you want its files gone.                                                                  |
| `not_finished`         | 409    | You sent a conversion as a case before it ended.                                                                 | Wait for it to end, then send it.                                                                               |
| `file_gone`            | 409    | You sent a case with `share: file`, but the conversion's file was deleted or expired.                            | Send the case with the report only, or convert the file again and send that one.                                |
| `not_found`            | 404    | No conversion with that id on this account, or it was deleted. Also any unknown path.                            | Check the id and the path.                                                                                      |
| `rate_limited`         | 429    | More than 120 requests in a minute with this key.                                                                | Wait, then retry. See [Rate limits](#rate-limits).                                                              |
| `method_not_allowed`   | 405    | The path exists, but not with that method.                                                                       | Check the method in the [API reference](/docs/api).                                                             |
| `http_error`           | varies | Any other HTTP error.                                                                                            | Check the method and path in the [API reference](/docs/api).                                                    |
| `server_error`         | 500    | Something failed on IntoSQL's side. It was reported.                                                             | Retry with the same `Idempotency-Key`.                                                                          |

`needs_pro`, `suspended`, `forbidden` and `unauthenticated` are checked before anything else, on every endpoint.

## Rate limits

There are two limits:

- **Requests:** 120 a minute per API key. Each key has its own limit, so one script can't slow another key of the same account.
- **Active conversions:** five queued or running at once per account, across all its keys. Conversions waiting for their upload don't count.

Successful answers carry the request limit in headers:

```http
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 117
```

Over the limit, the API answers `429` with `rate_limited`, with `X-RateLimit-Remaining: 0` and a `Retry-After` header that says how many seconds to wait before the next request.

`too_many_active` also answers `429`, with `Retry-After: 30`. A slot frees as soon as a running conversion ends, which a [webhook](/docs/api/webhooks) tells you.

## Idempotency

Every `POST` (opening, starting and canceling a conversion, and sending a case) accepts an `Idempotency-Key` header, so a request you send again after a timeout or a lost connection runs only once. `GET` and `DELETE` ignore it: they are safe to repeat as they are.

```http
POST /api/v1/conversions HTTP/1.1
Authorization: Bearer <key>
Content-Type: application/json
Idempotency-Key: import-ventas-2026-10-16
```

How the key works:

| Situation                                             | Answer                                                                                                 |
| ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
| First request with the key                            | Runs normally. The answer is kept for 24 hours, unless it is a 5xx.                                    |
| Same key, same method, path and body, within 24 hours | The first answer again, with the same status and the header `Idempotent-Replayed: true`. Nothing runs. |
| Same key, different method, path or body              | `422 idempotency_mismatch`.                                                                            |
| Same key while the first request is still running     | Waits up to 10 seconds for the first answer, then `409 request_in_progress`.                           |
| Key longer than 255 characters                        | `422 invalid_request`.                                                                                 |

Details that matter:

- **Keys belong to the account.** Two keys of the same account share them.
- **The body is compared byte for byte.** Send the retry with exactly the same body. Serializing the same object again with keys in another order counts as a different request.
- **Errors are kept too.** A 4xx such as `invalid_spec` is replayed for the same key. After fixing the request, use a new key.
- **5xx answers aren't kept**, so a retry with the same key after `server_error` runs again.

Use a random UUID per request, or a key tied to the work, such as `import-<file>-<date>`, so a retry after your process restarts reuses it.

## Retry safely

- **Retry** on a lost connection, a timeout, and the codes `rate_limited`, `too_many_active`, `request_in_progress` and `server_error` (any 5xx). Wait `Retry-After` seconds when the answer has it, otherwise double the wait between attempts, and send the same `Idempotency-Key` with each `POST`.
- **Never retry** `already_started`: the conversion is on its way, so read it instead. **Don't retry unchanged** on other 4xx codes either; they describe the request or the account, and sending it again gets the same answer.
- **Reads and deletes** (`GET`, `DELETE`) are safe to repeat as they are.
- The file upload to `upload.url` is a `PUT` to storage, outside the API. Repeating it replaces the file, which is safe before you start the conversion. The address works for an hour.

Every IntoSQL SDK does all of this: it adds a random `Idempotency-Key` to every `POST`, keeps it across retries, and retries lost connections and the codes above, twice by default.

## Failed conversions

Once a conversion is queued, problems with the file no longer come back as HTTP errors. The conversion ends with `status: "failed"`, and `error` says why, with a stable code:

```json
{
    "data": {
        "id": "01k7r3j9qf8m2v6x4t0c5n1h8d",
        "status": "failed",
        "error": {
            "code": "encrypted_file",
            "message": "\"ventas.xlsx\" is protected with a password.",
            "problems": []
        },
        "output": null,
        "finished_at": "2026-10-16T14:03:27+00:00"
    }
}
```

| Field      | Type   | Meaning                                                                                       |
| ---------- | ------ | --------------------------------------------------------------------------------------------- |
| `code`     | string | Stable. Branch on this.                                                                       |
| `message`  | string | What went wrong, for people.                                                                  |
| `problems` | array  | What the engine found that stopped it, such as a spec value out of range, each with its code. |

When the engine refuses the file, `code` is the engine's own: `empty_file`, `corrupt_file`, `encrypted_file`, `unsupported_format`, `sheet_not_found`, `no_columns`, or `invalid_spec` with the reasons in `problems`. [Errors](/docs/errors) explains each one. The rest happen on our side:

| Code                | Cause                                                                                           | Fix                                                     |
| ------------------- | ----------------------------------------------------------------------------------------------- | ------------------------------------------------------- |
| `unreachable`       | A file sent by `url` couldn't be downloaded: no answer, an error status, or too many redirects. | Check the address answers from the internet.            |
| `download_stalled`  | The download stopped sending, or ran past its hour.                                             | Try again, or upload the file instead.                  |
| `too_big`           | The file sent by `url` turned out larger than a conversion takes.                               | Split the file.                                         |
| `over_quota`        | The file sent by `url` is larger than what the plan has left this month.                        | Check [usage](/docs/api/usage), or wait for next month. |
| `not_uploaded`      | The upload changed or went missing after the conversion started.                                | Open a new conversion.                                  |
| `timed_out`         | It ran past three hours, download included.                                                     | Split the file and convert the parts.                   |
| `account_suspended` | The account was suspended while it waited.                                                      | Contact support.                                        |
| `internal_error`    | It stopped on IntoSQL's side. The failure was reported.                                         | Run it again.                                           |

A failed or canceled conversion doesn't count toward the month's allowance. The SDKs raise `ConversionFailedError` with the code and the message, and a [webhook](/docs/api/webhooks) sends `conversion.failed` with the whole conversion in `data`.

If a file didn't convert well, or at all, [send it to us](/docs/api/conversions#send-us-a-conversion): a failed conversion keeps its upload until it expires, so the case can carry the file.

Rows the engine rejects don't fail the conversion. They are counted in `report.rows.rejected`, with the reason for each in `report.problems`.
