Platform

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.

bash
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

JobsEvent types
Video generationvideo.generation.completed, .failed, .cancelled, .expired
Audio generationaudio.generation.completed, audio.generation.failed
3D generation, where available3d.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.

javascript
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.