Documentation

Webhooks

Instead of polling, give ViewGC an address and we call it when a video, a campaign or a film is ready. Every request is signed, and a receiver that is down misses nothing.

Add an endpoint

In the studio, Settings, Webhooks: an https address and the events you want. You get a signing secret (whsec_...) once; keep it on your server. Send test delivers a ping at once and shows what your endpoint answered. Owners and admins can manage endpoints there; over the API, /v1/organizations/:id/webhooks takes an API key with the webhooks:write scope (webhooks:read to list them). Addresses must be public: private, loopback and link-local addresses are refused, including names that resolve to them.

Change an endpoint’s address or events with PATCH .../webhooks/:webhookId. If the secret may have leaked, New secret (or POST .../webhooks/:webhookId/rotate-secret) replaces it and shows the new one once; deliveries are signed with the new secret from then on, so update your server straight away.

Events

EventWhendata
creative.completedA video finished.creativeId, campaignId, status, videoUrl, posterUrl, durationSeconds
creative.failedA video could not be made. Its credits are returned.creativeId, campaignId, status, failureReason
campaign.completedEvery video in a campaign finished.campaignId, name, status, creatives (count by status)
campaign.failedA campaign stopped.campaignId, name, status, creatives (count by status)
episode.storyboard_readyA production’s storyboard is drawn and waits for approval.episodeId, title, studioUrl
episode.completedA film is filmed, cut and scored.episodeId, title, studioUrl, filmUrl
episode.failedFilming stopped before the end.episodeId, title, studioUrl, failureReason
billing.api_credits_lowAn API request took your API credits below 500 (once each time they cross it), so you can buy more before calls are refused.apiCreditBalance, threshold
pingYou pressed Send test in Settings.message

Media links in a payload work for 7 days. Fetch the creative or episode from the API with its id for a fresh one.

What arrives

A delivery
POST /your/endpoint
Content-Type: application/json
User-Agent: ViewGC-Webhooks/1
ViewGC-Event: creative.completed
ViewGC-Delivery: cmw8x2k1f0003...
ViewGC-Signature: t=1761955200,v1=5f2c8e...

{
  "id": "evt_3b1f0c2a9e5d4f7a8b6c1d2e3f4a5b6c",
  "type": "creative.completed",
  "createdAt": "2026-10-31T22:40:00.000Z",
  "organizationId": "cmt28bem400001bnql3qw08ut",
  "data": {
    "creativeId": "cmvwwpdro000h93niov2v0z72",
    "campaignId": "cmvwtrtz4000211jtabmtvdlo",
    "status": "COMPLETED",
    "videoUrl": "https://...",
    "posterUrl": "https://...",
    "durationSeconds": 14.2
  }
}

id is the event, the same on every retry: use it to ignore a duplicate. ViewGC-Delivery identifies this attempt.

Check the signature

ViewGC-Signature is t=<unix seconds>,v1=<hex>, where v1 is the HMAC-SHA256 of <t>.<raw body> under your endpoint secret. Compute it over the body exactly as received (not re-serialised JSON), compare in constant time, and refuse a t more than five minutes old.

Node
import { createHmac, timingSafeEqual } from 'node:crypto'

// rawBody: the request body exactly as received, before JSON parsing.
export function verify(rawBody, header, secret, toleranceSeconds = 300) {
  const parts = Object.fromEntries(header.split(',').map(p => p.split('=')))
  const t = Number(parts.t)
  if (!t || !parts.v1) return false
  if (Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false // a replay
  const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest()
  const given = Buffer.from(parts.v1, 'hex')
  return given.length === expected.length && timingSafeEqual(given, expected)
}
Python
import hashlib, hmac, time

def verify(raw_body: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    t, v1 = int(parts.get("t", 0)), parts.get("v1", "")
    if not t or abs(time.time() - t) > tolerance:
        return False
    expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, v1)

Answering, retries and pausing

  • Answer with any 2xx within 10 seconds. Do slow work after answering.
  • Anything else, or no answer, is retried after 30 seconds, 2 minutes, 10 minutes, 30 minutes, 1, 3, 6 and 12 hours: eight retries over about 22 hours. Then the delivery is marked failed.
  • An endpoint whose deliveries fail 20 times in a row is paused. Settings, Webhooks shows it; turn it back on once it answers again.
  • Every attempt, with the status your endpoint returned, is listed under the endpoint in Settings, and a failed delivery can be sent again from there.
  • Deliveries are not guaranteed to arrive in order. Use createdAt, and fetch the resource when order matters.

The full API, including the webhook routes, is in the interactive reference. Something unclear? Ask us.