API reference
Errors
The error format, and every error code the API returns: its status, when it happens and what to do.
The error object
Every failed request returns a status of 400 or above and a JSON body with a single error object. This holds for the endpoints that return a file on success too. Failed requests are never billed.
HTTP/1.1 415 Unsupported Media Type
Content-Type: application/json; charset=utf-8
Smol-Request-Id: req_Qm4tY8sLx02BnVc7JdPe
Smol-Version: 2026-10-01
{
"error": {
"type": "invalid_request_error",
"code": "unsupported_target",
"message": "A pdf file can be converted to: png, jpg.",
"param": null,
"request_id": "req_Qm4tY8sLx02BnVc7JdPe",
"doc_url": "https://smolmac.com/docs/reference/errors#unsupported_target"
}
}| Field | Type | Description |
|---|---|---|
| type | string | The family of the error. One of the seven types below. |
| code | string | What went wrong. Stable: write your code against this field. |
| message | string | An explanation for a person. Safe to show or log. Its wording may change, so do not match on it. |
| param | string or null | The field or option at fault, such as image.quality or input.url. null when the error is not about one field. |
| request_id | string | The id of the request, the same as the Smol-Request-Id response header. Quote it to support. |
| doc_url | string | A link to the documentation of the code. The part after # is the code, and matches the anchors on this page. |
A 429, a 503 and a 409 idempotency_in_progress also carry a Retry-After header, in seconds. For which errors to retry and how, see Errors and retries.
Error types
| Type | Statuses | Meaning |
|---|---|---|
invalid_request_error | 400, 404, 405, 409, 413, 415 | The request cannot be carried out as sent. Change it. |
authentication_error | 401 | The API key is missing or not accepted. |
billing_error | 402 | The account cannot be charged for this request. |
permission_error | 403 | The caller is not allowed to do this. |
rate_limit_error | 429 | Too many requests, or too much in progress. Retry later. |
processing_error | 422 | The request is well formed, but this file cannot be processed. |
api_error | 500, 503, 504 | A fault or a shortage on our side. Retry. |
Request errors
Type invalid_request_error. The same request will fail again until you change it. The one exception is idempotency_in_progress, which clears by itself.
| Code | Status | When it happens | What to do |
|---|---|---|---|
bad_request | 400, 405 | The request is malformed. The body is not valid JSON, is not a JSON object, or is over 64 KB. A field is unknown, missing, or has a value that is not allowed. A URL is not a public https URL. A multipart body cannot be parsed. A Content-Length header is missing where it is required. The Idempotency-Key is empty or over 255 characters. The account already has 10 webhook endpoints. The request names a preset the account does not have. With status 405: the HTTP method is wrong for the path. | Read message. param names the field at fault when there is one. Correct the request. Do not retry it unchanged. |
invalid_options | 400 | An option is wrong. The option or its section is unknown. The value is outside the allowed values or range. null was sent for an option that does not accept it. The options part of a multipart form is not valid JSON. A convert request has no target. The page asked for is beyond the end of the PDF. The sample rate is not supported by the output format. | param holds the path of the option, such as image.quality. Check it against Options. A misspelled option is refused, not ignored. |
missing_file | 400 | A direct request has no file. The body is empty, Content-Length is 0, or the multipart form has no part named file. | Send the bytes as the request body, or as the file part of a multipart form. With curl, use --data-binary @path, not -d. |
unsupported_version | 400 | The request has a Smol-Version header that names a version the API does not have. | message lists the versions that exist. Send one of them, or leave the header out. See Versioning. |
not_found | 404 | The path is not an endpoint, or the thing named does not exist for this account: a job, a job's output, an upload, a preset or a webhook endpoint. A job's output is not found once it has been deleted, and when it was sent to your own storage. A job is not found by a key of the other mode, or 30 days after it finished. An upload in parts is not found by a job until it has been completed. | Check the path and the id. Check that you are using a key of the same account and the same mode, live or test, as the one that created the job. |
idempotency_conflict | 409 | POST /v1/jobs was sent with an Idempotency-Key that was already used in the last 24 hours, and the body is not byte-for-byte the same. | Use a new key for a new job. To retry, send exactly the same body. See Idempotency. |
idempotency_in_progress | 409 | POST /v1/jobs was repeated with the same Idempotency-Key and body while the first request was still being handled. No second job is created. The response carries Retry-After: 1. | Wait a second and send the same request again. You then get the response of the first request, or, if that request failed, your repeat is handled as a new one. This is the one 409 that is worth retrying unchanged. |
job_not_finished | 409 | A receipt was requested for a job that is still queued or processing. | Wait until the job has succeeded, failed or been canceled, then request the receipt again. |
upload_incomplete | 409 | An upload in parts was completed before every part had been sent. | message lists the part numbers still to send. Send them, then complete the upload again. See Uploads. |
upload_already_completed | 409 | A part was sent to, or a second completion was asked of, an upload in parts that has already been completed. | Nothing. The upload is ready: create the job with its id. To send different content, create a new upload. |
preset_limit_reached | 409 | A new preset was created on an account that already has 100. | Delete a preset you no longer use, or replace an existing one by sending its name. |
file_too_large | 413 | The file is over a size limit: 25 MB on a direct request (10 MB with a test key), 95 MB for upload content sent in one request, or the job file limit of your plan. | Over the direct limit: use a job. Over 95 MB: create the upload with the file's real size, and send it in parts. Over the plan limit: see Limits. |
unsupported_input | 415 | The file cannot be used. Its type is not one the API handles, or it could not be identified. Or the operation does not apply to its kind: compressing a font, a table or an office document, removing metadata from anything but an image or a PDF, or converting a camera RAW file. On an estimate: the extension of the file name is not one the API handles. | Check the file against Formats. Send the file name: text formats such as CSV are recognised by their extension. Convert a RAW file with compress. |
unsupported_target | 415 | A convert request names a target format that is not available for this kind of file, such as an image to mp3. | message lists the targets allowed for the file. The full table is in Convert. |
use_async_job | 415 | A video, Word document, spreadsheet or presentation was sent to a direct endpoint. These can take minutes, so they run as jobs. | Upload the file and create a job with the same operation and options. |
Authentication errors
Type authentication_error. See Authentication.
| Code | Status | When it happens | What to do |
|---|---|---|---|
missing_key | 401 | There is no Authorization header, or it is not of the form Bearer <key>. | Send Authorization: Bearer $SMOL_API_KEY. Check that the variable is set where your code runs. |
invalid_key | 401 | The key is not a Smol key, or it does not exist. A key is smol_live_ or smol_test_ followed by 32 letters and digits. | Check for a truncated key, stray spaces or quotes. Create a new key in the dashboard if the old one is lost. |
revoked_key | 401 | The key was revoked in the dashboard. A revoked key can keep working for up to 30 seconds after it is revoked. | Use another key, or create a new one. |
Billing errors
Type billing_error. The key is valid, but the account cannot be charged for the request. Fix these in the dashboard, not in code.
| Code | Status | When it happens | What to do |
|---|---|---|---|
no_payment_method | 402 | A live key was used on an account that has no plan and no payment method. | Add a payment method or choose a plan in the dashboard. Until then, use a test key with the sample files. |
payment_required | 402 | A live key was used on an account that has an unpaid invoice. | Pay the invoice in the dashboard. Access returns once the payment is recorded. |
spend_cap_reached | 402 | A direct request or a new job was sent with a live key after the month's usage reached the account's spend cap. | Raise the cap in the dashboard, or wait for the next calendar month (UTC). See Monthly spend cap. |
test_mode_sample_required | 402 | A test key was used with a file that is not one of the published sample files, or with a job that takes its input from input.url. | Use an unmodified sample file from Test mode, or use a live key for your own files. |
Permission errors
Type permission_error. The key is valid, but it is not allowed to do this.
| Code | Status | When it happens | What to do |
|---|---|---|---|
account_suspended | 403 | The account is suspended. Every key of the account, live and test, is refused. | Contact support. |
ip_not_allowed | 403 | The API key has a list of allowed IP addresses, and the request came from an address that is not on it. | Call from an allowed address. The list of a key cannot be edited: to change it, create a new key with the right list in the dashboard and revoke the old one. If you call through a proxy or a cloud function, the address seen is the one the request leaves from. See Restrict a key to IP addresses. |
feature_not_enabled | 403 | The request needs a feature that is in limited preview and is not enabled for the account. HEIC output: the request asked for image.format=heic or a convert target of heic, or it sent a HEIC file to compress with image.format left at original, which would write HEIC. Video: a job's input turned out to be a video. On a job, the video case and the HEIC-input case are found only once the file has been identified: the job is created and then fails with this code. | Contact support to have the feature enabled. Until then, choose another output format: avif, webp, jpg or png. Removing metadata from a HEIC file works without the feature. |
https_required | 403 | The request was sent over plain http. The API answers only over https. | Use https://api.smolmac.com. If the request carried an API key, treat the key as exposed: revoke it in the dashboard and create a new one. |
invalid_signature | 403 | A download link for a job's output has a signature that does not match, or its expires time has passed. | Use result.download_url exactly as given. If it has expired, the output has been deleted. Create the job again. |
Rate limit errors
Type rate_limit_error. Both carry a Retry-After header. These are worth retrying.
| Code | Status | When it happens | What to do |
|---|---|---|---|
rate_limited | 429 | The account sent requests faster than its plan allows, and the burst allowance is used up. Direct requests have one limit; every other endpoint shares a second, looser one. | Wait for the number of seconds in Retry-After, then retry. See Rate limit. |
concurrency_limited | 429 | Too much is in progress at once: direct requests being processed, or jobs that are queued or processing. | Wait for Retry-After (1 second for a direct request, 5 for a job), or until some of your work finishes, then retry. Limit the number of requests you run in parallel. |
upload_allowance_reached | 429 | The account has uploaded as many bytes as it may in 24 hours. Every upload counts, whether or not a job used it. | Wait and try again; the allowance frees up hour by hour. If your real volume needs more, move to a plan with a larger file limit, or pass files as input.url, which does not count. See Upload allowance. |
Processing errors
Type processing_error. The request was accepted and the file was read, but the file itself stands in the way. On a job these appear in the job's error, not as an HTTP response.
| Code | Status | When it happens | What to do |
|---|---|---|---|
limit_exceeded | 422 | The file is within the size limit in bytes but over another limit: pixels, frames of an animation, pages of a PDF, or length of audio or video. | message states the file's value and the limit. Send a smaller file, or see Limits for what each plan allows. |
encrypted_pdf | 422 | The PDF needs a password to open. | Remove the password and send the file again. The API has no way to accept a password. |
processing_failed | 422 | The file was recognised but could not be processed. It may be damaged or cut short, or it may use a variant of its format that the tools do not support. Also returned for a video with no video track, audio with no audio track, and a table file that cannot be parsed. | message names the step that failed. Open the file in another program to check it. If a good file fails, contact support with the request_id. Retrying does not help. |
API errors
Type api_error. Nothing is wrong with your request. These are worth retrying.
| Code | Status | When it happens | What to do |
|---|---|---|---|
internal | 500 | Something went wrong on our side. | Retry with backoff. If it keeps failing, contact support with the request_id. |
engine_busy | 503 | Every engine is at capacity for a moment. Nothing was processed. | Wait for Retry-After (1 second), then retry with backoff. |
timeout | 504 | A direct request was still being processed after 120 seconds. | Send the file as a job, which may run for up to 4 hours. A faster setting can also help. |
Job errors
POST /v1/jobs returns the errors above when it refuses to create a job. Once a job exists, a failure is reported on the job: its status becomes failed or canceled, and its error field is set. That object is shorter than an HTTP error. It has type, code and message only.
{
"type": "invalid_request_error",
"code": "input_fetch_failed",
"message": "The input URL returned HTTP 403."
}These four codes appear only on jobs:
| Code | Type | When it happens | What to do |
|---|---|---|---|
canceled | invalid_request_error | You sent DELETE /v1/jobs/{id} before the job finished. The job's status is canceled, not failed. | Nothing. This is the record of your own request. |
input_fetch_failed | invalid_request_error | The file at input.url could not be fetched. The request failed or was redirected to an address that is not allowed, the server answered with an error status, the response had no Content-Length header, or the file is larger than your plan allows. | message says which. Check that the URL answers a plain GET from the public internet with status 200 and a Content-Length. Then create a new job. |
input_changed | invalid_request_error | The upload was overwritten after the job was created. A job only runs on the file that was there when it was created. | Start a new upload for the new file, and create a new job with that upload id. Do not send a second file to an upload id a job is using. |
input_missing | invalid_request_error | The uploaded file was no longer there when the job started. The usual cause is that another job used the same upload and finished first. | Upload the file again and create a new job. An upload can be used by one job. |
output_upload_failed | invalid_request_error | The result could not be sent to your output.url. The URL is not a public https URL, or the PUT got a response outside 200 to 299, a redirect included. | message gives the status your server returned. Check that the URL accepts a PUT with the headers you supplied and has not expired. Then create a new job. See Output to your storage. |
A job's error can also have one of these codes, with the meaning given in the tables above:
| Code | On a job it means |
|---|---|
unsupported_input | The file type is not handled, or the operation does not apply to it. |
unsupported_target | The convert target is not available for this kind of file. |
invalid_options | An option could only be checked against the file itself: a page beyond the end of the PDF, or a sample rate the output format does not support. |
limit_exceeded | The file is over a pixel, frame, page or length limit. |
encrypted_pdf | The PDF needs a password. |
processing_failed | The file could not be processed. |
bad_request | The engine refused the job as it was handed over. The message says why. A fault inside the engine is reported as internal, not as this code. |
feature_not_enabled | The file is a video and video is not enabled for the account, or the output would be HEIC (a HEIC input with image.format left at original) and HEIC output is not enabled. The type is permission_error. |
timeout | No engine became free within 30 minutes, or the work ran for more than 4 hours. |
internal | A fault on our side: the engine failed while working on the file, or the job was started three times and each attempt was lost. |