Webhook Guide

Real-time notifications for MediaConvert.io job events. Webhooks allow you to build event-driven applications that respond immediately to conversion completions, failures, and progress updates.

Overview

MediaConvert.io sends HTTP POST requests to your specified endpoint when job events occur. This enables real-time user notifications, automated workflows, and immediate processing of converted files.

Webhook Events

Job Status Events

Job Completed

json
{
  "job_id": "job_abc123xyz",
  "status": "completed",
  "job_type": "video",
  "progress_percentage": 100,
  "cost_micros": 1575000,
  "cost_cents": 158,
  "processing_time_seconds": 45,
  "output_size_bytes": 8192000,
  "input_format": "mp4",
  "output_format": "webm",
  "created_at": "2024-01-15T10:30:00Z",
  "completed_at": "2024-01-15T10:31:15Z"
}

Job Failed

json
{
  "job_id": "job_def456xyz",
  "status": "failed",
  "job_type": "video",
  "progress_percentage": 25,
  "error_message": "Input file corrupted or unreadable",
  "error_code": "INVALID_INPUT",
  "created_at": "2024-01-15T10:30:00Z",
  "failed_at": "2024-01-15T10:30:45Z"
}

Job Progress (Optional)

json
{
  "job_id": "job_ghi789xyz", 
  "status": "processing",
  "progress_percentage": 67,
  "estimated_completion": "2024-01-15T10:32:00Z"
}

Security

Webhook Signatures

All webhooks include a signature header for verification:

X-MediaConvert-Signature: sha256=abc123def456...

Signature Verification

Ruby Example:
```ruby
def verify_webhook_signature(payload, signature, secret)
expected = OpenSSL::HMAC.hexdigest('sha256', secret, payload)
expected_with_prefix = sha256=#{expected}

Rack::Utils.secure_compare(signature, expected_with_prefix)
end
```

Python Example:
```python
import hashlib
import hmac

def verify_webhook_signature(payload, signature, secret):
expected = hmac.new(
secret.encode('utf-8'),
payload,
hashlib.sha256
).hexdigest()
expected_with_prefix = fsha256={expected}

text
return hmac.compare_digest(signature, expected_with_prefix)
text

**JavaScript Example:**
```javascript
const crypto = require('crypto');

function verifyWebhookSignature(payload, signature, secret) {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(payload)
    .digest('hex');
  const expectedWithPrefix = `sha256=${expected}`;

  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expectedWithPrefix)
  );
}

Implementation Examples

Rails Controller

ruby
class WebhooksController < ApplicationController
  protect_from_forgery except: [:mediaconvert]

  def mediaconvert
    unless verify_signature(request.raw_post, request.headers['X-MediaConvert-Signature'])
      head :unauthorized
      return
    end

    webhook_data = JSON.parse(request.raw_post)

    case webhook_data['status']
    when 'completed'
      handle_completed_job(webhook_data)
    when 'failed'
      handle_failed_job(webhook_data)
    when 'processing'
      update_job_progress(webhook_data)
    end

    head :ok
  end

  private

  def verify_signature(payload, signature)
    return false unless signature

    expected = OpenSSL::HMAC.hexdigest(
      'sha256',
      ENV['MEDIACONVERT_WEBHOOK_SECRET'],
      payload
    )
    expected_with_prefix = "sha256=#{expected}"

    Rack::Utils.secure_compare(signature, expected_with_prefix)
  end

  def handle_completed_job(data)
    job = ConversionJob.find_by(external_id: data['job_id'])
    return unless job

    job.update!(
      status: 'completed',
      progress: 100,
      cost_micros: data['cost_micros'],
      completed_at: Time.current
    )

    # Notify user
    ConversionCompleteJob.perform_later(job.user, job)

    # Process converted file
    ProcessConvertedFileJob.perform_later(job)
  end

  def handle_failed_job(data)
    job = ConversionJob.find_by(external_id: data['job_id'])
    return unless job

    job.update!(
      status: 'failed',
      error_message: data['error_message'],
      failed_at: Time.current
    )

    # Notify user of failure
    ConversionFailedJob.perform_later(job.user, job, data['error_message'])
  end
end

Express.js Handler

javascript
const express = require('express');
const crypto = require('crypto');
const app = express();

// Middleware to capture raw body for signature verification
app.use('/webhooks/mediaconvert', express.raw({type: 'application/json'}));

app.post('/webhooks/mediaconvert', (req, res) => {
  const signature = req.get('X-MediaConvert-Signature');
  const payload = req.body;

  // Verify signature
  if (!verifyWebhookSignature(payload, signature, process.env.MEDIACONVERT_WEBHOOK_SECRET)) {
    return res.status(401).send('Unauthorized');
  }

  const data = JSON.parse(payload);

  switch (data.status) {
    case 'completed':
      handleCompletedJob(data);
      break;
    case 'failed':
      handleFailedJob(data);
      break;
    case 'processing':
      updateJobProgress(data);
      break;
  }

  res.status(200).send('OK');
});

function verifyWebhookSignature(payload, signature, secret) {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(payload)
    .digest('hex');
  const expectedWithPrefix = `sha256=${expected}`;

  return crypto.timingSafeEqual(
    Buffer.from(signature || ''),
    Buffer.from(expectedWithPrefix)
  );
}

Django View

python
import json
import hashlib
import hmac
from django.http import HttpResponse
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_http_methods

@csrf_exempt
@require_http_methods(["POST"])
def mediaconvert_webhook(request):
    signature = request.META.get('HTTP_X_MEDIACONVERT_SIGNATURE')
    payload = request.body

    # Verify signature
    if not verify_webhook_signature(payload, signature, settings.MEDIACONVERT_WEBHOOK_SECRET):
        return HttpResponse(status=401)

    data = json.loads(payload.decode('utf-8'))

    if data['status'] == 'completed':
        handle_completed_job(data)
    elif data['status'] == 'failed':
        handle_failed_job(data)
    elif data['status'] == 'processing':
        update_job_progress(data)

    return HttpResponse(status=200)

def verify_webhook_signature(payload, signature, secret):
    if not signature:
        return False

    expected = hmac.new(
        secret.encode('utf-8'),
        payload,
        hashlib.sha256
    ).hexdigest()
    expected_with_prefix = f"sha256={expected}"

    return hmac.compare_digest(signature, expected_with_prefix)

Best Practices

Reliability

  • Idempotency: Handle duplicate webhooks gracefully
  • Timeouts: Respond within 10 seconds to avoid retries
  • Status Codes: Return 2xx for successful processing
  • Logging: Log all webhook events for debugging

Security

  • Always verify signatures before processing webhooks
  • Use HTTPS endpoints to protect webhook data in transit
  • Validate payload structure before processing
  • Rate limiting to prevent webhook abuse

Error Handling

  • Return 2xx even for business logic failures to prevent retries
  • Queue failed processing for later retry
  • Monitor webhook endpoint health and failures
  • Alert on signature verification failures

Testing Webhooks

Development Setup

bash
# Use ngrok for local webhook testing
ngrok http 3000

# Your webhook URL becomes:
# https://abc123.ngrok.io/webhooks/mediaconvert

Test Webhook Delivery

bash
# Send test webhook to your endpoint
curl -X POST "https://your-app.com/webhooks/mediaconvert" \
  -H "Content-Type: application/json" \
  -H "X-MediaConvert-Signature: sha256=test_signature" \
  -d '{
    "job_id": "job_test123",
    "status": "completed",
    "progress_percentage": 100,
    "cost_micros": 150000,
    "processing_time_seconds": 45
  }'

Webhook Debugging

  • Check logs for signature verification failures
  • Validate JSON parsing doesn't fail on malformed data
  • Test timeout handling with slow webhook endpoints
  • Monitor delivery success rates in your application

Webhook Configuration

Setting Webhook URL

Specify webhook URL when creating conversion jobs:
json
{
"job_type": "video",
"input_url": "https://...",
"output_url": "https://...",
"webhook_url": "https://your-app.com/webhooks/mediaconvert"
}

Global Webhook Settings

Configure account-wide webhook preferences in your dashboard:
- Default webhook URL
- Event type preferences
- Retry configuration
- Delivery timeout settings

Troubleshooting

Common Issues

Signature Verification Failures
- Check webhook secret configuration
- Ensure raw request body is used for verification
- Verify HMAC calculation implementation

Missing Webhooks
- Check endpoint availability and response time
- Verify HTTPS certificate validity
- Monitor for rate limiting or blocking

Duplicate Processing
- Implement idempotency keys
- Check for webhook retry logic
- Use database constraints to prevent duplicates

Next Steps

Support