Webhooks
Pass webhook_url when you create a task. When the task finishes, we POST the full task object to that URL.
The request we send
POST /hooks/tovano HTTP/1.1
Content-Type: application/json
Tovano-Signature: t=1763026241,v1=5f1c0b9e...
{ "id": "tsk_9f2c41ab7d0e", "status": "completed", "charged_micro": 80000, ... }Answer with any 2xx within 10 seconds. If you don't, we retry three times, after 10 seconds, 1 minute and 5 minutes.
Check the signature
Each project has a webhook secret, under Settings. The signature is an HMAC-SHA256 of t.body (the timestamp, a dot, then the raw body), using that secret.
import { createHmac, timingSafeEqual } from "node:crypto";
export function verify(rawBody: string, header: string, secret: string) {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const expected = createHmac("sha256", secret).update(`${parts.t}.${rawBody}`).digest("hex");
const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;
return fresh && timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}Or use the SDK: tovano.webhooks.verify(rawBody, header, secret).
Good practice
- Use the raw body for the check, before parsing JSON.
- Reject timestamps older than five minutes.
- The same task can arrive twice. Use the task
idto skip duplicates. - Rotate the secret in Settings if it leaks. The old one stops working at once.