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) # Rejectconst 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
endAlways 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.