# AWS event-driven media processing with Ittybit

Process S3 uploads automatically using EventBridge, Lambda, and Ittybit webhooks

Drop a file into S3 and walk away. EventBridge picks up the notification, triggers a Lambda that kicks off Ittybit processing, and a webhook callback writes the result to DynamoDB. Zero polling, fully serverless.

## Architecture

```
S3 upload -> EventBridge rule -> Lambda (dispatch) -> Ittybit API
                                                         |
DynamoDB <- Lambda (webhook) <- API Gateway <- Ittybit webhook
```

## Prerequisites

- An S3 bucket with EventBridge notifications enabled
- An [Ittybit connection](/guides/process-files-from-s3) configured for your bucket
- `ITTYBIT_API_KEY` stored in AWS Secrets Manager (or SSM Parameter Store)

## Enable EventBridge on your bucket

```bash
aws s3api put-bucket-notification-configuration \
  --bucket my-media-bucket \
  --notification-configuration '{"EventBridgeConfiguration": {}}'
```

## EventBridge rule

Match `PutObject` events for media files:

```json
{
  "source": ["aws.s3"],
  "detail-type": ["Object Created"],
  "detail": {
    "bucket": { "name": ["my-media-bucket"] },
    "object": {
      "key": [
        { "suffix": ".mov" },
        { "suffix": ".mp4" },
        { "suffix": ".mkv" },
        { "suffix": ".webm" }
      ]
    }
  }
}
```

Target this rule at your dispatch Lambda.

## Dispatch Lambda

The handler extracts the bucket and key from the EventBridge event, then POSTs a task to Ittybit.

<CodeGroup labels={["TypeScript", "Python", "curl"]}>
```typescript

const ITTYBIT_API_KEY = process.env.ITTYBIT_API_KEY!;
const WEBHOOK_URL = process.env.WEBHOOK_URL!; // your API Gateway / Function URL
const CONNECTION_ID = process.env.ITTYBIT_CONNECTION_ID!;

interface S3ObjectCreated {
bucket: { name: string };
object: { key: string; size: number };
}

export const handler = async (event: EventBridgeEvent<"Object Created", S3ObjectCreated>) => {
  const { bucket, object } = event.detail;
  const input = `s3://${bucket.name}/${object.key}`;

const task = {
input,
kind: "video",
options: {
connection_id: CONNECTION_ID,
width: 1920,
format: "mp4",
quality: "high",
},
webhook_url: WEBHOOK_URL,
metadata: {
source_bucket: bucket.name,
source_key: object.key,
},
};

const res = await fetch("https://api.ittybit.com/jobs", {
method: "POST",
headers: {
Authorization: `Bearer ${ITTYBIT_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify(task),
});

if (!res.ok) {
const body = await res.text();
throw new Error(`Ittybit API error ${res.status}: ${body}`);
}

const data = await res.json();
console.log("Task created:", data.id);
return data;
};

````

```python

ITTYBIT_API_KEY = os.environ["ITTYBIT_API_KEY"]
WEBHOOK_URL = os.environ["WEBHOOK_URL"]
CONNECTION_ID = os.environ["ITTYBIT_CONNECTION_ID"]

def handler(event, context):
    detail = event["detail"]
    bucket = detail["bucket"]["name"]
    key = detail["object"]["key"]
    input_url = f"s3://{bucket}/{key}"

    task = {
        "input": input_url,
        "kind": "video",
        "options": {
            "connection_id": CONNECTION_ID,
            "width": 1920,
            "format": "mp4",
            "quality": "high",
        },
        "webhook_url": WEBHOOK_URL,
        "metadata": {
            "source_bucket": bucket,
            "source_key": key,
        },
    }

    req = urllib.request.Request(
        "https://api.ittybit.com/jobs",
        data=json.dumps(task).encode(),
        headers={
            "Authorization": f"Bearer {ITTYBIT_API_KEY}",
            "Content-Type": "application/json",
        },
        method="POST",
    )

    with urllib.request.urlopen(req) as res:
        data = json.loads(res.read())
        print("Task created:", data["id"])
        return data
````

```bash
# Manual test -- simulate what the Lambda does

TASK='{
  "input": "s3://my-media-bucket/uploads/video.mov",
  "kind": "video",
  "options": {
    "connection_id": "conn_abc123",
    "width": 1920,
    "format": "mp4",
    "quality": "high"
  },
  "webhook_url": "https://xyz.lambda-url.us-east-1.on.aws/webhook",
  "metadata": {
    "source_bucket": "my-media-bucket",
    "source_key": "uploads/video.mov"
  }
}'

curl -X POST https://api.ittybit.com/jobs \
  -H "Authorization: Bearer $ITTYBIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d "$TASK"
```

</CodeGroup>

## Webhook Lambda

Ittybit POSTs to your `webhook_url` when the task completes. This handler writes the result to DynamoDB.

<CodeGroup labels={["TypeScript", "Python", "curl"]}>
```typescript

const db = new DynamoDBClient({});
const TABLE_NAME = process.env.TABLE_NAME!;

export const handler = async (event: { body: string }) => {
  const payload = JSON.parse(event.body);
  const task = payload.data;

await db.send(
new PutItemCommand({
TableName: TABLE_NAME,
Item: marshall({
pk: task.metadata.source_key,
sk: `task#${task.id}`,
status: task.status,
kind: task.kind,
output_url: task.output?.url,
duration_ms: task.duration_ms,
created_at: task.created_at,
completed_at: job.succeeded_at,
}),
})
);

console.log(`Stored result for ${task.id}: ${task.status}`);
return { statusCode: 200, body: "ok" };
};

````

```python

TABLE_NAME = os.environ["TABLE_NAME"]
table = boto3.resource("dynamodb").Table(TABLE_NAME)

def handler(event, context):
    payload = json.loads(event["body"])
    task = payload["data"]

    table.put_item(
        Item={
            "pk": task["metadata"]["source_key"],
            "sk": f"task#{task['id']}",
            "status": task["status"],
            "kind": task["kind"],
            "output_url": task.get("output", {}).get("url"),
            "duration_ms": task.get("duration_ms"),
            "created_at": task["created_at"],
            "completed_at": task.get("completed_at"),
        }
    )

    print(f"Stored result for {task['id']}: {task['status']}")
    return {"statusCode": 200, "body": "ok"}
````

```bash
# Example webhook payload from Ittybit

{
  "event": "job.succeeded",
  "data": {
    "id": "task_abc123",
    "status": "completed",
    "kind": "video",
    "input": "s3://my-media-bucket/uploads/video.mov",
    "output": {
      "url": "https://cdn.ittybit.com/abc/video.mp4"
    },
    "duration_ms": 4820,
    "metadata": {
      "source_bucket": "my-media-bucket",
      "source_key": "uploads/video.mov"
    },
    "created_at": 1775210400000,
    "completed_at": 1775210404000
  }
}
```

</CodeGroup>

## Multiple task types per upload

Kick off several tasks in parallel from the same dispatch Lambda -- video transcode, thumbnail, and audio extraction:

```typescript
const tasks = [
  { input, kind: 'video', options: { connection_id: CONNECTION_ID, width: 1920, format: 'mp4' } },
  {
    input,
    kind: 'image',
    options: { connection_id: CONNECTION_ID, width: 640, format: 'webp', start: 2 },
  },
  {
    input,
    kind: 'audio',
    options: { connection_id: CONNECTION_ID, format: 'mp3', quality: 'medium' },
  },
];

await Promise.all(
  tasks.map((task) =>
    fetch('https://api.ittybit.com/jobs', {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${ITTYBIT_API_KEY}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        ...task,
        webhook_url: WEBHOOK_URL,
        metadata: { source_bucket: bucket.name, source_key: object.key },
      }),
    }),
  ),
);
```

Each task fires its own webhook on completion, so your webhook Lambda handles them independently.

## See also

- [Process files from S3](/guides/process-files-from-s3) -- setting up S3 connections
- [Write output to S3](/guides/write-output-to-s3) -- writing processed files back to your bucket
- [Build a user upload pipeline](/guides/build-a-user-upload-pipeline) -- multi-step processing patterns