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

Transform uploads in batches

Users upload one image. Teams upload hundreds. Run transforms 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 outputs 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 CLI.

npm install -g @transform-kit/cli

tk login
tk help

08

Connect via MCP

Give Claude, Cursor, or your own agents access to the same API. Verify identity, inspect configuration, and automate transforms over 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.