Set up webhook notifications

View Markdown

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

ittybit video \
  -i https://clippable-app.com/projects/edit-38271.mov \
  --format mp4 \
  --width 1920 \
  --webhook_url https://clippable-app.com/webhooks/ittybit
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();
import requests

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

CLI

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:

{
  "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)

import express from 'express';
import crypto from 'crypto';

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)

import hmac
import hashlib
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.