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.

Events
EventWhendata
media.readythe media of the upload or import has been analysedmedia
media.failedthe media could not be analysedmedia
job.needs_actiona job waits for its keyframes (each time it does)job
job.completedthe result is readyjob
job.failedthe job failedjob
job.cancelledthe job was cancelledjob

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-Delivery is the same for every retry of an event - ignore one you have handled already.
  • Check the signature before you trust a delivery: v1 is the hexadecimal HMAC-SHA256 of <t>.<raw body> with your signing key. Refuse a t (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 any v1 matches.
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"