Skip to main content

Video Compliance Check — Full Guide

ZebraTruth runs compliance analysis on video ads through a server-side async pipeline. Your client uploads the video to managed blob storage, submits a job, then either polls or receives a webhook when the report is ready.

Primary endpoint

POST https://api.zebratruth.ai/v1/compliance/check-video
No client-side ffmpeg. ZebraTruth handles video probing, audio transcription, frame extraction, OCR, visual signals, and Stage A→C compliance analysis server-side.

Supported inputs

LimitValue
Content typesvideo/mp4, video/webm, video/quicktime
Duration3 to 180 seconds
File sizeUp to 200 MB
ExecutionAsync only
Client preprocessingNone required

The 3-step flow

  1. POST /v1/compliance/media/upload-url — get a 15-min signed Azure Blob upload URL
  2. PUT <uploadUrl> — upload your video bytes directly to managed storage
  3. POST /v1/compliance/check-video — submit the uploaded video for analysis (returns 202)
  4. Poll GET /v1/compliance/check-video/{requestId} OR receive an HMAC-signed webhook

Step 1 — Request an upload URL

curl -X POST https://api.zebratruth.ai/v1/compliance/media/upload-url \
  -H "Authorization: Bearer $ZEBRATRUTH_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"contentType": "video/mp4"}'
Response
{
  "uploadUrl": "https://studio2.blob.core.windows.net/videos/...?sv=...&sig=...",
  "blobPath": "videos/<tenantId>/<requestId>/source.mp4",
  "requestId": "319b5d9e-a67f-45ee-9789-d7384fd73a3e",
  "expiresAt": "2026-05-29T18:30:00Z",
  "maxSizeBytes": 209715200,
  "requiredTags": {
    "tenantId": "<your-tenant>",
    "requestId": "319b5d9e-...",
    "kind": "video"
  },
  "uploadInstructions": {
    "method": "PUT",
    "headers": {
      "x-ms-blob-type": "BlockBlob",
      "x-ms-tags": "tenantId=<your-tenant>&requestId=319b5d9e-...&kind=video",
      "Content-Type": "video/mp4"
    }
  }
}
The uploadUrl is valid for 15 minutes. Save requestId + blobPath — you’ll pass blobPath back in step 3 + use requestId to poll.

Step 2 — Upload the video

PUT your video bytes directly to uploadUrl. Use exactly the headers from uploadInstructions.headersx-ms-tags is required by Azure for the integrity check.
curl -X PUT "$UPLOAD_URL" \
  -H "x-ms-blob-type: BlockBlob" \
  -H "x-ms-tags: tenantId=...&requestId=...&kind=video" \
  -H "Content-Type: video/mp4" \
  --data-binary @your-ad.mp4
Azure returns 201 Created on success. Bytes go directly to ZebraTruth-managed blob storage — they don’t pass through ZebraTruth servers.

Step 3 — Submit for analysis

curl -X POST https://api.zebratruth.ai/v1/compliance/check-video \
  -H "Authorization: Bearer $ZEBRATRUTH_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "requestId": "319b5d9e-a67f-45ee-9789-d7384fd73a3e",
    "blobPath": "videos/<tenantId>/319b5d9e-.../source.mp4",
    "jurisdictions": ["us", "eu"],
    "platforms": ["tiktok", "youtube"],
    "mode": "fast",
    "callbackId": "your-internal-id-optional",
    "webhookUrl": "https://yourapp.com/zebratruth-callback"
  }'
Response (HTTP 202)
{
  "jobId": "319b5d9e-a67f-45ee-9789-d7384fd73a3e",
  "status": "queued",
  "estimatedCredits": 300
}
The endpoint validates the upload (existence + content-length under 200 MB + duration in [3, 180] sec), reserves credits, and enqueues the job.

Request body fields

FieldRequiredNotes
requestIdyesUUID. Same as the one returned from upload-url. Also serves as idempotency key.
blobPathyesFrom upload-url response.
jurisdictionsyesAt least 1. Values: us, eu, uk, in, cn, etc.
platformsyesAt least 1. Values: tiktok, youtube, instagram, facebook, linkedin.
modeyes"fast" only (V1).
callbackIdnoOpaque string echoed back in webhook payloads.
webhookUrlnoIf set + tenant has a signing secret, ZebraTruth POSTs the report when terminal.

Possible error responses

StatusBody codeMeaning
400blob_path_invalidPath doesn’t match videos/<tenantId>/<requestId>/...
400unsupported_modeMode other than fast
400video_too_shortProbed duration < 3 sec
400video_too_longProbed duration > 180 sec
400video_too_largeSize > 200 MB
402insufficient_creditsTenant budget exhausted
409idempotency_conflictSame requestId already used with different inputs (the response body includes `reason: “blobPath""feature""inputHash”`)
503video_analysis_disabledFeature flag off (should not happen in prod)

Step 4 — Poll for results

curl https://api.zebratruth.ai/v1/compliance/check-video/319b5d9e-a67f-45ee-9789-d7384fd73a3e \
  -H "Authorization: Bearer $ZEBRATRUTH_API_KEY"
Response (in-flight)
{
  "requestId": "319b5d9e-...",
  "status": "processing",
  "phase": "transcribeAudio",
  "billingStatus": "reserved"
}
Response (terminal)
{
  "requestId": "319b5d9e-...",
  "status": "completed",
  "reportStatus": "complete",
  "billingStatus": "committed",
  "workerCompletedAt": "2026-05-29T18:20:00Z",
  "completedAt": "2026-05-29T18:20:01Z",
  "durationMs": 28000,
  "reportBlobPath": "videos/<tenantId>/319b5d9e-.../report.json",
  "report": {
    "schemaVersion": "video-report.v1",
    "preprocessorVersion": "video-preprocessor.v1",
    "evidenceVersion": "evidence-timeline.v1",
    "tenantId": "<your-tenant>",
    "requestId": "319b5d9e-...",
    "generatedAt": "2026-05-29T18:20:00Z",
    "status": "complete",
    "checks": [
      {
        "agentId": "advertising-law",
        "checkName": "Unsubstantiated efficacy claim",
        "status": "flag",
        "severity": "medium",
        "evidenceIds": ["audio_003"],
        "timelineLocation": { "startMs": 4500, "endMs": 7200, "track": "audio" },
        "message": "Claim 'guaranteed results in 30 days' lacks substantiation — FTC §5 risk.",
        "recommendation": "Add 'individual results may vary' overlay or remove claim.",
        "citation": "FTC Act §5",
        "primaryCategory": "unsubstantiated-health-claim"
      },
      {
        "agentId": "rights-clearance-image",
        "checkName": "Brand detected: Nike",
        "status": "flag",
        "severity": "medium",
        "evidenceIds": ["frame_007", "frame_009"],
        "timelineLocation": { "startMs": 12000, "endMs": 18000, "track": "video" }
      }
    ],
    "analysisDiagnostics": [],
    "preprocessing": {
      "transcriptionStatus": "complete",
      "frameAnalysisStatus": "complete",
      "degraded": false,
      "evidenceTruncated": false
    }
  }
}

Status state machine

queued → processing → completed (reportStatus: complete | insufficient_evidence)

                    failed (reportStatus: analysis_failed)
Poll every 2 seconds. Typical completion: 20-60 seconds for a 30-second video. Hard ceiling: 3 minutes.

Terminal report statuses

reportStatusMeaningBilling
completeReal findings producedCharged
insufficient_evidenceSilent video / no OCR / no signals — pipeline ran but had nothing to flagReleased (no charge)
analysis_failedEngine error — failure recorded for auditReleased (no charge)
Instead of polling, configure a webhook URL once at the account level and submit videos with webhookUrl set.

Get / create your tenant webhook secret

curl https://api.zebratruth.ai/v1/compliance/webhooks/video/secret \
  -H "Authorization: Bearer $ZEBRATRUTH_API_KEY"
Response
{
  "secret": "whsec_a1b2c3d4...",
  "createdAt": "2026-05-29T18:00:00Z"
}
Save the secret securely — ZebraTruth only returns it once on creation.

Verify webhook signatures

Each delivery is HMAC-SHA256-signed. Signature is stable across retries (computed once at enqueue + cached on the row). Headers:
  • x-zt-timestamp: <unix-seconds>
  • x-zt-signature: <hex-signature>
Canonical signing format:
HMAC-SHA256(secret, "POST" + "\n" + path + "\n" + timestamp + "\n" + sha256(rawBody))
Node.js example:
import crypto from "node:crypto";

function verifyZebraTruthWebhook(rawBody, headers, secret) {
  const timestamp = headers["x-zt-timestamp"];
  const signature = headers["x-zt-signature"];

  // Reject if timestamp older than 5 min (replay protection)
  if (Math.abs(Date.now() / 1000 - parseInt(timestamp)) > 300) {
    return false;
  }

  const bodyHash = crypto.createHash("sha256").update(rawBody).digest("hex");
  const canonical = `POST\n/your-webhook-path\n${timestamp}\n${bodyHash}`;
  const expected = crypto.createHmac("sha256", secret).update(canonical).digest("hex");

  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(signature)
  );
}

Retry semantics

AttemptDelay after previous
1(immediate)
2+1 minute
3+5 minutes
4+15 minutes
5+1 hour
After 5 failures, the delivery is marked failed and dropped. Webhook failures do NOT mutate the compliance job row — the job stays completed_committed. The webhook delivery has its own retry state in videoWebhookDeliveries.

Rotate the secret

curl -X POST https://api.zebratruth.ai/v1/compliance/webhooks/video/secret/rotate \
  -H "Authorization: Bearer $ZEBRATRUTH_API_KEY"
In-flight deliveries keep their original signature (sig is computed at enqueue). New jobs use the rotated secret immediately.

Idempotency

Same (requestId, blobPath, jurisdictions, platforms, mode) → 202 with idempotent: true, no extra charge. Same requestId with different blobPath / jurisdictions / platforms / mode → 409 idempotency_conflict:
{
  "error": true,
  "statusCode": 409,
  "data": {
    "code": "idempotency_conflict",
    "reason": "blobPath",  // or "feature" or "inputHash"
    "existing": { "blobPath": "...", "inputHash": "..." },
    "incoming": { "blobPath": "...", "inputHash": "..." }
  }
}
Precedence: blobPath > feature > inputHash (deterministic order for stable error messages).

Cost

Per-second pricing. A 60-second video costs ~600 credits.
DurationCredits
3 sec (minimum)30
10 sec100
30 sec300
60 sec600
180 sec (maximum)1800
Credits are reserved at submit + committed on reportStatus: complete. insufficient_evidence and analysis_failed release the reservation (no charge). See Cost & Credits for tier multipliers + billing lifecycle.

Common errors

HTTPcodeCauseFix
400blob_path_invalidPath doesn’t match the tenant’s prefixUse the blobPath from upload-url verbatim
400video_too_short< 3 secondsCut the source long enough
400video_too_long> 180 secondsSplit into segments
400video_too_large> 200 MBReduce bitrate or resolution before upload
402insufficient_creditsBudget exhaustedTop up or wait for next billing period
409idempotency_conflictSame requestId, different inputsUse a fresh UUID per submission
503video_analysis_disabled(Should not happen in prod)Contact support

Next

Cost & credits

Per-second pricing math, tier multipliers, credit lifecycle.

Async + Webhooks

HMAC verification deep dive, retry semantics.

Interpreting reports

How to read scores + decisions + agent breakdowns.

LLM Skill

Install the skill so your LLM agent runs video checks autonomously.