Langsung ke konten utama

Dokumentasi

Webhook

Panduan terperinci dan referensi API hanya tersedia dalam bahasa Inggris.

Events

Register an HTTPS endpoint on the developer page (up to 5 endpoints per account). CheapVids sends a POST with a JSON body when a job:

EventSent when
generation.startedthe generator starts the job
generation.completedthe job finished and the result can be downloaded
generation.failedthe job failed and was refunded

The body contains the same job object as GET /v1/generations/{id}, with the content URL valid for one hour:

{
  "id": "0190a1b2-0000-7000-8000-000000000001",
  "type": "generation.completed",
  "created_at": "2026-10-03T08:00:00Z",
  "data": {
    "generation": {
      "id": "0190a1b2-0000-7000-8000-0000000000aa",
      "status": "completed",
      "output": { "url": "https://api.cheapvids.com/v1/generations/0190a1b2-0000-7000-8000-0000000000aa/content?exp=1760003600&sig=..." }
    }
  }
}

id identifies the delivery. Use it to ignore duplicates: the same event can arrive more than once.

Requirements for your endpoint

  • HTTPS only, port 443 or 8443, a public address. Endpoints on private networks, localhost or CheapVids domains are rejected.
  • Answer with any 2xx within 10 seconds. Anything else, or no answer, counts as a failed send.
  • Do the slow work after answering.
  • Webhooks come from Cloudflare addresses, not from a fixed IP range of CheapVids: authenticate with the signature, not with the IP address.

Headers

HeaderValue
Content-Typeapplication/json
User-AgentCheapVids-Webhooks/1.0
X-CheapVids-Eventthe event name
X-CheapVids-Deliverythe delivery id
X-CheapVids-Signaturet=<unix seconds>,v1=<hex>

Verify the signature

The signature is hex(HMAC_SHA256(secret, "<t>." + raw body)), where secret is the whsec_... string shown once when you create the endpoint (use the whole string, including whsec_) and t is the timestamp in the header. Compute it over the raw bytes you received, before parsing the JSON, compare in constant time and reject timestamps older than 5 minutes.

Python:

import hashlib
import hmac
import time


def verify_webhook(raw_body: bytes, signature_header: str, secret: str, tolerance: int = 300) -> bool:
    # The header comes from the sender: any odd value must give False, never an exception.
    try:
        timestamp, signatures = None, []
        for part in signature_header.split(","):
            key, _, value = part.strip().partition("=")
            if key == "t" and value.isascii() and value.isdigit():
                timestamp = int(value)
            elif key == "v1":
                signatures.append(value)
        if timestamp is None or not signatures or abs(time.time() - timestamp) > tolerance:
            return False
        message = str(timestamp).encode() + b"." + raw_body
        expected = hmac.new(secret.encode(), message, hashlib.sha256).hexdigest().encode()
        # Compare as bytes: compare_digest raises TypeError on a non-ASCII str.
        return any(hmac.compare_digest(expected, signature.encode()) for signature in signatures)
    except (ValueError, TypeError, OverflowError):
        return False

Node.js:

import { createHmac, timingSafeEqual } from "node:crypto";

export function verifyWebhook(rawBody, signatureHeader, secret, toleranceSec = 300) {
  let timestamp;
  const signatures = [];
  for (const part of signatureHeader.split(",")) {
    const [key, value = ""] = part.trim().split("=");
    if (key === "t" && /^\d+$/.test(value)) timestamp = Number(value);
    else if (key === "v1") signatures.push(value);
  }
  if (timestamp === undefined || signatures.length === 0) return false;
  if (Math.abs(Date.now() / 1000 - timestamp) > toleranceSec) return false;
  const expected = Buffer.from(createHmac("sha256", secret).update(`${timestamp}.`).update(rawBody).digest("hex"));
  // Compare the bytes: timingSafeEqual throws when the lengths differ, which a non-ASCII value can cause.
  return signatures.some((s) => {
    const got = Buffer.from(s);
    return got.length === expected.length && timingSafeEqual(got, expected);
  });
}

Run this check on the raw request body before you parse it, and reject the request when it fails.

Delivery schedule

Each event is sent at most 4 times: the first send and 3 retries after 10, 60 and 300 seconds. After the fourth failed send the delivery is marked as failed. The developer page shows the last 20 sends of each endpoint with the HTTP status and a short reason.

A failed webhook never loses the result: you can always read the job with GET /v1/generations/{id} and download it within the download window.

Rotating the secret

You can issue a new secret for an endpoint at any time; it is shown once. Events sent after the change are signed with the new secret.