API

Everything the tools on this site do, from your own code. Four endpoints, one API key, and no SDK to install.

Getting a key

Keys are issued by whoever runs this deployment — there is no sign-up form, and that is deliberate. A key carries its own daily allowance, so a self-serve one would be a self-serve way around the limits that keep the service up for everybody else.

A key looks like itk_<id>_<secret>. Only the id half is ever stored here; the secret is hashed, so a lost key cannot be recovered and has to be replaced. Send it on every request:

Authorization: Bearer itk_a1b2c3d4e5f6_your_secret_here

The shape of a job

Open a task, upload one or more files into it, tell it to run, then poll until it finishes. Options can be set when the task is opened or when it is processed; the second wins.

  1. 1

    POST /v1/tasks

    Open a task

    Chooses the tool and, optionally, its settings. Nothing is uploaded yet. The response carries the limits that apply to this tool, so a client can reject an oversized file locally instead of sending it to find out.

  2. 2

    POST /v1/tasks/{taskId}/files

    Upload one file

    The request body is the file itself — raw bytes, not a multipart form. Send one request per file. The type is decided by the bytes, never by the filename or the Content-Type header; `?name=` is stored only as a label. A file that is too large is refused while it streams, not after it lands.

  3. 3

    POST /v1/tasks/{taskId}/process

    Queue the work

    Settles the options and hands the task to a worker. This is the call that spends one of the key’s daily allowance, and it is charged only after every check has passed — a rejected request costs nothing. Calling it twice is not an error and is not charged twice.

  4. 4

    GET /v1/tasks/{taskId}

    Check status and collect the results

    Poll this. Once `status` is `done`, `outputs` carries a signed, expiring URL per file — fetch those directly and without an Authorization header. A batch can half-succeed, so check the per-file `status` too.

A complete example, in Node

No dependencies — this is the whole integration, from opening a task to writing the result to disk.

import { readFile, writeFile } from 'node:fs/promises'

const BASE = 'https://freeimagestudio.app/v1'
const KEY = process.env.IMAGE_TOOLKIT_KEY   // itk_<id>_<secret>

const auth = { Authorization: `Bearer ${KEY}` }

async function json(response) {
  if (!response.ok) {
    const { error } = await response.json()
    throw new Error(`${response.status} ${error.code}: ${error.message}`)
  }
  return response.json()
}

// 1. Open a task.
const { taskId, limits } = await json(
  await fetch(`${BASE}/tasks`, {
    method: 'POST',
    headers: { ...auth, 'content-type': 'application/json' },
    body: JSON.stringify({ tool: 'compress-image', options: { quality: 75 } }),
  }),
)

// 2. Upload the file as the raw body. One request per file.
const bytes = await readFile('photo.jpg')
if (bytes.byteLength > limits.maxFileBytes) throw new Error('too large for this tool')

await json(
  await fetch(`${BASE}/tasks/${taskId}/files?name=photo.jpg`, {
    method: 'POST',
    headers: auth,
    body: bytes,
  }),
)

// 3. Queue it. This is the call that spends one of today's allowance.
await json(
  await fetch(`${BASE}/tasks/${taskId}/process`, { method: 'POST', headers: auth }),
)

// 4. Poll until it settles.
let task
do {
  await new Promise((resolve) => setTimeout(resolve, 1000))
  task = await json(await fetch(`${BASE}/tasks/${taskId}`, { headers: auth }))
} while (task.status === 'queued' || task.status === 'processing')

if (task.status === 'error') throw new Error(task.files[0]?.error ?? 'processing failed')

// 5. The output URLs are signed and expire on their own — no header needed,
//    and nothing to follow a redirect onto.
for (const output of task.outputs) {
  const file = await fetch(output.url)
  await writeFile(output.name, Buffer.from(await file.arrayBuffer()))
}

Limits, and how long results live

Each key has a daily allowance, counted per tool. It is charged by /process and only once every check has passed, so a rejected file or a bad set of options costs nothing. Going over returns 429 with details.resetAt, which is the instant the count rolls over.

A task and everything in it — your upload and the result — are deleted 2 hours after the task is opened. There is no archive and no way to get a file back afterwards. Download the outputs as soon as they are ready.

Per-file and per-task size caps differ by tool. Rather than reading them here, take them from the limits object that comes back when the task is opened — those are the numbers the server will actually enforce.

Errors

Every failure has the same shape. Branch on code, which is stable; show message, which is written to be read by a person.

{
  "error": {
    "code": "too-large",
    "message": "Each file must be under 50 MB."
  }
}

Tools

The tool field takes one of these. Each one’s settings, defaults and permitted ranges are in the OpenAPI document, generated from the same schemas the server validates with.

One thing to know: a value out of range or of the wrong type is a 400, but a field the tool does not have is quietly dropped rather than refused. A misspelled option therefore does nothing instead of telling you — check the spec if a setting seems to have no effect.

Machine-readable

/v1/openapi.json is an OpenAPI 3.1 document covering every endpoint and every tool’s options. Point a generator at it rather than writing a client by hand.