Write output to Cloudflare R2

View Markdown

Ittybit writes processed files to its CDN by default. If you want output to land in your own Cloudflare R2 bucket instead, set an s3:// output URL on the task. R2 is S3-compatible, so the same s3:// scheme works — you just need a connection with write access.

Create an R2 connection

Generate an R2 API token in the Cloudflare dashboard under R2 > Manage R2 API Tokens. The token needs Object Read & Write permission on the target bucket.

Then register the connection with Ittybit:

ittybit connections add s3 \
  --name my-r2-bucket \
  --endpoint https://<account-id>.r2.cloudflarestorage.com \
  --region auto \
  --access-key-id $R2_ACCESS_KEY_ID \
  --secret-access-key $R2_SECRET_ACCESS_KEY
const res = await fetch('https://api.ittybit.com/connections', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.ITTYBIT_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    kind: 's3',
    name: 'my-r2-bucket',
    endpoint: `https://${CLOUDFLARE_ACCOUNT_ID}.r2.cloudflarestorage.com`,
    region: 'auto',
    access_key_id: process.env.R2_ACCESS_KEY_ID,
    secret_access_key: process.env.R2_SECRET_ACCESS_KEY,
  }),
});
const connection = await res.json();
// connection.id -> "conn_abc123"
import requests
import os

res = requests.post(
    "https://api.ittybit.com/connections",
    headers={"Authorization": f"Bearer {os.environ['ITTYBIT_API_KEY']}"},
    json={
        "kind": "s3",
        "name": "my-r2-bucket",
        "endpoint": f"https://{os.environ['CLOUDFLARE_ACCOUNT_ID']}.r2.cloudflarestorage.com",
        "region": "auto",
        "access_key_id": os.environ["R2_ACCESS_KEY_ID"],
        "secret_access_key": os.environ["R2_SECRET_ACCESS_KEY"],
    },
)
connection = res.json()
# connection["id"] -> "conn_abc123"
curl -X POST https://api.ittybit.com/connections \
  -H "Authorization: Bearer $ITTYBIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "kind": "s3",
    "name": "my-r2-bucket",
    "endpoint": "https://<account-id>.r2.cloudflarestorage.com",
    "region": "auto",
    "access_key_id": "'$R2_ACCESS_KEY_ID'",
    "secret_access_key": "'$R2_SECRET_ACCESS_KEY'"
  }'

Save the connection_id from the response. You’ll pass it in every task that reads from or writes to this bucket.

Write output to R2

Set the output field to an s3:// URL pointing to the destination path in your R2 bucket. Include the connection_id in options so Ittybit knows which credentials to use.

ittybit video \
  -i https://example.com/uploads/video.mov \
  -o s3://my-r2-bucket/processed/video.mp4 \
  --connection-id conn_abc123 \
  --width 1280 \
  --format mp4
const res = await fetch('https://api.ittybit.com/jobs', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.ITTYBIT_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    input: 'https://example.com/uploads/video.mov',
    kind: 'video',
    options: {
      connection_id: 'conn_abc123',
      width: 1280,
      format: 'mp4',
    },
    output: 's3://my-r2-bucket/processed/video.mp4',
  }),
});
const task = await res.json();
import requests
import os

res = requests.post(
    "https://api.ittybit.com/jobs",
    headers={"Authorization": f"Bearer {os.environ['ITTYBIT_API_KEY']}"},
    json={
        "input": "https://example.com/uploads/video.mov",
        "kind": "video",
        "options": {
            "connection_id": "conn_abc123",
            "width": 1280,
            "format": "mp4",
        },
        "output": "s3://my-r2-bucket/processed/video.mp4",
    },
)
task = res.json()
curl -X POST https://api.ittybit.com/jobs \
  -H "Authorization: Bearer $ITTYBIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "input": "https://example.com/uploads/video.mov",
    "kind": "video",
    "options": {
      "connection_id": "conn_abc123",
      "width": 1280,
      "format": "mp4"
    },
    "output": "s3://my-r2-bucket/processed/video.mp4"
  }'

When the task completes, the processed file is at s3://my-r2-bucket/processed/video.mp4 in your R2 bucket.

R2 to R2

Read from one path in R2, process, and write to another. Both the input and output use s3:// URLs:

ittybit video \
  -i s3://my-r2-bucket/uploads/raw.mov \
  -o s3://my-r2-bucket/processed/web.mp4 \
  --connection-id conn_abc123 \
  --width 1920 \
  --format mp4
const res = await fetch('https://api.ittybit.com/jobs', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.ITTYBIT_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    input: 's3://my-r2-bucket/uploads/raw.mov',
    kind: 'video',
    options: {
      connection_id: 'conn_abc123',
      width: 1920,
      format: 'mp4',
    },
    output: 's3://my-r2-bucket/processed/web.mp4',
  }),
});
const task = await res.json();
import requests
import os

res = requests.post(
    "https://api.ittybit.com/jobs",
    headers={"Authorization": f"Bearer {os.environ['ITTYBIT_API_KEY']}"},
    json={
        "input": "s3://my-r2-bucket/uploads/raw.mov",
        "kind": "video",
        "options": {
            "connection_id": "conn_abc123",
            "width": 1920,
            "format": "mp4",
        },
        "output": "s3://my-r2-bucket/processed/web.mp4",
    },
)
task = res.json()
curl -X POST https://api.ittybit.com/jobs \
  -H "Authorization: Bearer $ITTYBIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "input": "s3://my-r2-bucket/uploads/raw.mov",
    "kind": "video",
    "options": {
      "connection_id": "conn_abc123",
      "width": 1920,
      "format": "mp4"
    },
    "output": "s3://my-r2-bucket/processed/web.mp4"
  }'

Folder structure patterns

The output path determines where files land in your bucket. Use this to organize output by type, date, or user:

s3://my-r2-bucket/videos/{user_id}/{filename}.mp4
s3://my-r2-bucket/thumbnails/{user_id}/{filename}.webp
s3://my-r2-bucket/audio/{user_id}/{filename}.mp3
s3://my-r2-bucket/{year}/{month}/{filename}.mp4

For HLS output, use a trailing slash. Ittybit writes the manifest and segments into that directory:

s3://my-r2-bucket/streams/my-video/
  -> index.m3u8
  -> segment-000.ts
  -> segment-001.ts
  -> ...

Verify the file in R2

After the task status is succeeded, confirm the file exists in your bucket using Wrangler or the S3 API:

# Using Wrangler
npx wrangler r2 object get my-r2-bucket/processed/video.mp4 --pipe > /dev/null && echo "OK"

# Using the S3 API
aws s3api head-object \
  --bucket my-r2-bucket \
  --key processed/video.mp4 \
  --endpoint-url https://<account-id>.r2.cloudflarestorage.com

See also