Skip to content

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 id to skip duplicates.
  • Rotate the secret in Settings if it leaks. The old one stops working at once.
Questions? support@tovano.ai Machine-readable: openapi.json · llms.txt