Documentation menu

Concepts

Errors and retries

How the API reports an error, which errors are worth retrying, and how to retry them.

The error object

A failed request returns a status of 400 or above and a JSON body with one error object. This is true for every endpoint, including the ones that return a file on success.

json
{
  "error": {
    "type": "invalid_request_error",
    "code": "invalid_options",
    "message": "image.quality must be a whole number between 0 and 100.",
    "param": "image.quality",
    "request_id": "req_Qm4tY8sLx02BnVc7JdPe",
    "doc_url": "https://smolmac.com/docs/reference/errors#invalid_options"
  }
}

Branch on code. It is stable. Show or log message, but do not match on its text. param names the field or option at fault, when there is one. Quote request_id when you contact support. Every code is listed in the error reference.

Failed requests are never billed.

Errors worth retrying

These can succeed if you send the same request again.

StatusCodeWhat happenedWhat to do
429rate_limitedThe account sent requests faster than its plan allows.Wait for the number of seconds in Retry-After, then retry.
429concurrency_limitedToo many requests or jobs are in progress at once.Wait for Retry-After, or until one of your requests finishes, then retry.
503engine_busyThe service is at capacity for a moment.Wait for Retry-After (one second), then retry with backoff.
504timeoutThe file took longer than a direct request allows.Retry once. If it times out again, send the file as a job, which has a much longer time limit.
500internalSomething went wrong on our side.Retry with backoff. If it keeps failing, contact support with the request_id.
409idempotency_in_progressPOST /v1/jobs was repeated with an Idempotency-Key whose first request is still being handled.Wait for Retry-After (one second), then send the same request again. You then get the stored response of the first request.

A request that gets no response at all, because the connection failed or timed out on your side, is also worth retrying.

Errors not worth retrying

These fail the same way every time until you change something.

StatusCodesWhat to change
400bad_request, invalid_options, missing_file, unsupported_versionThe request. message and param say what is wrong.
401missing_key, invalid_key, revoked_keyThe API key.
402no_payment_method, payment_required, spend_cap_reached, test_mode_sample_requiredThe account, in the dashboard: add a payment method, pay the invoice, or raise the cap. With a test key, use a sample file.
403account_suspended, ip_not_allowed, feature_not_enabled, invalid_signatureContact support for a suspended account or a feature in preview, or choose an output format other than HEIC. Call from an allowed address. Fetch the job again for its download link.
404not_foundThe path or the id.
409idempotency_conflict, job_not_finished, upload_incomplete, upload_already_completed, preset_limit_reachedUse a new idempotency key for a new request. Wait for the job to finish before asking for its receipt. Send the missing parts of an upload before completing it. Delete a preset before adding another.
413file_too_largeSend the file as a job, or send a smaller file.
415unsupported_input, unsupported_target, use_async_jobThe file type, the target format, or the endpoint.
422processing_failed, limit_exceeded, encrypted_pdfThe file itself. It is damaged, too large in pixels, pages or length, or password protected.

How to retry

  • Respect Retry-After. When the header is present, wait at least that many seconds.
  • Back off exponentially. Without the header, wait about 1 second, then 2, 4, 8, up to a ceiling such as 30 seconds.
  • Add jitter. Multiply each wait by a random factor so that many clients do not retry at the same instant.
  • Stop. Give up after a fixed number of attempts, such as five, and report the last error.
  • Keep the bytes. A request body that was streamed from a file cannot be sent twice. Read the file into memory, or reopen it, for each attempt.

Retrying a direct request is safe: nothing is stored, and a failed attempt is not billed. If an attempt succeeded but its response never reached you, the retry is a second successful request and both are billed. To create a job exactly once, send an idempotency key.

The SDKs retry for you. They resend a request that failed with status 429, 500, 503 or 504, or with a network error, up to twice by default. They wait for Retry-After when the response has it, and otherwise back off with jitter. See the TypeScript and Python pages.

A retry loop

import { readFile } from "node:fs/promises";

const RETRYABLE = new Set([429, 500, 503, 504]);
const MAX_ATTEMPTS = 5;

async function compress(path) {
  const body = await readFile(path); // in memory, so it can be sent again

  for (let attempt = 1; ; attempt++) {
    const response = await fetch("https://api.smolmac.com/v1/compress", {
      method: "POST",
      headers: { Authorization: `Bearer ${process.env.SMOL_API_KEY}` },
      body,
    });
    if (response.ok) return Buffer.from(await response.arrayBuffer());

    const { error } = await response.json();
    if (!RETRYABLE.has(response.status) || attempt === MAX_ATTEMPTS) {
      throw new Error(`${error.code}: ${error.message} (${error.request_id})`);
    }

    const retryAfter = Number(response.headers.get("retry-after")) || 0;
    const backoff = Math.min(30, 2 ** (attempt - 1)) * (0.5 + Math.random() / 2);
    await new Promise((resolve) => setTimeout(resolve, 1000 * Math.max(retryAfter, backoff)));
  }
}

Jobs

A job reports a processing failure in a different place. POST /v1/jobs returns 202 once the job is accepted. If the work then fails, the job's status becomes failed and its error field holds type, code and message. The codes are the same ones, plus a few that only jobs can have, such as input_fetch_failed.

  • The API already starts a job again on its own when the engine working on it restarts. It gives up after three attempts, and the job then fails with internal. A fault inside the engine while it works on the file ends the job with internal too.
  • A job that failed with timeout or internal can be tried again by creating a new job. Other codes need a different file, different options, or a feature enabled on the account.
  • A job deletes its input when it finishes, whatever the outcome. To try again with an uploaded file, upload the file again and create the job with the new upload id.
  • A failed job is not billed.