API
Errors
Every IntoSQL API error code with its HTTP status, cause and fix, plus rate limits, idempotency keys and how to retry safely.
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.
The error object
Every error has the same shape:
{
"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/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 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. |
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. |
over_quota |
402 | The file is larger than what the plan has left this month. | Check 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 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, 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. |
method_not_allowed |
405 | The path exists, but not with that method. | Check the method in the API reference. |
http_error |
varies | Any other HTTP error. | Check the method and path in the API reference. |
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:
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 117Over 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 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.
POST /api/v1/conversions HTTP/1.1
Authorization: Bearer <key>
Content-Type: application/json
Idempotency-Key: import-ventas-2026-10-16How 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_specis 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_errorruns 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_progressandserver_error(any 5xx). WaitRetry-Afterseconds when the answer has it, otherwise double the wait between attempts, and send the sameIdempotency-Keywith eachPOST. - 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.urlis aPUTto 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:
{
"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 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, 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 sends conversion.failed with the whole conversion in data.
If a file didn't convert well, or at all, send it to us: 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.