# Set up webhook notifications

Get notified when media processing tasks complete

Polling wastes requests. Webhooks push the result to you the moment a job finishes. Pass a `webhook_url` when creating a job and Ittybit will POST the completed job payload to that URL.

"Clippable" (a video editing SaaS like Kapwing) needs to update its UI the instant an export finishes.

## API

<CodeGroup labels={["CLI", "TypeScript", "Python", "curl"]}>
```bash
ittybit video \
  -i https://clippable-app.com/projects/edit-38271.mov \
  --format mp4 \
  --width 1920 \
  --webhook_url https://clippable-app.com/webhooks/ittybit
```

```typescript
const task = {
  input: 'https://clippable-app.com/projects/edit-38271.mov',
  kind: 'video',
  options: {
    format: 'mp4',
    width: 1920,
  },
  webhook_url: 'https://clippable-app.com/webhooks/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(task),
});
const data = await res.json();
```

```python

task = {
    "input": "https://clippable-app.com/projects/edit-38271.mov",
    "kind": "video",
    "options": {
        "format": "mp4",
        "width": 1920,
    },
    "webhook_url": "https://clippable-app.com/webhooks/ittybit",
}

res = requests.post(
    "https://api.ittybit.com/jobs",
    headers={"Authorization": f"Bearer {api_key}"},
    json=task,
)
data = res.json()
```

```bash
TASK='{
  "input": "https://clippable-app.com/projects/edit-38271.mov",
  "kind": "video",
  "options": {
    "format": "mp4",
    "width": 1920
  },
  "webhook_url": "https://clippable-app.com/webhooks/ittybit"
}'

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

</CodeGroup>

## CLI

```bash
ittybit video \
  -i edit-38271.mov \
  -o export.mp4 \
  --width 1920
```

The CLI polls for completion directly, so webhooks aren't needed.

## Webhook payload

When the task completes (or fails), Ittybit POSTs a JSON payload to your `webhook_url`:

```json
{
  "id": "task_a1b2c3d4",
  "status": "succeeded",
  "kind": "video",
  "input": "https://clippable-app.com/projects/edit-38271.mov",
  "output": {
    "url": "https://cdn.ittybit.com/clippable/export-38271.mp4",
    "format": "mp4",
    "width": 1920,
    "height": 1080,
    "duration": 124.5,
    "size": 48230912
  },
  "metadata": {},
  "created_at": 1775212200000,
  "completed_at": 1775212274000
}
```

On failure, `status` is `"failed"` and an `error` field contains the reason.

## Verify the signature

Ittybit signs every webhook with an HMAC-SHA256 signature in the `Ittybit-Signature` header. Always verify it before trusting the payload.

## Receive webhooks (Express)

```typescript

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

app.post('/webhooks/ittybit', (req, res) => {
  const signature = req.headers['ittybit-signature'] as string;
  const computed = crypto
    .createHmac('sha256', process.env.ITTYBIT_WEBHOOK_SECRET!)
    .update(JSON.stringify(req.body))
    .digest('hex');

  if (!signature || !crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(computed))) {
    return res.status(401).send('Invalid signature');
  }

  const task = req.body;

  if (task.status === 'succeeded') {
    // Update your database, notify the user, trigger next steps
    console.log(`Task ${task.id} completed: ${task.output.url}`);
  } else {
    console.error(`Task ${task.id} failed: ${task.error}`);
  }

  res.sendStatus(200);
});

app.listen(3000);
```

## Receive webhooks (Flask)

```python

from flask import Flask, request, jsonify

app = Flask(__name__)

WEBHOOK_SECRET = os.environ["ITTYBIT_WEBHOOK_SECRET"]

@app.route("/webhooks/ittybit", methods=["POST"])
def handle_webhook():
    signature = request.headers.get("Ittybit-Signature")
    computed = hmac.new(
        WEBHOOK_SECRET.encode(),
        request.get_data(),
        hashlib.sha256,
    ).hexdigest()

    if not signature or not hmac.compare_digest(signature, computed):
        return "Invalid signature", 401

    task = request.get_json()

    if task["status"] == "succeeded":
        print(f"Task {task['id']} completed: {task['output']['url']}")
    else:
        print(f"Task {task['id']} failed: {task.get('error')}")

    return jsonify({"status": "ok"}), 200
```

## Tips

- **Return 200 quickly.** Do heavy work asynchronously. Ittybit retries on 5xx responses, so a slow handler can cause duplicate deliveries.
- **Use metadata.** Pass `metadata` when creating the task and it comes back in the webhook -- no extra database lookup to match the result to your records.
- **Use HTTPS.** Webhook URLs must be HTTPS in production. For local development, use a tunnel like ngrok or Cloudflare Tunnel.
- **Idempotency.** Webhooks can be delivered more than once. Use the `task.id` to deduplicate.