Webhooks
Get a signed POST when a job finishes.
On paid plans, add webhook_url to any job and we POST the result to your server when the job ends, whether it succeeded or failed. On Free, a request with webhook_url is refused with status 403.
{
"input": "https://example.com/clip.mov",
"webhook_url": "https://your-app.example.com/hooks/mediatofu"
}The URL must be public http or https, under the same rules as input URLs.
Delivery
The body is the same JSON as the job response, sent as a single compact line.
| Header | Value |
|---|---|
Content-Type | application/json |
X-MediaTofu-Signature | sha256=<hex> |
- Your endpoint must answer with a 2xx within 10 seconds. Redirects aren't followed and count as failures.
- A failed delivery is retried on a schedule, every 5 minutes or so, up to 5 attempts in total. After that it is dropped.
- Delivery is at least once, so the same job can arrive more than once. Use the job
idto ignore duplicates.
Verifying the signature
The signature is an HMAC-SHA256 of the raw request body. The HMAC key is the SHA-256 hex of the API key that started the job, so revoking or adding other keys doesn't change it.
Verify it against the raw bytes, before parsing the JSON. This is a complete Node server with no dependencies:
import { createHash, createHmac, timingSafeEqual } from "node:crypto";
import { createServer } from "node:http";
// The API key that started the job.
const apiKey = process.env.MEDIATOFU_API_KEY;
function verifySignature(rawBody, header) {
const secret = createHash("sha256").update(apiKey).digest("hex");
const expected = "sha256=" + createHmac("sha256", secret).update(rawBody).digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from(header ?? "");
return a.length === b.length && timingSafeEqual(a, b);
}
createServer(async (req, res) => {
const chunks = [];
for await (const chunk of req) chunks.push(chunk);
const rawBody = Buffer.concat(chunks);
if (!verifySignature(rawBody, req.headers["x-mediatofu-signature"])) {
res.writeHead(401).end();
return;
}
const job = JSON.parse(rawBody);
console.log(job.id, job.status);
res.writeHead(200).end();
}).listen(process.env.PORT ?? 3000);Any body parser that re-serializes the JSON will change the bytes and break the check, so keep the raw body. Answer with a 2xx before doing slow work, or the delivery can time out and repeat.