# Media processing pipeline on Railway with Ittybit

Deploy a webhook-driven media processing backend on Railway

Instead of polling for task completion, let Ittybit push results to you. Deploy a small webhook receiver on Railway, submit media for processing, and get async results posted back to your endpoint when each task finishes.

## Project setup

Scaffold a new project with an Express server and a Postgres client:

```bash
mkdir ittybit-pipeline && cd ittybit-pipeline
npm init -y
npm install express pg
```

Set your environment variables in the Railway dashboard or via `railway variables set`:

```bash
ITTYBIT_API_KEY=your_ittybit_api_key
DATABASE_URL=your_railway_postgres_url
RAILWAY_PUBLIC_DOMAIN=your-app.up.railway.app
```

## Create the database table

Create a `media_jobs` table to track submissions and results:

```sql
CREATE TABLE media_jobs (
  id SERIAL PRIMARY KEY,
  task_id TEXT UNIQUE,
  input_url TEXT NOT NULL,
  status TEXT DEFAULT 'pending',
  output_url TEXT,
  created_at TIMESTAMPTZ DEFAULT now(),
  completed_at TIMESTAMPTZ
);
```

## Build the server

The server has two routes: one to accept media URLs and dispatch tasks, and one to receive webhook callbacks from Ittybit.

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

const app = express();
app.use(express.json());

const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL });
const ITTYBIT_API_KEY = process.env.ITTYBIT_API_KEY;
const RAILWAY_DOMAIN = process.env.RAILWAY_PUBLIC_DOMAIN;

// Accept a media URL, dispatch an Ittybit task with a webhook
app.post("/process", async (req, res) => {
const { url, kind = "video", options = {} } = req.body;

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

const data = await task.json();

await pool.query(
"INSERT INTO media_jobs (task_id, input_url, status) VALUES ($1, $2, $3)",
[data.id, url, "processing"]
);

res.json({ task_id: data.id, status: "processing" });
});

// Receive completion webhooks from Ittybit
app.post("/webhooks/ittybit", async (req, res) => {
const { id, status, output } = req.body;

await pool.query(
`UPDATE media_jobs
     SET status = $1, output_url = $2, completed_at = now()
     WHERE task_id = $3`,
[status, output?.url ?? null, id]
);

res.sendStatus(200);
});

// Check job status
app.get("/jobs/:taskId", async (req, res) => {
const { rows } = await pool.query(
"SELECT \* FROM media_jobs WHERE task_id = $1",
[req.params.taskId]
);
if (!rows.length) return res.status(404).json({ error: "not found" });
res.json(rows[0]);
});

app.listen(process.env.PORT || 3000);

````

```bash
# Submit a video for processing
curl -X POST https://your-app.up.railway.app/process \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/uploads/video.mov",
    "kind": "video",
    "options": { "width": 1280, "format": "mp4", "quality": "high" }
  }'

# Check status
curl https://your-app.up.railway.app/jobs/task_abc123
````

</CodeGroup>

## Deploy to Railway

Connect your repo to Railway via GitHub or push directly:

```bash
railway up
```

Railway auto-detects the Node.js project, installs dependencies, and starts the server. Add a Postgres plugin from the Railway dashboard and the `DATABASE_URL` is injected automatically.

## What happens at runtime

1. You POST a media URL to `/process`
2. Your server creates an Ittybit task with `webhook_url` pointing to your Railway domain
3. Ittybit processes the media asynchronously
4. On completion, Ittybit POSTs the result to `/webhooks/ittybit`
5. Your server updates Postgres with the output URL and status

## Multiple tasks per upload

Submit several tasks for the same input to build a full pipeline:

<CodeGroup labels={["TypeScript", "curl"]}>
```typescript
const tasks = [
  { kind: "video", options: { width: 1920, format: "mp4", quality: "high" } },
  { kind: "image", options: { width: 640, format: "webp", start: 2 } },
  { kind: "video", options: { width: 480, format: "mp4", quality: "low" } },
];

const results = await Promise.all(
tasks.map((task) =>
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",
...task,
webhook_url: `https://${RAILWAY_DOMAIN}/webhooks/ittybit`,
}),
}).then((r) => r.json())
)
);

````

```bash
# HD version
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": { "width": 1920, "format": "mp4", "quality": "high" },
    "webhook_url": "https://your-app.up.railway.app/webhooks/ittybit"
  }'

# Thumbnail
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": "image",
    "options": { "width": 640, "format": "webp", "start": 2 },
    "webhook_url": "https://your-app.up.railway.app/webhooks/ittybit"
  }'

# Low-res preview
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": { "width": 480, "format": "mp4", "quality": "low" },
    "webhook_url": "https://your-app.up.railway.app/webhooks/ittybit"
  }'
````

</CodeGroup>

Each task completes independently and fires its own webhook. Your `/webhooks/ittybit` handler updates the database as each result arrives.

## See also

- [Ittybit Task API reference](/reference/tasks)
- [Build a user upload pipeline](/guides/build-a-user-upload-pipeline)
- [Railway docs](https://docs.railway.com)