Search Documentation
Search across all documentation pages
Webhooks

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

EventTrigger
job.completedAll outputs finished successfully
job.failedJob failed with an error
job.canceledJob was canceled by the user
job.progressJob progress updated (throttled, not every percentage)
output.completedA single output within a job finished
output.failedA 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

  1. Extract the raw request body (before any JSON parsing)
  2. Extract the X-Transcodely-Signature header
  3. Compute HMAC-SHA256 of the raw body using your webhook secret
  4. 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', 200
func 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:

AttemptDelay
1st retry30 seconds
2nd retry2 minutes
3rd retry10 minutes
4th retry1 hour
5th retry4 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

  1. Respond quickly. Return a 200 status code as soon as you receive the event. Process the payload asynchronously if it triggers slow operations.
  2. Handle duplicates. In rare cases, the same event may be delivered more than once. Use the event id to deduplicate.
  3. Verify signatures. Always validate the X-Transcodely-Signature header before processing events.
  4. Use HTTPS. Webhook URLs must use HTTPS in production to protect payloads in transit.
  5. Monitor failures. If your endpoint consistently fails, webhooks will be paused and you will be notified.