Webhooks
Webhooks allow you to receive real-time HTTP notifications when events occur, like when a video finishes generating. This eliminates the need for polling.
Coming Soon
How Webhooks Work
When you configure a webhook, Twin.Actor will send an HTTP POST request to your specified URL whenever certain events occur:
Event Types
| Event | Description |
|---|---|
project.created | A new project was created |
project.processing | Video generation started |
project.completed | Video is ready for download |
project.failed | Generation failed |
voice_clone.completed | Voice clone is ready |
voice_clone.failed | Voice cloning failed |
credits.low | Credit balance below threshold |
credits.depleted | Credit balance is zero |
Webhook Payload
All webhook payloads follow this structure:
{
"id": "evt_abc123",
"type": "project.completed",
"created_at": "2024-03-20T10:15:00Z",
"data": {
"project_id": 42,
"project_type": "scene_project",
"video_url": "https://storage.twin.actor/videos/project_42_final.mp4",
"duration_seconds": 50,
"credits_used": 500
}
}project.completed
{
"id": "evt_abc123",
"type": "project.completed",
"created_at": "2024-03-20T10:15:00Z",
"data": {
"project_id": 42,
"project_type": "scene_project",
"video_url": "https://storage.twin.actor/videos/project_42_final.mp4",
"video_url_upscaled": null,
"thumbnail_url": "https://storage.twin.actor/thumbs/project_42.jpg",
"duration_seconds": 50,
"credits_used": 500,
"person_id": 1,
"person_name": "Alex Chen"
}
}project.failed
{
"id": "evt_abc124",
"type": "project.failed",
"created_at": "2024-03-20T10:15:00Z",
"data": {
"project_id": 42,
"project_type": "scene_project",
"error_code": "VIDEO_GENERATION_FAILED",
"error_message": "Scene 3 video generation failed after 3 retries",
"failed_scene": 3,
"credits_refunded": 200
}
}voice_clone.completed
{
"id": "evt_abc125",
"type": "voice_clone.completed",
"created_at": "2024-03-20T10:05:00Z",
"data": {
"person_id": 1,
"person_name": "Alex Chen",
"voice_id": "voice_abc123",
"provider": "elevenlabs",
"credits_used": 500
}
}Signature Verification
Every webhook request includes a signature header for verification:
X-Webhook-Signature</span>: t=1679529600,v1=abc123def456...
X-Webhook-Event</span>: project.completed
X-Webhook-ID</span>: evt_abc123The signature is a HMAC-SHA256 hash of the timestamp and payload. Always verify signatures to ensure requests are from Twin.Actor:
"text-violet-400">import hmac
"text-violet-400">import hashlib
"text-violet-400">import time
"text-violet-400">from flask "text-violet-400">import Flask, request, jsonify
app = Flask(__name__)
WEBHOOK_SECRET = "your_webhook_secret"
"text-violet-400">def verify_signature(payload: bytes, signature: str) -> bool:
"""Verify webhook signature ">with timestamp validation."""
"text-violet-400">try:
parts = dict(p.split("=") "text-violet-400">for p "text-violet-400">in signature.split(","))
timestamp = int(parts["t"])
provided_sig = parts["v1"]
# Reject old timestamps (5 minute window)
"text-violet-400">if abs(time.time() - timestamp) > 300:
"text-violet-400">return "text-violet-400">False
# Compute expected signature
signed_payload = f"{timestamp}.{payload.decode()}"
expected = hmac.new(
WEBHOOK_SECRET.encode(),
signed_payload.encode(),
hashlib.sha256
).hexdigest()
"text-violet-400">return hmac.compare_digest(provided_sig, expected)
"text-violet-400">except:
"text-violet-400">return "text-violet-400">False
@app.route("/webhooks/twin-actor", methods=["POST"])
"text-violet-400">def handle_webhook():
signature = request.headers.get("X-Webhook-Signature", "")
"text-violet-400">if "text-violet-400">not verify_signature(request.data, signature):
"text-violet-400">return jsonify({"error": "Invalid signature"}), 401
event = request.json
event_type = event["type"]
"text-violet-400">if event_type == "project.completed":
video_url = event["data"]["video_url"]
# Download video, notify user, etc.
print(f"Video ready: {video_url}")
"text-violet-400">elif event_type == "project.failed":
error = event["data"]["error_message"]
# Handle failure, alert team, etc.
print(f"Project failed: {error}")
"text-violet-400">return jsonify({"received": "text-violet-400">True}), 200Best Practices
- Always verify signatures - Never process unsigned webhook requests
- Respond quickly - Return a 2xx status within 30 seconds. Process events asynchronously if needed.
- Handle duplicates - Use the event ID to deduplicate. Webhooks may be retried if your server doesn't respond.
- Use HTTPS - Webhook endpoints must use HTTPS in production
- Log events - Store raw payloads for debugging and auditing
Retry Policy
If your endpoint returns a non-2xx status or times out, we'll retry with exponential backoff:
| Attempt | Delay |
|---|---|
| 1 | Immediate |
| 2 | 1 minute |
| 3 | 5 minutes |
| 4 | 15 minutes |
| 5 | 1 hour |
| 6 | 2 hours (final) |
After 6 failed attempts, the webhook endpoint will be disabled. Check the developer portal to view delivery logs and re-enable.
Current Alternative: Firestore
While webhooks are being developed, you can use Firestore real-time updates to track project status:
"text-violet-400">import { doc, onSnapshot } "text-violet-400">from "firebase/firestore";
"text-violet-400">import { db } "text-violet-400">from "./firebase";
// Listen "text-violet-400">for project status updates
"text-violet-400">const unsubscribe = onSnapshot(
doc(db, "projects", projectId),
(doc) => {
"text-violet-400">const data = doc.data();
console.log("Status:", data.status);
console.log("Progress:", data.progress);
"text-violet-400">if (data.status === "completed") {
console.log("Video URL:", data.final_video_url);
unsubscribe(); // Stop listening
}
}
);