Webhooks & signature verification

Receive signed event callbacks and verify their authenticity.

Zentag delivers signed HTTP POST callbacks when domain events occur. Register
endpoints in Settings → Partner integrations → Webhooks or via the
webhooks:manage scope.

Event types

EventWhen it fires
clip.createdClip row created (processing may still be running)
clip.readyClip finished processing and is ready for delivery
clip.failedClip processing failed
highlight.readyHighlight finished successfully
highlight.failedHighlight processing failed
publish.succeededPublish job completed
publish.failedPublish job failed
stream.startedStream session started
stream.endedStream session ended

In the API Reference, see Webhooks → Outbound event payload for the shared
JSON schema (discriminate on type).

Payload

{
  "id": "evt_1a2b3c",
  "type": "clip.ready",
  "created_at": "2026-07-01T18:24:00.000Z",
  "data": { "object": "clip", "id": "clip_123", "status": "COMPLETED" }
}

Headers

HeaderDescription
Zentag-Signaturet=<unix>,v1=<hex HMAC-SHA256>
Zentag-EventEvent type
Zentag-Event-IdUnique event id (dedupe)
Zentag-DeliveryDelivery attempt id

Verifying the signature

The signature is HMAC-SHA256(signing_secret, "{timestamp}.{raw_body}"). Compare
using a constant-time function and reject timestamps outside a ~5 minute window.

Node.js

import crypto from "node:crypto";

export function verify(rawBody, header, secret) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${parts.t}.${rawBody}`)
    .digest("hex");
  const a = Buffer.from(expected, "hex");
  const b = Buffer.from(parts.v1, "hex");
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

Python

import hashlib, hmac

def verify(raw_body: bytes, header: str, secret: str) -> bool:
    parts = dict(p.split("=") for p in header.split(","))
    expected = hmac.new(secret.encode(), f"{parts['t']}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, parts["v1"])

Retries

Failed deliveries (non-2xx or timeout) are retried with exponential backoff
(~1m, 2m, 4m, 8m, 16m, 32m). Always return 2xx quickly and process
asynchronously. Use Zentag-Event-Id to make handling idempotent. You can
replay any delivery from the dashboard.


Did this page help you?