# Write output to Cloudflare R2

Send Ittybit processing output directly to your R2 bucket

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:

<CodeGroup labels={["CLI", "TypeScript", "Python", "curl"]}>
```bash
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
```

```typescript
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"
```

```python

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"
```

```bash
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'"
  }'
```

</CodeGroup>

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.

<CodeGroup labels={["CLI", "TypeScript", "Python", "curl"]}>
```bash
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
```

```typescript
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();
```

```python

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()
```

```bash
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"
  }'
```

</CodeGroup>

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:

<CodeGroup labels={["CLI", "TypeScript", "Python", "curl"]}>
```bash
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
```

```typescript
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();
```

```python

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()
```

```bash
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"
  }'
```

</CodeGroup>

## 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:

```bash
# 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

- [Process files from S3](/guides/process-files-from-s3) -- set up S3-compatible connections
- [Write output to S3](/guides/write-output-to-s3) -- same pattern for AWS S3 buckets
- [Media processing with Cloudflare Workers, R2, and Ittybit](/guides/media-processing-with-cloudflare-workers-and-r2) -- full pipeline with Workers and D1
- [Replace Cloudflare Stream with Ittybit and R2](/guides/replace-cloudflare-stream-with-r2) -- HLS streaming from R2