Webhooks
Instead of polling, your server can be told when something happens: give a notify_url (a public https address of
yours) on POST /uploads, POST /media/imports or POST /jobs. API keys only (scope
webhooks). The address is checked when you give it and before every delivery; redirects are not followed.
| Event | When | data |
|---|---|---|
media.ready | the media of the upload or import has been analysed | media |
media.failed | the media could not be analysed | media |
job.needs_action | a job waits for its keyframes (each time it does) | job |
job.completed | the result is ready | job |
job.failed | the job failed | job |
job.cancelled | the job was cancelled | job |
Each event is a POST of JSON to your address - the media or job exactly as GET shows it - with these headers:
A delivery
POST /recuro-webhook HTTP/1.1
Content-Type: application/json
User-Agent: RecuroMedia-Webhook/1.0
Recuro-Event: job.completed
Recuro-Delivery: evt_job_3150_5d41402a
Recuro-Signature: t=1791277460,v1=6b0f4c0e0d1c7a2a9f5e8b3d1c4e7f9a0b2c5d8e1f3a6b9c2d5e8f1a4b7c0d3e
{"id":"evt_job_3150_5d41402a","type":"job.completed","created_at":"2026-10-06T09:04:20Z","data":{"job":{"id":"job_3150","status":"completed","...":"..."}}}- Answer with any 2xx status within 30 seconds (do the work afterwards). Anything else is tried again after 1 minute,
5 minutes, 30 minutes, 2 hours and 12 hours. A delivery can arrive more than once:
Recuro-Deliveryis the same for every retry of an event - ignore one you have handled already. - Check the signature before you trust a delivery:
v1is the hexadecimal HMAC-SHA256 of<t>.<raw body>with your signing key. Refuse at(a Unix time in seconds) older than 5 minutes - it could be a recording sent again. Use the raw body exactly as it arrived - not JSON you have decoded and encoded again. - Create your signing key first, before you set a
notify_url: until you have pressed CREATE SIGNING KEY, deliveries are signed with a key nobody has been shown, and your receiver cannot check them. - Your signing key (
whsec_...) is made in My Account > Administrator Tools, next to the API key, with CREATE SIGNING KEY; like the API secret it is shown only once - store it at once. To change it, press CREATE NEW SIGNING KEY: for 24 hours every delivery carries two signatures, the new key's and the old one's (t=...,v1=<new>,v1=<old>), so your receiver keeps working while you install the new key. Accept a delivery when anyv1matches.
PHP
<?php
// The receiver of your notify_url. RECURO_WEBHOOK_KEY: your whsec_... signing key (keep it out of your web root and your repository)
$signing_key = getenv('RECURO_WEBHOOK_KEY');
function RecuroSignatureValid($header, $body, $key, $tolerance = 300) {
$time = null;
$signatures = array();
foreach (explode(',', $header) as $part) {
$pair = explode('=', trim($part), 2);
if (count($pair) != 2)
continue;
if ($pair[0] === 't')
$time = $pair[1];
else if ($pair[0] === 'v1')
$signatures[] = $pair[1]; // two during a key change: the new key's and the old one's
}
if (($time === null) || !ctype_digit($time) || (abs(time() - (int) $time) > $tolerance))
return false;
$expected = hash_hmac('sha256', $time . '.' . $body, $key);
foreach ($signatures as $signature)
if (hash_equals($expected, $signature))
return true;
return false;
}
$body = file_get_contents('php://input'); // the raw body, exactly as it arrived
if (!RecuroSignatureValid($_SERVER['HTTP_RECURO_SIGNATURE'] ?? '', $body, $signing_key)) {
http_response_code(400);
exit;
}
$event = json_decode($body, true);
// Skip a Recuro-Delivery ($_SERVER['HTTP_RECURO_DELIVERY']) you have handled before, then queue the work, e.g. on job.completed
// download $event['data']['job']['result']['url'] with your API key
http_response_code(204);Python
import hashlib, hmac, time
def recuro_signature_valid(header, body, key, tolerance=300):
"""header: the Recuro-Signature value; body: the raw request body (bytes); key: your whsec_... signing key."""
pairs = [part.strip().split("=", 1) for part in header.split(",") if "=" in part]
times = [value for name, value in pairs if name == "t"]
signatures = [value for name, value in pairs if name == "v1"] # two during a key change
if not times or not times[0].isdigit() or abs(time.time() - int(times[0])) > tolerance:
return False
expected = hmac.new(key.encode(), times[0].encode() + b"." + body, hashlib.sha256).hexdigest()
return any(hmac.compare_digest(expected, signature) for signature in signatures)JavaScript (Node.js)
const crypto = require('crypto');
// header: the Recuro-Signature value; body: the raw request body (a Buffer); key: your whsec_... signing key
function recuroSignatureValid(header, body, key, tolerance = 300) {
const pairs = header.split(',').map((part) => part.trim().split('='));
const time = (pairs.find(([name]) => name === 't') || [])[1] || '';
if (!/^\d+$/.test(time) || Math.abs(Date.now() / 1000 - Number(time)) > tolerance) return false;
const expected = Buffer.from(crypto.createHmac('sha256', key).update(time + '.').update(body).digest('hex'));
return pairs.filter(([name]) => name === 'v1') // two during a key change
.some(([, signature]) => signature.length === expected.length && crypto.timingSafeEqual(Buffer.from(signature), expected));
}
module.exports = { recuroSignatureValid };Test your receiver with a delivery of your own, signed with your key:
Test delivery
KEY=whsec_YOUR_SIGNING_KEY
BODY='{"id":"evt_test","type":"job.completed","created_at":"2026-10-06T09:04:20Z","data":{"job":{"id":"job_3150","status":"completed"}}}'
T=$(date +%s)
SIG=$(printf '%s.%s' "$T" "$BODY" | openssl dgst -sha256 -hmac "$KEY" -r | cut -d' ' -f1)
curl -sS -i https://your-server.example.com/recuro-webhook -H "Content-Type: application/json" \
-H "Recuro-Event: job.completed" -H "Recuro-Delivery: evt_test" -H "Recuro-Signature: t=$T,v1=$SIG" --data-binary "$BODY"