API
Webhooks
Name a URL when you start a job, or set one for the project, and the job is posted to it when it ends. Each call is signed with your project's secret.
Ask for a call
Set webhook on the job to a public http or https URL, up to 2,048 bytes. When the job is done, failed or canceled, an event is posted to it.
Or set a URL for the whole project on the Webhooks page of the dashboard. A job that names no webhook posts its event there. The page can also send a webhook.test event, signed like the others, to check the URL and your signature code.
$ export CV_API_KEY=cv_sk_…Set your key once. Every cv command reads it.
$ curl -X POST https://api.convert.dev/v1/jobs \ -H "Authorization: Bearer $CV_API_KEY" \ -H "Content-Type: application/json" \ -d '{"inputs": ["file_01JC9W3TQ6V8ZK2N5R7M4H1XEB"], "to": "pdf", "webhook": "https://example.com/hooks/convert"}'What arrives
A POST with a JSON body on one line, its keys in alphabetical order, and the user agent convert.dev-webhooks/1. type is job.done, job.failed or job.canceled, and data.object is the job as it is when the call is made, with fresh URLs for its first 100 outputs.
The event's id is the job's id with evt_ in place of job_, so each job has one event, and the webhook-id header carries it too. It can arrive more than once, so use the id to drop repeats.
{ "created_at": "2026-11-03T09:12:36Z", "data": { "object": { "id": "job_01JC9W5D3K8N2Q6T9V1M4R7XAE", "object": "job", "outputs": [ … ], "status": "done", … } }, "id": "evt_01JC9W5D3K8N2Q6T9V1M4R7XAE", "object": "event", "type": "job.done"}Check the signature
Calls are signed as the Standard Webhooks spec says, so a library for it can check them with the whole secret. Each call has three headers: webhook-id, the event's id; webhook-timestamp, when it was signed, in Unix seconds; and webhook-signature, such as v1,g0hM9SsE+OTPJTGt….
The signature is the HMAC-SHA256 of the id, a dot, the timestamp, a dot and the raw body, in base64. The key is the signing secret on the Webhooks page of the dashboard with whsec_ taken off, decoded from base64. It is the same for every job of the project.
webhook-signature can hold more than one signature, separated by spaces. Accept the call when one of them matches. Compare in constant time, and refuse a timestamp more than a few minutes old so an old call cannot be replayed.
Check the signature on the body exactly as it arrived, before you parse it.
If the secret leaks, roll it on the Webhooks page. Every call from then on, retries too, is signed with the new one. For 24 hours each call also carries a signature made with the old one, so your receiver keeps accepting calls while you change the secret.
$ key=$(printf %s "${CV_WEBHOOK_SECRET#whsec_}" | openssl base64 -d -A | od -An -tx1 | tr -d " \n")printf %s "$id.$timestamp.$body" | openssl dgst -sha256 -mac HMAC -macopt "hexkey:$key" -binary | openssl base64 -ARetries
A 2xx answer within 10 seconds counts as delivered. Anything else is tried again after 1 minute, then 2, 4 and so on, doubling up to about 17 hours, for 12 tries over about a day and a half.
Answer quickly and do the work after. The Webhooks page of the dashboard lists recent deliveries with their last error and their next try.
Updated 3 November 2026