Core concepts

TransformKit has a small, consistent shape. You hand the SDK files and the transforms you want; it uploads the bytes, runs the work as async jobs, and returns signed URLs — in input order.

Two entry points

Everything is built on two methods.

  • runQueue(files, media) — a fluent chain for linear work. Add steps (maxSize, convert, rename) and await; they apply to every file.
  • runPipeline(files, pipeline) — run a graph that can fan one input out to many outputs.

Both take the same file inputs and return the same result shape. A queue compiles to a pipeline under the hood, so there is exactly one engine.

convert.ts
const results = await tk
  .runQueue([{ bytes, filename: 'photo.jpg' }], 'image')
  .maxSize(1600)
  .convert({ format: 'webp' });

const [photo] = results;
if (photo.ok) console.log(photo.outputs[0]!.media.url);

Uploads go direct to storage

The SDK uploads each file straight to object storage over a short-lived presigned URL — the bytes never proxy through the API — so large files upload quickly and cheaply. You pass the bytes (and a filename or contentType so the service knows the media type); the SDK handles the rest.

Uploading is unmetered. You're only billed per output produced, so you can upload freely.

Every output is an async job

Transforms are durable jobs — they run in the background, survive deploys, and don't block a request. You never write the polling loop: runQueue and runPipeline submit the work and wait for each output to finish before resolving. Under the hood, a job moves through a canonical status:

type JobStatus = 'queued' | 'running' | 'succeeded' | 'failed';

Stream upload → submit → output stages with .options({ onProgress }) — see Run a pipeline for a batch example.

Failures are isolated

One result comes back per input. A bad file (a corrupt image, or quota running out mid-batch) is ok: false with an error message — the rest of the batch still completes. Only a malformed pipeline throws, before anything uploads.

Status vs. state

Alongside the canonical status, the API exposes a derived state that also reflects expiry — useful when you list jobs in a dashboard. A succeeded job whose output has aged out reads as success-expired; a failed one whose input is gone reads as error-expired.

Results are signed URLs

Outputs are never streamed back inline. Each result output carries a signed URL plus metadata (dimensions, content type, size). Hand the URL to a browser or CDN as-is.

{
  "job_id": "…",
  "output": "hero",
  "media": {
    "url": "https://…signed…",
    "expires_in": 86400,
    "content_type": "image/webp",
    "format": "webp",
    "width": 1600,
    "height": 1067,
    "file_size": 184320
  }
}

Everything expires in 24h

Inputs and outputs both live for about 24 hours, then storage deletes them — and that's intentional: the less we retain, the less there is to expose. We don't train on your media. Keep a result by fetching the signed URL, or skip our storage entirely with bring your own storage. One output costs one credit; Auth & billing has the retention policy, quotas, and the error contract.

Next steps