# Process uploads in Next.js with Ittybit

Handle media uploads with Next.js Server Actions and Ittybit webhook callbacks

Users upload a file through a form. A Server Action stores it and dispatches a processing task to Ittybit. When the task finishes, Ittybit POSTs a webhook to your Route Handler so you can update your database.

## Create a task

Once the file is stored (S3, Vercel Blob, wherever), POST the URL to Ittybit:

<CodeGroup labels={["TypeScript", "curl"]}>
```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://your-bucket.s3.amazonaws.com/uploads/video.mov",
    kind: "video",
    options: {
      width: 1280,
      format: "mp4",
      quality: "high",
    },
  }),
});
const { id, status } = await res.json();
// id: "task_abc123", status: "pending"
```

```bash
curl -X POST https://api.ittybit.com/jobs \
  -H "Authorization: Bearer $ITTYBIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "input": "https://your-bucket.s3.amazonaws.com/uploads/video.mov",
    "kind": "video",
    "options": {
      "width": 1280,
      "format": "mp4",
      "quality": "high"
    }
  }'
```

</CodeGroup>

The response includes a task `id` and `status: "pending"`. Ittybit will POST to your configured webhook URL when processing completes.

## Server Action

Handle the form submission, upload the file, and create the Ittybit task in a single action.

```typescript
// app/actions.ts
'use server';

export async function uploadMedia(formData: FormData) {
  const file = formData.get('file') as File;
  if (!file) throw new Error('No file provided');

  // 1. Store the file
  const blob = await put(file.name, file, { access: 'public' });

  // 2. Save a record in your database
  const record = await db.media.create({
    data: {
      filename: file.name,
      sourceUrl: blob.url,
      status: 'processing',
    },
  });

  // 3. Dispatch to Ittybit
  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: blob.url,
      kind: 'video',
      options: { width: 1280, format: 'mp4', quality: 'high' },
      metadata: { media_id: record.id },
    }),
  });

  if (!res.ok) {
    throw new Error('Failed to create task');
  }

  revalidatePath('/uploads');
}
```

The `metadata` field is passed through to the webhook payload, so you can tie the callback back to your database record.

## Webhook Route Handler

When Ittybit finishes processing, it POSTs to your webhook endpoint. Create a Route Handler to receive it.

```typescript
// app/api/webhooks/ittybit/route.ts

export async function POST(request: Request) {
  const body = await request.json();
  const { id, status, kind, output, metadata } = body;

  if (status === 'completed' && metadata?.media_id) {
    await db.media.update({
      where: { id: metadata.media_id },
      data: {
        status: 'ready',
        outputUrl: output?.url,
      },
    });
  }

  if (status === 'failed' && metadata?.media_id) {
    await db.media.update({
      where: { id: metadata.media_id },
      data: { status: 'failed' },
    });
  }

  return NextResponse.json({ received: true });
}
```

## Upload form

Wire the Server Action to a form component:

```tsx
// app/uploads/page.tsx

export default function UploadPage() {
  return (
    <form action={uploadMedia}>
      <input type="file" name="file" accept="video/*" required />
      <button type="submit">Upload</button>
    </form>
  );
}
```

## See also

- [Build a user upload pipeline](/guides/build-a-user-upload-pipeline)
- [Process files from S3](/guides/process-files-from-s3)
- [Write output to S3](/guides/write-output-to-s3)