Webhooks
When a video, audio or capability job finishes, AI Hub posts a signed event to your URL, so you do not have to poll. Polling still works.
1. Set a URL and a signing secret
In Settings → Organisation & integrations → Webhooks, an admin sets the workspace's default URL and generates a signing secret. The secret starts with whsec_ and is shown once. The URL must be HTTPS and must resolve to public addresses only.
2. Or name a URL per request
Video and audio generation, and capability submits that declare the field accept callback_url, which receives that job's event instead of the default URL. The workspace needs a signing secret first; without one the request is refused with 400 webhook_secret_missing.
curl https://tokenhub-api.belive.sg/api/v1/videos \ -H "Authorization: Bearer $TOKENHUB_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "alibaba/wan3.0-video", "prompt": "a red apple slowly rotating on a white table", "callback_url": "https://example.com/tokenhub/webhooks" }'
3. The events
| Jobs | Event types |
|---|---|
| Video generation | video.generation.completed, .failed, .cancelled, .expired |
| Audio generation | audio.generation.completed, audio.generation.failed |
| 3D generation, where available | 3d.generation.completed, 3d.generation.failed, 3d.generation.cancelled, 3d.generation.expired |
| Capability jobs (media, speech, background removal, upscale, denoise) | job.completed, job.failed, job.expired |
The body is {type, created_at, data}, where data is the job exactly as its status route returns it, including usage.cost. A terminal event is sent once the job's charge is settled. A media or speech job whose charge never settles sends its event an hour after it ended; a video or audio job sends its completed event only after its charge is settled. A job refused when it was submitted sends no event: the refusal was its answer.
Async chat jobs do not accept callbacks. Poll their status route instead. Event names describe supported job families; they do not make an unavailable model callable.
Opt in to video queue admission events
Set queue_webhook: true on a video submit to receive video.generation.queuedwhen the job enters the queue. A configured request callback or workspace default hook with a signing secret is required. Omitted or false keeps terminal-only delivery; callback_url alone does not opt in.
This separate event contains the canonical job snapshot with status: pending and queued: true. It records admission, not execution, completion or a settled charge. The outbox creates the admission event once; delivery is at least once and may arrive after a later terminal event because of retries. Deduplicate using the X-TokenHub-Idempotency-Key ending in -queued, and read the job's GET route for current state. Existing terminal events and their payloads remain unchanged.
4. Verify the signature
Every delivery carries X-TokenHub-Signature: t=<unix seconds>,v1=<hex>. The v1 value is the HMAC-SHA256, keyed with your signing secret, of the timestamp, a comma, and the raw request body. For 24 hours after a rotation the header carries a second v1, made with the previous secret; accept the event when any v1 matches.
import { createHmac, timingSafeEqual } from "node:crypto"; // rawBody: the request body exactly as received, before any JSON parsing. export function verify(rawBody, header, secret) { const parts = header.split(","); const t = parts.find((p) => p.startsWith("t="))?.slice(2); if (!t || Math.abs(Date.now() / 1000 - Number(t)) > 300) return false; // refuse replays const expected = createHmac("sha256", secret).update(`${t},${rawBody}`).digest(); return parts .filter((p) => p.startsWith("v1=")) .some((p) => { const given = Buffer.from(p.slice(3), "hex"); return given.length === expected.length && timingSafeEqual(given, expected); }); }
5. Answer quickly, and expect retries
Any 2xx answer within 10 seconds acknowledges the event; the body is ignored and redirects are not followed. Anything else is retried 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours, 12 hours and 24 hours after the first attempt, then marked failed. Delivery is at least once: deduplicate on X-TokenHub-Idempotency-Key, which is <job_id>-<status> on every attempt. X-TokenHub-Event repeats the event type.
A failed delivery never changes the job or what it cost. Settings → Organisation & integrations → Webhooks lists the recent deliveries and their status, and Send test posts a signed webhook.test event.