Webhooks
Webhooks let your application receive real-time HTTP notifications when job events occur — a job completes, fails, makes progress, or gets canceled. Instead of polling the API, Transcodely pushes events to your server as they happen.
Setup
Webhooks are configured at the App level. All jobs within an app share the same webhook endpoint:
curl -X POST https://api.transcodely.com/transcodely.v1.AppService/Update
-H "Authorization: Bearer {{API_KEY}}"
-H "X-Organization-ID: org_a1b2c3d4e5"
-H "Content-Type: application/json"
-d '{
"id": "app_k1l2m3n4o5",
"webhook": {
"url": "https://api.yourapp.com/webhooks/transcodely",
"regenerate_secret": true,
"events": ["job.completed", "job.failed"]
}
}'The response includes a one-time webhook_secret — store it securely for signature verification.
You can also configure webhooks when creating an app, or override the webhook URL on a per-job basis using the webhook_url field in the job creation request.
Event Types
| Event | Trigger |
|---|---|
job.completed | All outputs finished successfully |
job.failed | Job failed with an error |
job.canceled | Job was canceled by the user |
job.progress | Job progress updated (throttled, not every percentage) |
output.completed | A single output within a job finished |
output.failed | A single output within a job failed |
If no events are configured, the default subscription is ["job.completed", "job.failed"].
Payload Format
Webhook notifications are sent as POST requests with a JSON body:
{
"id": "evt_m7n8o9p0q1r2s3t4",
"type": "job.completed",
"created_at": "2026-01-15T10:35:00Z",
"data": {
"job": {
"id": "job_a1b2c3d4e5f6",
"status": "completed",
"progress": 100,
"priority": "standard",
"total_estimated_cost": 0.045,
"total_actual_cost": 0.043,
"currency": "EUR",
"outputs": [
{
"id": "out_x1y2z3",
"status": "completed",
"progress": 100,
"output_url": "gs://acme-video-assets/videos/2026-01-15/job_a1b2c3/h264_1080p.mp4",
"output_size_bytes": 15234567,
"estimated_cost": 0.045,
"actual_cost": 0.043
}
],
"metadata": {
"user_id": "usr_12345"
},
"created_at": "2026-01-15T10:30:00Z",
"completed_at": "2026-01-15T10:35:00Z"
}
}
}The data.job object contains the full job resource at the time of the event, including all outputs, costs, and metadata.
Signature Verification
Every webhook request includes an HMAC-SHA256 signature in the X-Transcodely-Signature header. Always verify this signature to ensure the request is authentic and has not been tampered with.
Verification Steps
- Extract the raw request body (before any JSON parsing)
- Extract the
X-Transcodely-Signatureheader - Compute HMAC-SHA256 of the raw body using your webhook secret
- Compare the computed signature with the header value (constant-time comparison)
import crypto from 'node:crypto';
function verifyWebhookSignature(
body: string,
signature: string,
secret: string
): boolean {
const expected = crypto
.createHmac('sha256', secret)
.update(body)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}
// In your webhook handler
app.post('/webhooks/transcodely', (req, res) => {
const signature = req.headers['x-transcodely-signature'];
const isValid = verifyWebhookSignature(req.rawBody, signature, WEBHOOK_SECRET);
if (!isValid) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(req.rawBody);
// Process the event...
res.status(200).send('OK');
});import hashlib
import hmac
def verify_webhook_signature(body: bytes, signature: str, secret: str) -> bool:
expected = hmac.new(
secret.encode('utf-8'),
body,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected)
# In your webhook handler (Flask)
@app.route('/webhooks/transcodely', methods=['POST'])
def handle_webhook():
signature = request.headers.get('X-Transcodely-Signature')
if not verify_webhook_signature(request.data, signature, WEBHOOK_SECRET):
return 'Invalid signature', 401
event = request.get_json()
# Process the event...
return 'OK', 200func verifyWebhookSignature(body []byte, signature, secret string) bool {
mac := hmac.New(sha256.New, []byte(secret))
mac.Write(body)
expected := hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(signature), []byte(expected))
}Retry Policy
If your endpoint returns a non-2xx status code (or the request times out), Transcodely retries the delivery with exponential backoff:
| Attempt | Delay |
|---|---|
| 1st retry | 30 seconds |
| 2nd retry | 2 minutes |
| 3rd retry | 10 minutes |
| 4th retry | 1 hour |
| 5th retry | 4 hours |
After 5 failed attempts, the event is marked as failed and no further retries are attempted. Your endpoint should respond within 30 seconds to avoid a timeout.
Best Practices
- Respond quickly. Return a
200status code as soon as you receive the event. Process the payload asynchronously if it triggers slow operations. - Handle duplicates. In rare cases, the same event may be delivered more than once. Use the event
idto deduplicate. - Verify signatures. Always validate the
X-Transcodely-Signatureheader before processing events. - Use HTTPS. Webhook URLs must use HTTPS in production to protect payloads in transit.
- Monitor failures. If your endpoint consistently fails, webhooks will be paused and you will be notified.