Skip to main content

Brand safety

Ad blocking is programmatic brand-safety screening for a single ad creative. You send the creative and an optional list of natural-language policy rules; the service returns a deterministic block / allow verdict, the GARM categories that fired and the rules that matched. One synchronous call, no polling, no callbacks.

How screening works​

A screening request does the following inside one synchronous call:

  1. Stores the creative and, if needed, transcodes it for the analyzer.
  2. Extracts audio and a keyframe.
  3. Runs a single multimodal pass that evaluates both the 11 GARM brand-safety categories and every policy rule: your team's saved blocklist plus any ad-hoc rules in the request.
  4. Computes the verdict in code from the evidence: block_decision is block if and only if at least one GARM category fired or at least one policy rule matched. Otherwise it is allow.
  5. Persists the result and returns it.

Properties to design around:

  • Synchronous. Typical latency is a few seconds, dominated by the multimodal pass.
  • Deterministic verdict. The reasoning is the model's; the verdict is computed in code from garm and blocklist.hits, so it always agrees with the evidence it ships with. The evidence is AI output and can be wrong in either direction, so treat block as "hold for review" and keep a human in the loop for anything that matters. See Data handling.
  • Fail-closed. If the analysis itself errors, you still get 200 with block_decision: "block" and screening_failed: true, so an integration that branches on block_decision alone holds the creative instead of serving it.
  • GARM is always evaluated, even with no rules. Rules are additional policy on top.
  • Length cap. Creatives are capped at about 120 seconds.

Screen an uploaded file​

POST /api/v1/ad-blocking takes multipart/form-data.

filefilebodyrequired

Ad video file (mp4 / mov / ...), up to ~120 seconds.

titlestringbody

Display title for audit and listing.

rulesstringbody

Ad-hoc policy rules in natural language, comma- or newline-separated. Evaluated in addition to your team's saved blocklist (managed in the dashboard's Ad Blocking tab).

curl -X POST https://video.pyler.ai/api/v1/ad-blocking \
-H "Authorization: Bearer $VUF_API_KEY" \
-F "file=@./ads/crunchy-sticks-q4.mp4" \
-F "title=Crunchy Sticks Q4 brand campaign" \
-F "rules=no on-screen consumption of alcohol, no minors holding weapons"

Response — 200 OK:

{
"id": "9f3c2a4d-0000-0000-0000-000000000000",
"ad_id": null,
"title": "Crunchy Sticks Q4 brand campaign",
"duration_ms": 18000,
"one_line": "Chocolate-stick snack; friends share boxes at a party.",
"garm": [
{
"category": "illegal_drugs_alcohol_tobacco_vaping",
"reasoning": "Bar scene at 0:12 shows a beer pour and a toast."
}
],
"blocklist": {
"rules": [
"no on-screen consumption of alcohol",
"no minors holding weapons"
],
"hits": [
{
"rule": "no on-screen consumption of alcohol",
"matched": true,
"evidence": "Characters drink beer at 0:14."
}
]
},
"block_decision": "block",
"explanation": "BLOCKED — GARM illegal_drugs_alcohol_tobacco_vaping; blocklist rule 'no on-screen consumption of alcohol'. Bar scene shows pour and toast.",
"screening_failed": false,
"created_at": "2026-06-16T10:23:01Z"
}

Screenings run on an uploaded file carry ad_id: null; only the v1.1 route below fills it.

Screen a registered creative (v1.1)​

If the creative already lives in the ad registry, skip the re-upload. POST /api/v1.1/ad-blocking takes a JSON body with the ad_id (which must be status: ready) and optional rules. The screening pipeline, including codec normalization and audio-track analysis, is the same as the file path, so the same creative gets the same verdict either way. The verdict shape is identical, with ad_id filled in.

ad_idstringbodyrequired

Registered ad creative UUID with status: ready.

rulesstring | nullbody

Optional ad-hoc policy rules, comma- or newline-separated.

curl -X POST https://video.pyler.ai/api/v1.1/ad-blocking \
-H "Authorization: Bearer $VUF_API_KEY" \
-H "Content-Type: application/json" \
-d "{\"ad_id\": \"$AD_ID\", \"rules\": \"no on-screen consumption of alcohol\"}"
HTTPWhen
200Verdict returned.
404Unknown ad_id, owned by another team, or not a valid UUID (malformed ids are normalized to 404 so existence is not leaked).
409The creative is not status: ready.
note

/api/v1.1 is a separate version root alongside /api/v1 for the routes that take a registered ad_id as input. Both roots accept the same API key. The v1 multipart route is unchanged.

Look up past screenings​

Every screening result is stored, so a response lost to a client timeout is not lost work. GET /api/v1.1/ad-blocking lists your team's results, newest first; filter by ad_id to retrieve the one you missed instead of re-submitting.

QueryTypeNotes
ad_idUUIDOptional. Unknown id returns an empty list; a value that is not a UUID returns 422.
limitinteger1–200, default 50.
offsetintegerDefault 0.
curl "https://video.pyler.ai/api/v1.1/ad-blocking?ad_id=$AD_ID" \
-H "Authorization: Bearer $VUF_API_KEY"
{
"items": [
{
"id": "1a2b3c4d-0000-0000-0000-000000000000",
"ad_id": "c54eff39-0000-0000-0000-000000000000",
"block_decision": "allow",
"screening_failed": false,
"created_at": "2026-06-16T10:25:14Z"
}
],
"total": 1
}

Items carry the full verdict shape below. Screenings run on an uploaded file (the v1 multipart route) have ad_id: null and cannot be looked up this way; pass ad_id only for creatives registered through the ad registry. Past screenings are also listed in the dashboard's Ad Blocking tab.

The verdict shape​

Both screening routes and the lookup return the same AdBlockingResult.

idstringrequired

Server-issued id of this screening.

ad_idstring | null

The registered creative that was screened. null for file uploads. Use it with GET /api/v1.1/ad-blocking?ad_id=... to find the result again.

titlestring | null

Display title: the request title, else the filename.

duration_msinteger | null

Creative length in milliseconds.

one_linestring | null

One-line summary of what the ad shows.

garmobject[]

GARM categories that fired; an empty array when clean. Each item is { category, reasoning }. category is a stable slug from the table below and safe to switch on; reasoning cites the deciding cue, including the time it occurs (for example "Bar scene at 0:12").

blocklistobject

{ rules, hits }. rules is the full set evaluated (team blocklist + ad-hoc). hits is [{ rule, matched, evidence }]; items with matched: true are the rules that fired.

block_decisionstringdefault: allow

The verdict: block if and only if garm is non-empty or any blocklist.hits[i].matched is true, else allow.

explanationstring | null

Human-readable reasoning prefixed with the triggering categories and rules. For display, not parsing: switch on garm and blocklist.hits instead.

screening_failedbooleandefault: false

true when screening itself failed and the block is precautionary rather than a content verdict (fail-closed). Retry the call; never surface it to end users as a real block.

created_atstringrequired

ISO 8601 UTC timestamp.

warning

Branch on screening_failed, not on the explanation text. A real block and a failed screening both open with BLOCKED —.

GARM categories​

The screener evaluates the 11 GARM Brand Safety Floor categories. Slugs are stable; the titles are the GARM names.

SlugGARM title
adult_and_explicit_sexual_contentAdult & Explicit Sexual Content
arms_and_ammunitionArms & Ammunition
crime_human_rights_violationsCrime & Harmful Acts, Human Rights Violations
death_injury_or_military_conflictDeath, Injury, or Military Conflict
online_piracyOnline Piracy
hate_speech_and_acts_of_aggressionHate Speech & Acts of Aggression
obscenity_and_profanityObscenity & Profanity
illegal_drugs_alcohol_tobacco_vapingIllegal Drugs / Tobacco / E-cigarettes / Vaping / Alcohol
spam_or_harmful_contentSpam or Harmful Content
terrorismTerrorism
debated_sensitive_social_issueDebated Sensitive Social Issue
info

Ad-blocking garm entries carry a category and reasoning only; there is no severity level or scene range on the screening verdict. Severity (level: high / medium / low) and time_ranges appear on the video-side garm_video entries returned by indexing, search and GET /api/v1/ads/{ad_id}/analysis. The indexing-side taxonomy has 12 categories (it adds misinformation). See Taxonomies and verdicts.

Recipe: Submit and act on the verdict​

A typical integration: screen, branch on block_decision, and hand the explanation to a human if blocked.

  1. Screen the creative

    Upload the file (v1) or pass a registered ad_id (v1.1), with your ad-hoc rules.

  2. Check `screening_failed` first

    If it is true, the block is precautionary. Retry once under your deadline; do not show it as a content block.

  3. Branch on `block_decision`

    allow → clear for delivery. block → hold for review, attaching explanation, the garm[].category slugs and the matched rules.

response=$(curl -s -X POST https://video.pyler.ai/api/v1/ad-blocking \
-H "Authorization: Bearer $VUF_API_KEY" \
-F "file=@./ads/new-creative.mp4" \
-F "title=spring-2026-launch" \
-F "rules=no lingerie or swimwear modeling, no on-screen consumption of alcohol")

if [ "$(echo "$response" | jq -r .screening_failed)" = "true" ]; then
echo "Screening failed — retry"; exit 1
fi

if [ "$(echo "$response" | jq -r .block_decision)" = "block" ]; then
echo "BLOCKED — hold for human review"
echo "$response" | jq -r .explanation
echo "GARM:"; echo "$response" | jq -r '.garm[].category'
echo "Matched rules:"; echo "$response" | jq -r '.blocklist.hits[] | select(.matched) | .rule'
else
echo "Cleared for delivery"
fi

An allow response looks like this:

{
"id": "1a2b3c4d-0000-0000-0000-000000000000",
"ad_id": null,
"title": "spring-2026-launch",
"duration_ms": 15000,
"one_line": "Unbranded running-shoe ad; runner on a coastal road at sunrise.",
"garm": [],
"blocklist": { "rules": ["no on-screen consumption of alcohol"], "hits": [] },
"block_decision": "allow",
"explanation": "ALLOWED — no GARM or blocklist violation. Standard product showcase with voiceover.",
"screening_failed": false,
"created_at": "2026-06-16T10:25:14Z"
}

Timeouts and retries​

A timeout is an unknown outcome, not a failure. Once the request is accepted, screening runs to completion and the result is stored whether or not your connection is still open. Only the response is lost, so do not assume the ad was not screened.

  • Recommended client timeout: 120 seconds. Screening normally returns in seconds; the ceiling is for the tail.

  • Raising it past 5 minutes has no effect. A request that has produced no data for 5 minutes is closed with 504. Streaming endpoints (/api/v1/ads/match, /api/v1/videos/{video_id}/scenes/stream) keep producing data and can run longer; a quiet request ends at the 5-minute mark.

  • Recover, do not re-submit. For a v1.1 screening, look the result up by ad_id instead of screening again. It is cheaper and returns the original verdict:

    curl "https://video.pyler.ai/api/v1.1/ad-blocking?ad_id=$AD_ID" \
    -H "Authorization: Bearer $VUF_API_KEY"

    Re-screening the same ad_id is allowed but not idempotent: it runs again and writes a second result.

  • Backoff. One or two retries under an overall deadline is enough. A retry occupies screening capacity that would otherwise serve your other requests, and request-rate limits may apply; see Usage and limits.

Which errors to retry:

HTTPMeaningRetry?
400Invalid upload: missing file, unprobeable, or too long (v1 route).No
401 / 403Missing or invalid key; key used outside the public API roots.No
404Unknown or malformed ad_id, or owned by another team.No
409Creative is not ready.No
422Body or query failed validation (for example a non-UUID ad_id on the lookup).No
500 / 502 / 503 / 504Upstream or edge error.Yes
Network error, client timeoutNo response reached you.Yes, after a lookup

The "no" rows are deterministic: the same request fails the same way, so fix the input instead. 200 with block_decision: "block" and screening_failed: true is also an error path, surfaced as a verdict so your system fails closed; retry it once. All error bodies share the { "detail", "code" } envelope described in Errors.

Content-side brand safety​

Ad screening judges the creative. For the content an ad runs next to, use the indexing output: the video-level garm_video entries on GET /api/v1/videos/{video_id}, the scene-level garm_categories from GET /api/v1/videos/{video_id}/scenes, and the garm_min_level filter on search and the video list.