Set up webhook notifications
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/ittybitconst 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
metadatawhen 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.idto deduplicate.