Webhooks

Receive real-time notifications when jobs complete or fail.

Overview

Instead of polling /v1/jobs/{id} repeatedly, you can configure webhook endpoints to receive push notifications. When a job completes or fails, VideoConduit sends an HTTP POST request to your configured URL with the full job details. This eliminates the need for polling loops and lets your application react to events the moment they happen.

Every webhook delivery is signed with HMAC-SHA256, so you can verify that the request is authentic and hasn’t been tampered with. You’ll receive a signing secret when you create a webhook endpoint — use it to validate the X-VideoConduit-Signature header on each delivery.

Setting Up Webhooks

There are two ways to receive webhook notifications:

1. Configured Webhook Endpoints

Set up persistent webhook URLs via the API. These endpoints receive events for all your jobs automatically. This is the recommended approach for production systems where you want a centralized handler for all VideoConduit events.

2. Per-Job Webhook URL

Pass a webhook_url parameter when creating a job. That URL receives events only for that specific job. This is useful when you want different handlers for different workflows, or when you’re integrating webhooks into an existing pipeline without setting up a global endpoint.

Webhook Management API

Method Endpoint Description
POST /v1/webhooks Create a webhook endpoint
GET /v1/webhooks List all webhook endpoints
GET /v1/webhooks/{id} Get webhook details
PATCH /v1/webhooks/{id} Update a webhook endpoint
DELETE /v1/webhooks/{id} Delete a webhook endpoint
GET /v1/webhooks/{id}/deliveries List delivery history
POST /v1/webhooks/{id}/test Send a test event

Creating a Webhook

curl -X POST "https://videoconduit.com/v1/webhooks" \
  -H "Authorization: Bearer vc_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://yourapp.com/webhooks/videoconduit",
    "events": ["job.completed", "job.failed"]
  }'
import requests

response = requests.post(
    "https://videoconduit.com/v1/webhooks",
    headers={"Authorization": "Bearer vc_your_api_key"},
    json={
        "url": "https://yourapp.com/webhooks/videoconduit",
        "events": ["job.completed", "job.failed"],
    },
)
webhook = response.json()
print(webhook["id"], webhook["secret"])
const response = await fetch("https://videoconduit.com/v1/webhooks", {
  method: "POST",
  headers: {
    "Authorization": "Bearer vc_your_api_key",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    url: "https://yourapp.com/webhooks/videoconduit",
    events: ["job.completed", "job.failed"],
  }),
});
const webhook = await response.json();
console.log(webhook.id, webhook.secret);
$client = new GuzzleHttp\Client();
$response = $client->post("https://videoconduit.com/v1/webhooks", [
    "headers" => ["Authorization" => "Bearer vc_your_api_key"],
    "json" => [
        "url" => "https://yourapp.com/webhooks/videoconduit",
        "events" => ["job.completed", "job.failed"],
    ],
]);
$webhook = json_decode($response->getBody(), true);
echo $webhook["id"] . " " . $webhook["secret"];
body := strings.NewReader(`{
  "url": "https://yourapp.com/webhooks/videoconduit",
  "events": ["job.completed", "job.failed"]
}`)
req, _ := http.NewRequest("POST", "https://videoconduit.com/v1/webhooks", body)
req.Header.Set("Authorization", "Bearer vc_your_api_key")
req.Header.Set("Content-Type", "application/json")
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
var webhook map[string]interface{}
json.NewDecoder(resp.Body).Decode(&webhook)
require "net/http"
require "json"

uri = URI("https://videoconduit.com/v1/webhooks")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer vc_your_api_key"
req["Content-Type"] = "application/json"
req.body = {
  url: "https://yourapp.com/webhooks/videoconduit",
  events: ["job.completed", "job.failed"],
}.to_json
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
webhook = JSON.parse(res.body)
puts webhook["id"], webhook["secret"]

Response:

{
  "id": 1,
  "url": "https://yourapp.com/webhooks/videoconduit",
  "secret": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2",
  "events": ["job.completed", "job.failed"],
  "is_active": true,
  "created_at": "2025-01-15T10:00:00Z"
}

Store Your Secret

The secret is only returned once — on creation. Store it securely. You'll need it to verify webhook signatures.

Event Types

Event Trigger
job.completed A job finished successfully. result_data and download_url are available.
job.failed A job failed. error_message has details. Credits are refunded.

Receive All Events

Set events to an empty array [] when creating the webhook to receive all event types.

Webhook Payload

Every webhook delivery sends a JSON payload with the event type and full job data:

{
  "event": "job.completed",
  "timestamp": "2025-01-15T10:05:00Z",
  "data": {
    "job_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "job_type": "download",
    "status": "completed",
    "source_url": "https://youtube.com/watch?v=dQw4w9WgXcQ",
    "download_url": "https://dl.videoconduit.com/files/a1b2c3d4.mp4",
    "result_data": {
      "title": "...",
      "format": "mp4",
      "filesize": 15234567
    },
    "credits_charged": 1,
    "created_at": "2025-01-15T10:00:00Z",
    "completed_at": "2025-01-15T10:05:00Z"
  }
}

Verifying Webhook Signatures

Every webhook delivery includes an X-VideoConduit-Signature header containing an HMAC-SHA256 signature. Verify it to ensure the request is authentic and hasn’t been tampered with.

The signature is computed as: HMAC-SHA256(webhook_secret, request_body)

import hmac
import hashlib


def verify_webhook(payload: bytes, signature: str, secret: str) -> bool:
    expected = hmac.new(
        secret.encode(),
        payload,
        hashlib.sha256,
    ).hexdigest()
    return hmac.compare_digest(expected, signature)


# In your webhook handler:
signature = request.headers.get("X-VideoConduit-Signature")
if not verify_webhook(request.body, signature, WEBHOOK_SECRET):
    return HttpResponse(status=401)  # Reject
const crypto = require("crypto");

function verifyWebhook(payload, signature, secret) {
  const expected = crypto
    .createHmac("sha256", secret)
    .update(payload)
    .digest("hex");
  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(signature)
  );
}

// In your webhook handler:
const signature = req.headers["x-videoconduit-signature"];
if (!verifyWebhook(req.body, signature, WEBHOOK_SECRET)) {
  return res.status(401).send();
}
function verifyWebhook(string $payload, string $signature, string $secret): bool {
    $expected = hash_hmac("sha256", $payload, $secret);
    return hash_equals($expected, $signature);
}

// In your webhook handler:
$payload = file_get_contents("php://input");
$signature = $_SERVER["HTTP_X_VIDEOCONDUIT_SIGNATURE"] ?? "";
if (!verifyWebhook($payload, $signature, $webhookSecret)) {
    http_response_code(401);
    exit;
}
import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
)

func verifyWebhook(payload []byte, signature, secret string) bool {
    mac := hmac.New(sha256.New, []byte(secret))
    mac.Write(payload)
    expected := hex.EncodeToString(mac.Sum(nil))
    return hmac.Equal([]byte(expected), []byte(signature))
}

// In your webhook handler:
// signature := r.Header.Get("X-VideoConduit-Signature")
// if !verifyWebhook(body, signature, webhookSecret) {
//     w.WriteHeader(http.StatusUnauthorized)
// }
require "openssl"

def verify_webhook(payload, signature, secret)
  expected = OpenSSL::HMAC.hexdigest("SHA256", secret, payload)
  Rack::Utils.secure_compare(expected, signature)
end

# In your webhook handler:
signature = request.env["HTTP_X_VIDEOCONDUIT_SIGNATURE"]
unless verify_webhook(request.body.read, signature, WEBHOOK_SECRET)
  halt 401
end

Always Verify Signatures

Never process a webhook delivery without verifying the signature first. This protects you from spoofed requests.

Delivery & Retries

VideoConduit expects your endpoint to return a 2xx status code within 10 seconds. If the delivery fails (non-2xx response, timeout, or connection error), it retries up to 3 times with exponential backoff: 1 minute, 5 minutes, then 30 minutes after the initial attempt.

All deliveries are logged and can be inspected via GET /v1/webhooks/{id}/deliveries. Each delivery record includes the HTTP status code, response time, and any error details so you can diagnose issues with your endpoint.

Testing Webhooks

Use the test endpoint to send a sample event to your webhook URL without creating a real job:

curl -X POST "https://videoconduit.com/v1/webhooks/1/test" \
  -H "Authorization: Bearer vc_your_api_key"

This sends a synthetic job.completed event with dummy data to your webhook URL, signed with your real webhook secret. The delivery is logged just like a real event.

Local Development

For local development, use tools like ngrok or localtunnel to expose your local server to the internet so VideoConduit can reach your webhook endpoint.

Best Practices

Respond quickly to webhook deliveries — return a 200 status code as soon as you receive the request, then do any heavy processing asynchronously in a background job. If your handler takes too long, VideoConduit will time out and retry, potentially causing duplicate deliveries.

Always verify the X-VideoConduit-Signature header on every request. Use the event type field to route handling logic rather than assuming all events are job.completed — your endpoint may receive job.failed events too, and future event types may be added.

Make your handler idempotent, since the same event may be delivered more than once during retries. Use the job_id to deduplicate if needed. Store your webhook secret securely in environment variables — never hard-code it in your source code or commit it to version control.

This site uses only essential cookies required for the service to function (session authentication and security). We do not use analytics, tracking, or advertising cookies. Learn more