Webhooks
Get a signed HTTP call when a conversion or job finishes. The events with their payloads, and how to check the signature in your code.
Instead of polling, let switchfile call your server when work is done. Webhooks are POST requests with a JSON body and a signature header.
Simple API (v1)#
Set a webhook URL for the whole account in the dashboard under Webhooks or with PATCH /v1/account, or per request with webhook_url in POST /v1/convert and POST /v1/batches. A URL in the request wins over the account setting.
| Event | When |
|---|---|
conversion.finished | A conversion is done and its result can be downloaded |
conversion.failed | A conversion failed. data.error has the code |
{
"event": "conversion.finished",
"created_at": "2026-11-06T18:00:02+00:00",
"data": {
"id": "0b8a7c1e-5d0f-4a51-9f39-2f7f4c1d9a10",
"object": "conversion",
"status": "finished",
"download_url": "https://api.switchfile.io/files/..."
}
}data is the full conversion object, shortened here. The headers are Switchfile-Event with the event name and Switchfile-Signature with the signature. In the dashboard you can choose which events the account URL gets.
Jobs and tasks (v2)#
Set webhook_url when you create the job. When the job ends, switchfile sends job.finished or job.failed with the job and all its tasks, in the format of the CloudConvert API v2:
{
"event": "job.finished",
"job": {
"id": "6e2c1b1f-3c1a-4d4e-9a57-0d7b2f6a8e10",
"tag": "invoice-4711",
"status": "finished",
"tasks": [{ "name": "export-1", "operation": "export/url", "status": "finished", "result": { "files": [] } }]
}
}The signature is in the header CloudConvert-Signature, so existing webhook handlers keep working.
Check the signature#
The signature is the HMAC-SHA256 of the raw request body, hex encoded, with your signing secret as the key. The secret is webhook_signing_secret in GET /v1/account, and in the dashboard under Webhooks. Check the signature before you trust the request, and compute it over the raw body, before any JSON parsing.
import { createHmac, timingSafeEqual } from "node:crypto";
import { createServer } from "node:http";
const secret = process.env.SWITCHFILE_WEBHOOK_SECRET;
function isValid(rawBody, signature) {
const expected = createHmac("sha256", secret).update(rawBody).digest("hex");
return typeof signature === "string" && signature.length === expected.length && timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}
createServer((req, res) => {
const chunks = [];
req.on("data", (c) => chunks.push(c));
req.on("end", () => {
const raw = Buffer.concat(chunks);
const signature = req.headers["switchfile-signature"] ?? req.headers["cloudconvert-signature"];
if (!isValid(raw, signature)) {
res.writeHead(401).end();
return;
}
const event = JSON.parse(raw.toString("utf8"));
console.log(event.event, event.data?.id ?? event.job?.id);
res.writeHead(204).end();
});
}).listen(3000);<?php
$raw = file_get_contents('php://input');
$signature = $_SERVER['HTTP_SWITCHFILE_SIGNATURE'] ?? $_SERVER['HTTP_CLOUDCONVERT_SIGNATURE'] ?? '';
$expected = hash_hmac('sha256', $raw, getenv('SWITCHFILE_WEBHOOK_SECRET'));
if (!hash_equals($expected, $signature)) {
http_response_code(401);
exit;
}
$event = json_decode($raw, true);
http_response_code(204);import hashlib
import hmac
import os
def is_valid(raw_body: bytes, signature: str) -> bool:
secret = os.environ["SWITCHFILE_WEBHOOK_SECRET"].encode()
expected = hmac.new(secret, raw_body, hashlib.sha256).hexdigest().encode()
# Compare bytes: compare_digest raises TypeError for str with non-ASCII characters.
return hmac.compare_digest(expected, (signature or "").encode())
# Flask: is_valid(request.get_data(), request.headers.get("Switchfile-Signature", ""))To test your endpoint, sign a body yourself:
BODY='{"event":"conversion.finished","data":{}}'
SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$SWITCHFILE_WEBHOOK_SECRET" | cut -d' ' -f2)
curl https://example.com/hooks/switchfile -H "Content-Type: application/json" -H "Switchfile-Signature: $SIG" -d "$BODY"Answer and retries#
Answer with a 2xx status within 10 seconds. If your endpoint answers with 5xx or does not answer in time, switchfile tries again three more times with growing pauses, starting at 5 seconds. Answers with 4xx are not retried. Redirects are not followed.
Webhook URLs must be public http or https addresses on port 80 or 443. The same event can arrive twice, for example when your answer got lost, so handle it in a way that does no harm when repeated, for example by keeping the IDs you have seen.