The pipeline you were about to build.

TransformKit is a hosted API for media transforms. Turn user uploads into the assets your app needs, without having to build and maintain a media pipeline.

Create an API key

Free plan. No credit card.

import { TransformKit } from '@transform-kit/sdk';

const tk = new TransformKit({ apiKey: process.env.API_KEY! });

await tk
  .runQueue([{ bytes, filename: 'photo.png' }], 'image')
  .convert({ format: 'jpg', quality: 90 });

You didn't set out to build a media pipeline.

It starts with one upload. The steps below are what your product asks for next, before you notice you're working on infrastructure, not your product.

01

Start with a photo upload

At some point your app needs a photo upload. A user picks a photo from their camera roll and you show it in their profile.

import { TransformKit } from '@transform-kit/sdk';

const tk = new TransformKit({ apiKey: process.env.API_KEY! });

export async function onProfilePhotoUpload(formData: FormData) {
  const file = formData.get('photo') as File;
  const bytes = Buffer.from(await file.arrayBuffer());

  const [result] = await tk.runQueue(
    [{ bytes, filename: file.name }],
    'image',
  );

  return result.outputs[0]!.media.url;
}

02

Make every upload consistent

Photos arrive in different formats, sizes, and orientations. Normalize them once as they enter your system so every screen can rely on the same output.

import { TransformKit } from '@transform-kit/sdk';

const tk = new TransformKit({ apiKey: process.env.API_KEY! });

export async function onProfilePhotoUpload(formData: FormData) {
  const file = formData.get('photo') as File;
  const bytes = Buffer.from(await file.arrayBuffer());

  const [result] = await tk
    .runQueue([{ bytes, filename: file.name }], 'image')
    .maxSize(2048)
    .convert({ format: 'webp', quality: 82 });

  return result.outputs[0]!.media.url;
}

03

Handle real-world file sizes

Small images work in development. Production brings multi-megabyte photos from modern phones. Keep uploads fast and reliable without routing everything through your server.

import { TransformKit } from '@transform-kit/sdk';

const tk = new TransformKit({ apiKey: process.env.API_KEY! });

export async function startProfilePhotoUpload(contentType: string) {
  const ticket = await tk.createUpload({ contentType });
  return {
    uploadUrl: ticket.upload_url,
    inputKey: ticket.input_key,
  };
}

export async function putProfilePhotoFile(uploadUrl: string, file: File) {
  await fetch(uploadUrl, {
    method: 'PUT',
    body: file,
    headers: { 'Content-Type': file.type },
  });
}

export async function onProfilePhotoUpload(inputKey: string, filename: string) {
  const [result] = await tk
    .runQueue([{ inputKey, filename, contentType: 'image/jpeg' }], 'image')
    .maxSize(2048)
    .convert({ format: 'webp', quality: 82 });

  return result.outputs[0]!.media.url;
}
export async function uploadProfilePhoto(file: File) {
  const ticket = await startProfilePhotoUpload(file.type);
  await putProfilePhotoFile(ticket.uploadUrl, file);
  return onProfilePhotoUpload(ticket.inputKey, file.name);
}

04

Process uploads in batches

Users upload one image. Teams upload hundreds. Run transformations across entire folders with progress, retries, and per-file results instead of failing the whole job.

import { TransformKit } from '@transform-kit/sdk';

const tk = new TransformKit({ apiKey: process.env.API_KEY! });

export async function onProfilePhotoBatchUpload(formData: FormData) {
  const files = formData.getAll('photos') as File[];
  const inputs = await Promise.all(
    files.map(async (file) => ({
      bytes: Buffer.from(await file.arrayBuffer()),
      filename: file.name,
    })),
  );

  const results = await tk
    .runQueue(inputs, 'image')
    .maxSize(2048)
    .convert({ format: 'webp', quality: 82 })
    .options({
      concurrency: 6,
      onProgress: (e) => console.log(e),
    });

  for (const r of results) {
    if (r.ok) console.log(r.filename, r.outputs[0]?.media.url);
    else console.error(r.filename, r.error);
  }
}

05

Generate every size you need

One original should produce every asset your product needs. Profile photos, thumbnails, banners, and high-resolution exports, all from a single upload.

import { TransformKit } from '@transform-kit/sdk';

const tk = new TransformKit({ apiKey: process.env.API_KEY! });

export async function onProfilePhotoUpload(formData: FormData) {
  const file = formData.get('photo') as File;
  const bytes = Buffer.from(await file.arrayBuffer());

  const [photo] = await tk.runPipeline(
    [{ bytes, filename: file.name }],
    {
      nodes: [
        { id: 'in', type: 'pipeline.input' },
        { id: 'thumb', type: 'image.resize', config: { mode: { value: 'pixels' }, width: { value: 400 }, height: { value: 400 }, fit: { value: 'inside' } } } },
        { id: 'thumbOut', type: 'pipeline.output', config: { suffix: { value: 'thumb' } } },
        { id: 'banner', type: 'image.resize', config: { mode: { value: 'pixels' }, width: { value: 1200 }, height: { value: 630 }, fit: { value: 'inside' } } } },
        { id: 'bannerOut', type: 'pipeline.output', config: { suffix: { value: 'banner' } } },
        { id: 'export', type: 'image.resize', config: { mode: { value: 'pixels' }, width: { value: 2400 }, height: { value: 2400 }, fit: { value: 'inside' } } } },
        { id: 'exportOut', type: 'pipeline.output', config: { suffix: { value: 'export' } } },
      ],
      edges: [
        { source: 'in', target: 'thumb' },
        { source: 'thumb', target: 'thumbOut' },
        { source: 'in', target: 'banner' },
        { source: 'banner', target: 'bannerOut' },
        { source: 'in', target: 'export' },
        { source: 'export', target: 'exportOut' },
      ],
    },
  );

  return photo.outputs.map((o) => ({ label: o.output, url: o.media.url }));
}

06

Store media where you already do

Keep your existing infrastructure. Write processed assets directly to your own storage provider without changing the upload experience in your application.

import { TransformKit } from '@transform-kit/sdk';

const tk = new TransformKit({ apiKey: process.env.API_KEY! });

export async function onProfilePhotoUpload(formData: FormData) {
  const file = formData.get('photo') as File;
  const bytes = Buffer.from(await file.arrayBuffer());

  const [photo] = await tk
    .runQueue([{ bytes, filename: file.name }], 'image')
    .upload(async (file) => signedGetUrlForInput(file))
    .maxSize(2048)
    .convert({ format: 'webp', quality: 82 })
    .deliver(async (target) => {
      const putUrl = await presignedPutToYourBucket(target);
      return { putUrl, contentType: 'image/webp' };
    });

  return photo.outputs[0]!.media.url;
}

07

Debug production quickly

When uploads fail, you need answers fast. Verify configuration, credentials, limits, and connectivity from the command line without digging through dashboards.

npm install -g @transform-kit/cli

tk login
tk help

08

Build with AI agents

Give Claude, Cursor, or your own agents a safe way to work with your media pipeline. Verify access, inspect configuration, and automate workflows using MCP.

Run this once, then run claude /mcp, pick transform-kit, and authenticate via OAuth.

 
claude mcp add --transport http transform-kit "https://mcp.transform-kit.com"

The hard parts are already done.

Big files don't touch your server

Bytes go straight to storage over a short-lived presigned URL. A 40 MB file never proxies through the API, or through your function's memory limit.

Nothing blocks a request

Every output is a durable background job that survives your deploys. The SDK submits and waits, so you never write the polling loop, or hit a serverless timeout.

Results are ready to serve

A signed URL plus real metadata, width, height, content type, bytes. Drop it in an <img>, hand it to your CDN, or copy it to your own bucket.

A transform pipeline, not a media library.

Inputs and outputs are a short-lived handoff (about 24 hours), then storage deletes them. Pull the result while the signed URL is valid, or use bring-your-own storage to read from and write to your own bucket.

We don't train on your media.

Two minutes to your first transform.

Create a key, install the SDK, run one call. The free plan is waiting and it doesn't need a card.