# Advalidation
> Advalidation is an automated ad creative validation platform. Upload ad creatives (display banners, video files, VAST tags) and each creative is evaluated through automated tests that return structured results such as file weight, dimensions, duration, click-through verification, and SSL compliance. Integrate via the TypeScript SDK (recommended) or the REST API.
Questions, need an API key, or want to discuss integration? Email us at hello@advalidation.com
The full machine-readable API specification is available at [openapi.json](https://advalidation.com/openapi.json).
---
# TypeScript SDK
The fastest way to integrate Advalidation. One call to validate a creative, no manual polling, no response parsing. Zero runtime dependencies.
Source code: [github.com/advalidation/sdk](https://github.com/advalidation/sdk)
## Install
```
npm install advalidation
```
## Quick start
```ts
import { Advalidation } from "advalidation";
const client = new Advalidation({ apiKey: "your-api-key" });
const result = await client.validate({
url: "https://rtr.innovid.com/r1.66f3e735e66ba5.38642747;cb=[timestamp]",
type: "video",
});
console.log(result.passed); // true or false
console.log(result.issues); // number of failed tests
console.log(result.reportUrl); // link to the full visual report
```
That's it. The SDK resolves the ad specification, creates a campaign, uploads, polls until done, and returns the result.
## Authentication
```ts
// Pass directly
const client = new Advalidation({
apiKey: "your-api-key",
baseUrl: "https://app.advalidation.io", // optional, this is the default
});
// Or use environment variables (ADVALIDATION_API_KEY, ADVALIDATION_BASE_URL)
const client = new Advalidation();
```
Constructor options take precedence over environment variables. `baseUrl` defaults to `https://app.advalidation.io`.
## Creative input types
Exactly one of `url`, `tag`, or `file` must be provided.
**URL** -- hosted creative, VAST XML endpoint, or ad tag URL. The file is fetched server-side by Advalidation, so there is no upload size limit.
```ts
await client.validate({ url: "https://example.com/ad.html", type: "display" });
```
**Tag** -- raw HTML/JavaScript ad tag or VAST XML string.
```ts
await client.validate({ tag: "", type: "display" });
```
**File** -- local file path. Supports ZIP archives, images, video files, and HTML files. Uploads are limited to **16 MB** -- use `url` instead for larger files.
```ts
await client.validate({ file: "/path/to/video.mp4", type: "video" });
```
> **Note:** The creative must match the campaign type. Uploading a video file against a `type: "display"` spec (or vice versa) will fail with an `ApiError`.
## Target options
Every validation needs to know where to put the creative and which ad specification to use. Provide exactly one of `campaign`, `spec`, or `type`.
**Existing campaign** -- upload into an existing campaign (adspec is inherited):
```ts
await client.validate({ url: "https://example.com/ad.html", campaign: 12345 });
```
**By spec ID** -- use a specific ad specification (creates a new campaign):
```ts
await client.validate({ url: "https://example.com/ad.html", spec: "123" });
```
**By type** -- use the default ad specification for a type (creates a new campaign):
```ts
await client.validate({ url: "https://example.com/ad.html", type: "display" });
await client.validate({ url: "https://example.com/ad.html", type: "video" });
```
> **Note:** `type` resolves to whichever ad specification is marked as default for that type in the Advalidation UI. If the default is changed in the UI, subsequent SDK runs will use the new one. Use `spec` with an explicit ID if you need a pinned ad specification.
## Summary vs detailed results
By default, `validate()` and `getResults()` return a **summary** with pass/fail, issue count, and report URL -- no extra API calls beyond what's needed for scanning.
```ts
const result = await client.validate({ url: "https://example.com/ad.html", type: "display" });
console.log(result.passed); // true
console.log(result.issues); // 0
console.log(result.reportUrl); // "https://app.advalidation.com/share/..."
console.log(result.tests); // [] (empty in summary mode)
```
Pass `details: true` to fetch the full test breakdown, including individual test results, VAST media files, and variations. This requires additional API calls (20+ for complex VAST creatives).
```ts
const result = await client.validate({
url: "https://example.com/vast.xml",
type: "video",
details: true,
});
console.log(result.tests); // full test results
console.log(result.mediaFiles); // VAST media files with their tests
```
You can also start with a summary and fetch details later using `getResults()`:
```ts
// Fast CI gate -- summary only
const summary = await client.validate({ url: "https://example.com/ad.html", type: "display" });
if (!summary.passed) {
// Fetch full details for the failure report
const detailed = await client.getResults(summary.creativeId, { details: true });
if (detailed.status === "finished") {
console.log(detailed.tests);
}
}
```
## Fetch existing results
Already have a creative ID from a previous run or the Advalidation UI? Skip the upload and poll:
```ts
const response = await client.getResults(creativeId);
if (response.status === "finished") {
console.log(response.passed, response.issues, response.reportUrl);
} else {
console.log(response.status); // "pending", "failed", or "cancelled"
}
// Full test breakdown (only available when finished)
const detailed = await client.getResults(creativeId, { details: true });
if (detailed.status === "finished") {
console.log(detailed.tests);
}
```
`getResults()` returns a discriminated union -- check `status` before accessing result fields. See Result shape for the full type.
| Option | Type | Default | Description |
|-----------|-----------|---------|--------------------------------------------------|
| `verbose` | `boolean` | `false` | Log progress to console. |
| `details` | `boolean` | `false` | Fetch full test breakdown including VAST variations and media files. Only applies when status is `"finished"`. |
## Serverless / split workflow
`validate()` bundles upload + polling in a single long-running call. In serverless environments (Vercel, AWS Lambda, Cloudflare Workers) the function may timeout before the scan completes. Use `submit()` + `getResults()` to split the workflow across separate requests.
```ts
// Request 1: submit the creative (fast -- no polling)
const { creativeId } = await client.submit({
url: "https://example.com/vast.xml",
type: "video",
});
// Store creativeId (database, KV, cookie, query param, etc.)
// Request 2+: poll from separate short-lived requests
const response = await client.getResults(creativeId);
if (response.status === "finished") {
console.log(response.passed, response.issues);
} else if (response.status === "pending") {
// Not done yet -- try again in a few seconds
}
```
`submit()` accepts the same creative and targeting options as `validate()`, minus `timeout` and `details` (irrelevant without polling).
## Options reference
All options are passed in the same object as the creative input. `submit()` accepts the same options as `validate()` except `timeout` and `details`.
| Field | Type | Default | `validate` | `submit` | Description |
|-----------|----------------------------|-----------|:----------:|:--------:|--------------------------------------------------|
| `url` | `string` | - | x | x | URL of the hosted creative. Mutually exclusive with `file`, `tag`, and `data`. |
| `file` | `string` | - | x | x | Local file path. Mutually exclusive with `url`, `tag`, and `data`. |
| `tag` | `string` | - | x | x | Raw ad tag markup. Mutually exclusive with `url`, `file`, and `data`. |
| `data` | `Buffer \| Uint8Array` | - | x | x | Raw file bytes. Mutually exclusive with `url`, `file`, and `tag`. |
| `fileName`| `string` | - | x | x | Filename sent with `data` uploads. Only used with `data`. |
| `campaign`| `number` | - | x | x | Existing campaign ID. Adspec is inherited. Mutually exclusive with `spec` and `type`. |
| `spec` | `string` | - | x | x | Ad specification ID. Creates a new campaign. Mutually exclusive with `campaign` and `type`. |
| `type` | `"display" \| "video"` | - | x | x | Use the default ad specification for this type. Creates a new campaign. Mutually exclusive with `campaign` and `spec`. |
| `name` | `string` | auto | x | x | Campaign name. Auto-generated from the input if omitted. |
| `timeout` | `number` | `300000` | x | - | Polling timeout in milliseconds (default 5 minutes). |
| `signal` | `AbortSignal` | - | x | x | Standard `AbortSignal` for cancellation. |
| `verbose` | `boolean` | `false` | x | x | Log progress messages to console. |
| `details` | `boolean` | `false` | x | - | Fetch full test breakdown including VAST variations and media files. |
## Result shape
`validate()` returns `ValidationResult` directly (polling guarantees a finished scan).
`getResults()` returns `GetResultsResponse` -- a discriminated union:
```ts
type GetResultsResponse =
| { status: "pending"; creativeId: number }
| { status: "failed"; creativeId: number }
| { status: "cancelled"; creativeId: number }
| (ValidationResult & { status: "finished" });
```
`submit()` returns `SubmitResult`:
```ts
interface SubmitResult {
campaignId: number;
creativeId: number;
}
```
`ValidationResult` (returned by `validate()` and embedded in `GetResultsResponse` when finished):
```ts
interface ValidationResult {
campaignId: number;
creativeId: number;
scanId: number;
passed: boolean; // true if zero issues
reportUrl: string; // link to the full visual report
issues: number; // count of failed tests
tests: Test[]; // test results for this creative
mediaFiles: MediaFile[]; // VAST child video files (non-variation VAST only)
variations: Variation[]; // VAST variations (multi-variation VAST only)
}
interface Test {
name: string; // test identifier
value: string | number | boolean | null; // measured value
valueFormatted: string | null; // human-readable value
result: "pass" | "fail" | "warn"; // outcome
spec: string | null; // threshold from the adspec
}
```
For non-VAST creatives, `tests` has the results and both `mediaFiles` and `variations` are empty. For VAST creatives the result is nested:
```ts
// VAST without variations -- media files at the top level
result.mediaFiles[0].tests // tests for the first video rendition
result.mediaFiles[0].issues // failed test count for that rendition
// VAST with variations -- variations contain media files
result.variations[0].label // "Variation A"
result.variations[0].mediaFiles[0].tests // tests for that rendition
```
## Error handling
All errors extend `AdvalidationError`.
| Error class | When thrown |
|----------------------|--------------------------------------------------------------|
| `AuthenticationError`| API returns 401 (invalid or missing API key). |
| `InputError` | Invalid parameters (missing input, both `spec` and `type` provided, etc). |
| `ApiError` | API returns a non-401 error. Has `status` and `body` properties. |
| `RateLimitError` | API returned 429 repeatedly (after 3 retries with backoff). |
| `ScanFailedError` | The scan finished with a `failed` status. |
| `ScanCancelledError` | The scan was cancelled. |
| `TimeoutError` | Scan did not complete within the timeout period. |
| `AbortError` | The operation was aborted via the provided `AbortSignal`. |
```ts
import {
Advalidation,
AuthenticationError,
InputError,
ApiError,
TimeoutError,
} from "advalidation";
try {
const result = await client.validate({
url: "https://example.com/ad.html",
type: "display",
});
} catch (error) {
if (error instanceof AuthenticationError) {
console.error("Bad API key");
} else if (error instanceof InputError) {
console.error("Invalid input:", error.message);
} else if (error instanceof ApiError) {
console.error(`API error ${error.status}:`, error.body);
} else if (error instanceof TimeoutError) {
console.error("Scan timed out");
}
}
```
## Requirements
- Node.js >= 18 (uses native `fetch`)
- Zero runtime dependencies
---
# REST API
All requests use the base URL `https://app.advalidation.io/v2` and require the `X-API-Key` header.
---
# Getting started
This guide walks you through the core API operations: verifying your API key, creating a campaign, uploading a creative, polling for results, and retrieving scan data.
## Prerequisites
- An API key -- contact your account manager to get one.
## Setup
All examples use the base URL `https://app.advalidation.io/v2` and require the `X-API-Key` header.
```js
const BASE_URL = "https://app.advalidation.io/v2";
const headers = {
Accept: "application/json",
"X-API-Key": "your-api-key-here",
};
```
## 1. Verify your API key
Check that your key works by fetching your user profile.
**curl:**
```bash
curl https://app.advalidation.io/v2/users/me \
-H "Accept: application/json" \
-H "X-API-Key: your-api-key-here"
```
**JavaScript:**
```js
const response = await fetch(`${BASE_URL}/users/me`, { headers });
const { data } = await response.json();
console.log(data);
```
## 2. List available ad specifications
Ad specifications define what tests run against your creatives. Each account has a default specification for display and one for video. If the defaults work for you, you can skip this step and pass `adspecId: null` when creating a campaign. Otherwise, list the available specifications and note the `id` of the one you need.
**curl:**
```bash
curl https://app.advalidation.io/v2/ad-specifications \
-H "Accept: application/json" \
-H "X-API-Key: your-api-key-here"
```
**JavaScript:**
```js
const response = await fetch(`${BASE_URL}/ad-specifications`, { headers });
const { data } = await response.json();
console.log(data);
```
## 3. Create a new campaign
Campaigns group creatives together. Set `type` to `display` or `video`, and pass an `adspecId` from step 2 or `null` for the default.
**curl:**
```bash
curl -X POST https://app.advalidation.io/v2/campaigns \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-H "X-API-Key: your-api-key-here" \
-d '{"name": "My first campaign", "type": "display", "adspecId": null}'
```
**JavaScript:**
```js
const response = await fetch(`${BASE_URL}/campaigns`, {
method: "POST",
headers: { ...headers, "Content-Type": "application/json" },
body: JSON.stringify({
name: "My first campaign",
type: "display",
adspecId: null,
}),
});
const { data } = await response.json();
const campaignId = data.id;
console.log("Campaign ID:", campaignId);
```
## 4. Upload a creative
Upload a creative to your campaign. The simplest method sends a URL or HTML tag as a JSON payload. See the file upload guide for other methods including binary and base64.
**curl:**
```bash
curl -X POST https://app.advalidation.io/v2/campaigns/CAMPAIGN_ID/creatives \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-H "X-API-Key: your-api-key-here" \
-d '{"payload": ""}'
```
**JavaScript:**
```js
const response = await fetch(
`${BASE_URL}/campaigns/${campaignId}/creatives`,
{
method: "POST",
headers: { ...headers, "Content-Type": "application/json" },
body: JSON.stringify({
payload:
'',
}),
}
);
const { data } = await response.json();
const creativeId = data[0].id;
console.log("Creative ID:", creativeId);
```
## 5. Poll until the scan finishes
Most scans complete within 30 seconds. Poll the creative endpoint until `processingStatus` is `finished`. We recommend polling every 20 seconds.
**curl:**
```bash
# Repeat until processingStatus is "finished"
curl https://app.advalidation.io/v2/creatives/CREATIVE_ID \
-H "Accept: application/json" \
-H "X-API-Key: your-api-key-here"
```
**JavaScript:**
```js
async function waitForScan(creativeId) {
while (true) {
const response = await fetch(`${BASE_URL}/creatives/${creativeId}`, {
headers,
});
const { data } = await response.json();
const status = data.latestScanStatus?.processingStatus;
if (status === "finished") {
console.log("Scan finished. Scan ID:", data.latestScanStatus.id);
return data.latestScanStatus.id;
}
if (status !== "queued" && status !== "processing") {
throw new Error(`Unexpected status: ${status}`);
}
await new Promise((resolve) => setTimeout(resolve, 20000));
}
}
const scanId = await waitForScan(creativeId);
```
## 6. Retrieve scan results
Test results live on the scan object, not the creative. Use the scan ID from step 5 to fetch results. For VAST creatives, the scan hierarchy can be more complex (media files, variations). See the data model for the full picture.
**curl:**
```bash
curl https://app.advalidation.io/v2/scans/SCAN_ID \
-H "Accept: application/json" \
-H "X-API-Key: your-api-key-here"
```
**JavaScript:**
```js
const response = await fetch(`${BASE_URL}/scans/${scanId}`, { headers });
const { data } = await response.json();
console.log("Issues found:", data.nbIssues);
console.log("Tests:", data.tests);
```
The `tests` array contains each check with its `name`, `value`, `result` (`pass`, `fail`, or `warn`), and detailed `attributes`. For display and hosted video creatives, this single scan contains all test results. For VAST creatives, additional scans may exist at deeper levels (media files, variations). See the data model for how to navigate the hierarchy.
## 7. Re-scan a creative
If the ad specification changes or remote tag assets are updated (e.g. a VAST or HTML tag), you can re-scan without re-uploading. The request body must be an empty JSON object (`{}`).
**curl:**
```bash
curl -X POST https://app.advalidation.io/v2/creatives/CREATIVE_ID/rescan \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-H "X-API-Key: your-api-key-here" \
-d '{}'
```
**JavaScript:**
```js
const response = await fetch(`${BASE_URL}/creatives/${creativeId}/rescan`, {
method: "POST",
headers: { ...headers, "Content-Type": "application/json" },
body: JSON.stringify({}),
});
const { data } = await response.json();
console.log("Rescan ID:", data.id);
```
After triggering a rescan, poll for results using the same approach as step 5. The rescan's `sequenceType` will be `manual-rescan`.
---
# Data model
Campaigns contain creatives. Every creative has a `latestScanStatus.id`, a scan ID. Call `/scans/{scanId}` on any scan ID to get its test results.
Display creatives are always flat: one creative, one scan. Video creatives can have additional layers (media files and variations). The structures below show how they are organized and which endpoints reveal each level.
## Creative structures
### Display creative
All display types (hosted images, HTML5, tags) follow the same flat structure. One creative, one scan.
```
GET /campaigns/{campaignId}/creatives
Creative <- has latestScanStatus.id
GET /scans/{scanId} <- display tests (dimensions, filesize, load time, etc.)
```
Display `sourceType` values include `image`, `html5`, `tag-html5`, `tag-beacon`, among others. The test set varies by type but the structure is always the same.
### Hosted video
Same flat structure as display. One file, one scan.
```
GET /campaigns/{campaignId}/creatives
Creative <- has latestScanStatus.id
GET /scans/{scanId} <- video tests (resolution, duration, codec, etc.)
```
### VAST without variations
The creative holds the VAST XML scan. Its media files each have their own video scan.
```
GET /campaigns/{campaignId}/creatives
Creative <- has latestScanStatus.id
|
| GET /scans/{scanId} <- VAST XML tests (version, validation, skip, etc.)
|
+-- GET /creatives/{id}/media-files
|
+-- Media file <- has latestScanStatus.id
| GET /scans/{scanId} <- video tests (duration, codec, audio, etc.)
|
+-- Media file <- has latestScanStatus.id
| GET /scans/{scanId} <- video tests
|
+-- ...
```
### VAST with variations
Detected by `nbVariations` being present on the creative. The variations endpoint returns both the variation XML scan AND its child video scans, so there is no need to call `/media-files`.
```
GET /campaigns/{campaignId}/creatives
Creative <- has latestScanStatus.id
(nbVariations present)
|
| GET /scans/{scanId} <- top-level nbIssues only (no tests array)
|
+-- GET /creatives/{id}/variations
|
+-- Variation A (creativeId, nbObservations)
+-- Variation B
+-- ...
|
+-- GET /creatives/{id}/variations/{variationCreativeId}
|
| Returns ALL items for this variation:
|
+-- vast-variation <- has latestScanStatus.id
| GET /scans/{id} <- VAST XML tests for this variation
|
+-- vast-child-video <- has latestScanStatus.id
| GET /scans/{id} <- video tests
|
+-- vast-child-video <- has latestScanStatus.id
| GET /scans/{id} <- video tests
|
+-- ...
```
## Navigating the hierarchy
Starting from `/campaigns/{campaignId}/creatives`:
```
For each creative:
|
+-- Has nbVariations?
| |
| YES -> VAST with variations
| | 1. /scans/{latestScanStatus.id} -> top-level nbIssues (no tests array)
| | 2. /creatives/{id}/variations -> list variations
| | 3. /creatives/{id}/variations/{varId} -> variation + media files
| | 4. /scans/{id} for each item -> XML or video tests
| |
| NO -> fetch /creatives/{id}/media-files
| |
| +-- Has results -> VAST without variations
| | 1. /scans/{latestScanStatus.id} -> VAST XML tests
| | 2. /scans/{id} for each media file -> video tests
| |
| +-- Empty -> flat creative (display or hosted video)
| 1. /scans/{latestScanStatus.id} -> all tests in one response
```
## Key rules
- **`/scans/{scanId}` is the only endpoint that returns test results.** Everything else is about discovering scan IDs.
- **`nbVariations` on the creative determines the path.** Only VAST creatives can have variations.
- **The variations detail endpoint includes media files.** When using `/creatives/{id}/variations/{variationCreativeId}`, the response contains both the `vast-variation` item and all `vast-child-video` items. No need to call `/media-files` separately.
- **Every node has `latestScanStatus.id`.** Whether it's a creative, a variation, or a media file: if it has `latestScanStatus.id`, you can call `/scans/{id}` on it.
- **Display creatives are always flat.** No variations, no media files. One scan covers everything.
## Scan types
Tests are prefixed `Test_Display_*` or `Test_Video_*` depending on the campaign type.
### Display tests
| Context | Example tests |
|---|---|
| Hosted image | AdDimensions, AnimationLength, Border, Filesize |
| Hosted HTML5 | AdDimensions, Audio, VisualStart, AnimationLength, Border, MouseClick, Filesize, TagLoadInitial, TagLoadSubload, Cpu, ExternalRequests, Connections, Cookies, Ssl, LocalStorage, CodeRules, FlashLso, AdVerificationTags, HeavyAdInterventions |
| HTML5 tag | AdDimensions, Audio, VisualStart, AnimationLength, MouseClick, LandingPage, ClickRedirects, TagLoadInitial, TagLoadSubload, Cpu, ExternalRequests, Connections, Cookies, Ssl, LocalStorage, GdprConsent, CodeRules, FlashLso, AdVerificationTags, HeavyAdInterventions |
| Tracking tag | AdDimensions, Audio, TagLoadInitial, TagLoadSubload, Cpu, ExternalRequests, Connections, Cookies, Ssl, LocalStorage, GdprConsent, FlashLso, AdVerificationTags, HeavyAdInterventions |
### Video tests
| Context | Example tests |
|---|---|
| Hosted video | Resolution, Duration, TotalBitrate, FPS, Filesize, ContainerFormat, Codec, ScanType, AudioCodec, AudioAverage, AudioPeak, AudioSampleRate, ChromaSubsampling, Boxing |
| VAST creative | VastDurationDiscrepancy, VastRequiredMediaFiles, VastVersion, VastValidateXml, VastApiProperties, VastSkipDetection, VastCreativeCount, VastVariations, VastConnectionRules, VastNonHttps, AdVerificationTags |
| VAST variation | Same as VAST creative (per-variation) |
| VAST child video | Duration, FPS, Filesize, Codec, ScanType, AudioCodec, AudioAverage, AudioPeak, AudioSampleRate, VastPropertyDiscrepancy, ChromaSubsampling, Boxing |
---
# Retrieving results for a VAST tag
VAST analysis involves XML validation followed by video asset processing. A VAST tag can return different content on subsequent calls (variations), and each response can reference multiple video files. Retrieving the full scan results may require several API calls. This guide covers the steps involved.
> **Your case may be simpler:** This guide walks through the most complex scenario. If your VAST tag has a single media file and no variations, you can skip the sections that don't apply.
## Prerequisites
You need a campaign with VAST creatives that have already been processed. Make sure you can see results in the Advalidation UI before trying to fetch them via the API.
## Overview
The general flow is:
1. **List creatives** in your campaign from `/campaigns/{campaignId}/creatives`
2. **Check for `nbVariations`** on each creative
3. **If `nbVariations` is present**, use the variations route to get per-variation results (which includes media files)
4. **If no `nbVariations`**, fetch media files from `/creatives/{creativeId}/media-files` and get scan results for each
5. **Fetch test results** from `/scans/{scanId}` for every scan ID discovered
See the data model for the full hierarchy and decision logic.
## Step 1: Find the campaign
Use `/campaigns` to retrieve your campaigns and find the one you need. Note the `id` -- this is your `campaignId`.
```json
{
"data": [
{
"id": 25,
"name": "My VAST campaign",
"adspecId": 11
}
]
}
```
## Step 2: List campaign creatives
Use `/campaigns/{campaignId}/creatives` to list the creatives. The key fields are:
| Field | Description |
|-------|-------------|
| `id` | The creative ID |
| `name` | Creative name |
| `nbVariations` | Number of VAST variations (only present if the tag returns varying responses) |
| `latestScanStatus.id` | The scan ID for retrieving test results |
| `latestScanStatus.nbIssues` | Number of failing tests |
```json
{
"data": [
{
"id": 56,
"name": "Summer Sponsorship 30s",
"latestScanStatus": {
"id": 42,
"processingStatus": "finished",
"nbIssues": 7
}
},
{
"id": 53,
"name": "Pre-roll Demo",
"nbVariations": 4,
"latestScanStatus": {
"id": 61,
"processingStatus": "finished",
"nbIssues": 16
}
}
]
}
```
What you do next depends on whether the creative has `nbVariations`:
- **Has `nbVariations`** -- the VAST tag returns different content on subsequent calls. The top-level scan (`latestScanStatus.id`) contains `nbIssues` as an aggregate count but no `tests` array. The actual test results live on the variations. Go to Step 3a: Variations.
- **No `nbVariations`** -- the creative's `latestScanStatus.id` gives you the VAST XML scan with test results. To get video file results, go to Step 3b: Media files.
## Step 3a: Variations
When Advalidation detects that a VAST tag returns varying responses, it records each variation separately.
> **How variation detection works:** When the VAST variations check is enabled in the ad specification, Advalidation downloads the VAST XML ten times and compares Ad IDs and Creative IDs across each load. The `nbObservations` field on each variation shows how many times it appeared in those ten loads.
**1. List variations** using `/creatives/{creativeId}/variations`:
```json
{
"data": [
{
"creativeId": 111,
"label": "Variation A",
"nbObservations": 4
},
{
"creativeId": 222,
"label": "Variation B",
"nbObservations": 3
}
]
}
```
Note the `creativeId` for each variation.
**2. Get variation details** using `/creatives/{mainCreativeId}/variations/{variationCreativeId}`. For example, `/creatives/53/variations/111`:
```json
{
"data": [
{
"id": 112,
"parentId": 53,
"sourceType": "vast-variation",
"sourceTypeLabel": "VAST variation",
"latestScanStatus": {
"id": 118,
"nbIssues": 1
}
},
{
"id": 113,
"parentId": 53,
"sourceType": "vast-child-video",
"sourceTypeLabel": "VAST video media file",
"latestScanStatus": {
"id": 119,
"nbIssues": 0
}
},
{
"id": 114,
"parentId": 53,
"sourceType": "vast-child-video",
"sourceTypeLabel": "VAST video media file",
"latestScanStatus": {
"id": 120,
"nbIssues": 0
}
}
]
}
```
The response includes:
- Items with `sourceType: "vast-variation"` -- the VAST XML analysis for this variation
- Items with `sourceType: "vast-child-video"` -- each video media file referenced in the tag
Use `latestScanStatus.id` from each item to fetch the test results.
> When using the variations route, media file results are included in the response. There is no need to call the `/media-files` endpoint separately.
## Step 3b: Media files
For VAST creatives without variations, each media file referenced in the tag has its own scan. Fetch them using `/creatives/{creativeId}/media-files`:
```json
{
"data": [
{
"id": 444,
"parentId": 56,
"sourceType": "vast-child-video",
"sourceTypeLabel": "VAST video media file",
"latestScanStatus": {
"id": 4444,
"nbIssues": 0
}
},
{
"id": 555,
"parentId": 56,
"sourceType": "vast-child-video",
"sourceTypeLabel": "VAST video media file",
"latestScanStatus": {
"id": 5555,
"nbIssues": 0
}
}
]
}
```
Use `latestScanStatus.id` from each media file to fetch test results. You can also fetch results for the VAST XML itself using `latestScanStatus.id` from the parent creative (step 2).
## Step 4: Fetch test results
Use `/scans/{scanId}` to get the test results for any scan ID obtained in the previous steps:
```json
{
"data": {
"nbIssues": 2,
"tests": [
{
"name": "Test_Video_Duration",
"value": "15.04",
"valueFormatted": "15 seconds",
"valuePhrase": "Video track duration is 15 seconds (15.01). Audio track duration is 15 seconds (15.04).",
"result": "fail",
"attributes": [
{
"name": "Video_PlayTime",
"description": "Duration of video in milliseconds",
"value": 15.04
}
]
},
{
"name": "Test_Video_Codec",
"value": "H264",
"valueFormatted": "H264",
"valuePhrase": "The video codec is H264.",
"result": "pass",
"attributes": [
{ "name": "Video_Codec", "description": "Video codec", "value": "H264" }
]
}
]
}
}
```
See the test array reference below for all available tests and their fields.
## Retrieving the ad specification
Each campaign has an associated ad specification that defines which tests are run and their expected values. You can retrieve it using the `adspecId` from the campaign object.
Use `/ad-specifications/{adspecId}`:
```json
{
"data": {
"id": 1111,
"name": "VAST",
"isDefault": false,
"isPublic": false,
"tests": [
{
"name": "Test_Video_Resolution",
"conditionsString": "320x240",
"evaluationExpression": "..."
},
{
"name": "Test_Video_AudioCodec",
"conditionsString": "MP3 (MPEG Audio), AAC, WMA",
"evaluationExpression": "..."
}
]
}
}
```
The `conditionsString` field shows the human-readable expected value for each test. The `evaluationExpression` field contains the evaluation logic in JsonLogic format -- this is what Advalidation uses internally to determine pass or fail. Some tests (like `Test_Video_VastCreativeCount`) have no configurable parameters and will not include these fields.
---
# Creative files upload guide
Creatives are uploaded to a campaign using `POST /campaigns/{campaignId}/creatives`. The API supports four upload methods depending on your content type.
> **Campaign type must match:** Uploaded files must match their campaign's type (display or video). Uploading a video file to a display campaign, or vice versa, returns a `422` error with `CREATIVE_UPLOAD_NO_CREATIVES_FOUND`.
## Common headers
All upload methods require these headers:
| Header | Description |
|--------|-------------|
| `X-API-Key` | Your API key |
| `Accept` | `application/json` |
| `Content-Type` | Varies by method (see below) |
| `X-Filename` | Optional. Sets the display name in the Advalidation UI. Defaults to a timestamp-based name if omitted. |
## JSON payload
**Content-Type:** `application/json`
**Best for:** URLs, HTML tags, VAST tags, and other text-based content.
Send the content in a `payload` field:
**curl:**
```bash
curl -X POST https://app.advalidation.io/v2/campaigns/CAMPAIGN_ID/creatives \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-H "X-API-Key: your-api-key-here" \
-H "X-Filename: Interactive.html" \
-d '{"payload": ""}'
```
**JavaScript:**
```js
const response = await fetch(
`${BASE_URL}/campaigns/${campaignId}/creatives`,
{
method: "POST",
headers: {
...headers,
"Content-Type": "application/json",
"X-Filename": "Interactive.html",
},
body: JSON.stringify({
payload:
'',
}),
}
);
const { data } = await response.json();
const creativeId = data[0].id;
```
## JSON payload with base64 encoding
**Content-Type:** `application/json`
**Best for:** Binary files (images, videos, ZIP archives) when you need to use JSON.
Encode the file as a base64 string and send it in the `payload` field:
**curl:**
```bash
BASE64=$(base64 < ./creative.mov)
curl -X POST https://app.advalidation.io/v2/campaigns/CAMPAIGN_ID/creatives \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-H "X-API-Key: your-api-key-here" \
-H "X-Filename: creative.mov" \
-d "{\"payload\": \"$BASE64\"}"
```
**JavaScript:**
```js
import { readFile } from "node:fs/promises";
const file = await readFile("./creative.mov");
const base64 = file.toString("base64");
const response = await fetch(
`${BASE_URL}/campaigns/${campaignId}/creatives`,
{
method: "POST",
headers: {
...headers,
"Content-Type": "application/json",
"X-Filename": "creative.mov",
},
body: JSON.stringify({ payload: base64 }),
}
);
```
> Base64 encoding increases file size by approximately 30%. For large files, use the binary upload method instead.
## Plain text upload
**Content-Type:** `text/plain`
**Best for:** Raw HTML files or plain text content.
Send the file content directly as the request body:
**curl:**
```bash
curl -X POST https://app.advalidation.io/v2/campaigns/CAMPAIGN_ID/creatives \
-H "Accept: application/json" \
-H "Content-Type: text/plain" \
-H "X-API-Key: your-api-key-here" \
-H "X-Filename: creative.html" \
--data-binary @./creative.html
```
**JavaScript:**
```js
import { readFile } from "node:fs/promises";
const content = await readFile("./creative.html", "utf8");
const response = await fetch(
`${BASE_URL}/campaigns/${campaignId}/creatives`,
{
method: "POST",
headers: {
...headers,
"Content-Type": "text/plain",
"X-Filename": "creative.html",
},
body: content,
}
);
```
## Binary upload
**Content-Type:** `application/octet-stream`
**Best for:** ZIP archives, images, videos, and other binary files. Most efficient for large files since there is no encoding overhead.
Send the raw file bytes as the request body:
**curl:**
```bash
curl -X POST https://app.advalidation.io/v2/campaigns/CAMPAIGN_ID/creatives \
-H "Accept: application/json" \
-H "Content-Type: application/octet-stream" \
-H "X-API-Key: your-api-key-here" \
-H "X-Filename: creative.zip" \
--data-binary @./creative.zip
```
**JavaScript:**
```js
import { readFile } from "node:fs/promises";
const file = await readFile("./creative.zip");
const response = await fetch(
`${BASE_URL}/campaigns/${campaignId}/creatives`,
{
method: "POST",
headers: {
...headers,
"Content-Type": "application/octet-stream",
"X-Filename": "creative.zip",
},
body: file,
}
);
```
## Method comparison
| Method | Content-Type | Encoding overhead | Best for |
|--------|-------------|-------------------|----------|
| JSON payload | `application/json` | None (text) | URLs, HTML tags, VAST tags |
| JSON base64 | `application/json` | ~30% larger | Binary files when JSON is required |
| Plain text | `text/plain` | None | Raw HTML files |
| Binary | `application/octet-stream` | None | ZIP, images, videos (most efficient) |
> **Upload limits:** All uploads are subject to a maximum payload size of 16 MB. For base64-encoded uploads, the encoded size is approximately 33% larger than the original file. This limit does not apply when submitting a URL -- the file is downloaded by Advalidation directly.
---
# Conventions
## Successful responses
Successful requests return `200 OK` or `204 No Content`.
Responses that include data always have a `data` property. Single-resource endpoints return the object directly:
```json
{
"data": {
"id": 123,
"name": "My campaign"
}
}
```
Collection endpoints return an array in `data`, and may include a `meta` property with pagination information.
## Empty collections vs. not found
- **Collection endpoints** return an empty `data` array when no items match -- not a `404`.
- **Single-resource endpoints** return `404 Not Found` when the requested ID does not exist.
## Error responses
Error responses return an appropriate HTTP status code (`401`, `404`, `422`, etc.) with an `error` property in the response body explaining the issue.
## Content type
The API accepts and returns `application/json` for most requests. Binary uploads use `application/octet-stream` or `text/plain` -- see the file upload guide for details.
---
# Rate limiting
The API enforces IP-based rate limiting to ensure fair usage across all clients.
## Response headers
Every response includes headers indicating your current rate limit status:
| Header | Description |
|--------|-------------|
| `X-RateLimit-IPLimit` | Maximum requests allowed in the current window |
| `X-RateLimit-IPRemaining` | Requests remaining in the current window |
Authenticated requests (those with a valid `X-API-Key` header) receive a higher allowance than unauthenticated requests.
## Exceeding the limit
Requests over the allowed quota return `429 Too Many Requests`. When this happens, back off and wait before retrying. Monitor the `X-RateLimit-IPRemaining` header to pace your requests and avoid hitting the limit.
---
# The test array
When you retrieve scan results, the response includes a `tests` array. Each entry in that array corresponds to one of the tests documented below, with a `name`, `value`, `result`, and `attributes`.
## Display tests
This section lists every display test that Advalidation can run against a creative. Each test checks one aspect of the creative and returns `pass`, `fail`, or `warn`.
### Test_Display_AdDimensions
Checks if the creative's pixel dimensions match one of the accepted ad unit sizes defined in the ad specification.
| Result | Condition |
|--------|-----------|
| **pass** | Dimensions match an accepted size |
| **fail** | Dimensions do not match any accepted size |
Example response:
```json
{
"name": "Test_Display_AdDimensions",
"value": "300x250",
"valueFormatted": "300x250",
"valuePhrase": "300x250 matches one of the accepted dimensions.",
"result": "pass",
"attributes": [
{ "name": "Display_Width", "description": "Pixel width of creative", "value": 300 },
{ "name": "Display_Height", "description": "Pixel height of creative", "value": 250 }
]
}
```
**Attributes:** `Display_Width`, `Display_Height`
---
### Test_Display_AdVerificationTags
Detects ad verification vendor tags (DoubleVerify, IAS, MOAT, etc.) and checks whether they are set to monitor or block delivery. Blocking tags can cause impression discrepancies between your ad server and the advertiser's reporting.
| Result | Condition |
|--------|-----------|
| **pass** | Only monitoring tags detected, or tags match the ad spec policy |
Example response:
```json
{
"name": "Test_Display_AdVerificationTags",
"value": "monitoring",
"valueFormatted": "Monitoring",
"valuePhrase": "cdn.doubleverify.com/dvtp_src.js detected in third party tag. This allows the ad verification vendor to monitor tag performance. Only monitoring tags allowed.",
"result": "pass",
"attributes": [
{ "name": "Display_AdVerificationType", "description": "Display ad verification type (blocking/monitoring)", "value": "monitoring" },
{ "name": "Display_AdVerificationVendor", "description": "Display ad verification vendor", "value": "doubleverify" }
]
}
```
**Attributes:** `Display_AdVerificationType`, `Display_AdVerificationVendor`
---
### Test_Display_AnimationLength
Checks if the ad's animation stops within the allowed duration. Long-running animations distract visitors and violate most publisher specifications.
| Result | Condition |
|--------|-----------|
| **pass** | Animation stops within the threshold (e.g., 15 seconds) |
| **fail** | Animation runs longer than allowed |
Example response:
```json
{
"name": "Test_Display_AnimationLength",
"value": 0,
"valueFormatted": "0 seconds",
"valuePhrase": "The ad animates for 0 seconds. Animation must stop within 15 seconds.",
"result": "pass",
"attributes": [
{ "name": "Display_AnimationDuration", "description": "Animation duration in milliseconds", "value": 0 }
]
}
```
**Attributes:** `Display_AnimationDuration`
---
### Test_Display_Audio
Checks if the creative plays audio before user interaction. Most publisher specs prohibit auto-playing audio.
| Result | Condition |
|--------|-----------|
| **pass** | No audio detected before hover |
| **fail** | Audio plays automatically |
Example response:
```json
{
"name": "Test_Display_Audio",
"value": false,
"valueFormatted": "Not detected",
"valuePhrase": "No audio before hover detected. Audio is not allowed before hover.",
"result": "pass",
"attributes": [
{ "name": "Display_AudioBeforeHover", "description": "Audio before hover", "value": false },
{ "name": "Display_AudioAfterHover", "description": "Audio after hover", "value": false }
]
}
```
**Attributes:** `Display_AudioBeforeHover`, `Display_AudioAfterHover`
---
### Test_Display_Border
Checks if the creative has a distinct border that visually separates it from page content. This is a common publisher requirement to prevent ads from blending into editorial content.
| Result | Condition |
|--------|-----------|
| **pass** | Distinct border detected |
| **warn** | No distinct border detected |
Example response:
```json
{
"name": "Test_Display_Border",
"value": true,
"valueFormatted": "Indistinct border",
"valuePhrase": "Distinct border not detected. Border required.",
"result": "warn",
"attributes": [
{ "name": "Display_LowContrastBorder", "description": "Low contrast border", "value": true }
]
}
```
**Attributes:** `Display_LowContrastBorder`
---
### Test_Display_ClickRedirects
Counts the number of redirects between clicking the ad and reaching the landing page. Excessive redirects cause visitors to drop off before arriving.
| Result | Condition |
|--------|-----------|
| **pass** | Redirect count within limit (e.g., 6 max) |
| **fail** | Too many redirects |
Example response:
```json
{
"name": "Test_Display_ClickRedirects",
"value": 1,
"valueFormatted": "1 redirect",
"valuePhrase": "Detected 1 redirect. No more than 6 redirects allowed.",
"result": "pass",
"attributes": [
{ "name": "Display_ClickRedirects", "description": "Redirects on click", "value": 1 }
]
}
```
**Attributes:** `Display_ClickRedirects`
---
### Test_Display_CodeRules
Validates the creative's source code against custom rules defined in the ad specification. These rules can check for prohibited code patterns, required scripts, or other code-level requirements.
| Result | Condition |
|--------|-----------|
| **pass** | Code passes all custom validation rules |
| **fail** | Code violates one or more rules |
Example response:
```json
{
"name": "Test_Display_CodeRules",
"value": 1,
"valueFormatted": "1 issue",
"valuePhrase": "This ad fails custom code validation rules set up by the ad spec.",
"result": "fail",
"attributes": []
}
```
---
### Test_Display_Connections
Checks the creative's external connections against an approved vendor list defined in the ad specification. Connections to unapproved domains are flagged.
| Result | Condition |
|--------|-----------|
| **pass** | All connections match approved vendors |
| **fail** | One or more connections violate the rules |
Example response:
```json
{
"name": "Test_Display_Connections",
"value": 4,
"valueFormatted": "4 violations",
"valuePhrase": "This ad violates the ad specification rules for external connections.",
"result": "fail",
"attributes": []
}
```
---
### Test_Display_Cookies
Counts the number of cookies set by the creative and checks against the allowed limit.
| Result | Condition |
|--------|-----------|
| **pass** | Cookie count within limit |
| **fail** | Too many cookies |
Example response:
```json
{
"name": "Test_Display_Cookies",
"value": 0,
"valueFormatted": 0,
"valuePhrase": "Detected no cookies. The maximum number of cookies allowed is 0.",
"result": "pass",
"attributes": [
{ "name": "Display_TotalCookies", "description": "Number of cookies", "value": 0 }
]
}
```
**Attributes:** `Display_TotalCookies`
---
### Test_Display_Cpu
Measures the creative's CPU usage as average and peak percentages. High CPU usage drains battery life and degrades the browsing experience, especially on mobile devices.
| Result | Condition |
|--------|-----------|
| **pass** | Both average and peak within limits |
| **fail** | Average or peak exceeds the threshold |
Example response:
```json
{
"name": "Test_Display_Cpu",
"value": 1,
"valueFormatted": "1% average, 4% peak",
"valuePhrase": "CPU usage within allowed limits. CPU usage during testing was: 1% average, 4% peak. The maximum CPU usage allowed is 10% average, 20% peak.",
"result": "pass",
"attributes": [
{ "name": "Display_CPUAverage", "description": "CPU average", "value": 1 },
{ "name": "Display_CPUPeak", "description": "CPU peak", "value": 4 }
]
}
```
**Attributes:** `Display_CPUAverage`, `Display_CPUPeak`
---
### Test_Display_ExternalRequests
Counts the total number of HTTP requests the creative makes and checks against the allowed limit. Fewer requests means faster load times and better viewability.
| Result | Condition |
|--------|-----------|
| **pass** | Request count within limit |
| **fail** | Too many requests |
Example response:
```json
{
"name": "Test_Display_ExternalRequests",
"value": 5,
"valueFormatted": 5,
"valuePhrase": "This ad is making 5 requests. The maximum allowed is 15 requests.",
"result": "pass",
"attributes": [
{ "name": "Display_FileRequestsTotal", "description": "Total requests (post exclusions)", "value": 4 },
{ "name": "Display_FileRequestsInitial", "description": "Initial requests (post exclusions)", "value": 4 }
]
}
```
**Attributes:** `Display_FileRequestsTotal`, `Display_FileRequestsInitial`
---
### Test_Display_Filesize
Checks the total hosted file size of the creative's assets against the allowed limit. Oversized creatives hurt viewability and user experience.
| Result | Condition |
|--------|-----------|
| **pass** | File size within limit |
| **fail** | File size exceeds limit |
Example response:
```json
{
"name": "Test_Display_Filesize",
"value": "141977",
"valueFormatted": "139 kb",
"valuePhrase": "The hosted filesize of this ad is 139 kb. The maximum hosted filesize allowed is 100 kb.",
"result": "fail",
"attributes": [
{ "name": "Display_HTML5InflatedSize", "description": "Hosted filesize (sum)", "value": 141977 }
]
}
```
**Attributes:** `Display_HTML5InflatedSize`
---
### Test_Display_FlashLso
Checks if the creative uses Flash local shared objects (Flash cookies). These persist after clearing browser cookies and are generally prohibited.
| Result | Condition |
|--------|-----------|
| **pass** | No Flash cookies detected |
| **fail** | Flash cookies detected |
Example response:
```json
{
"name": "Test_Display_FlashLso",
"value": false,
"valueFormatted": "Not detected",
"valuePhrase": "Detected no flash cookies.",
"result": "pass",
"attributes": []
}
```
---
### Test_Display_GdprConsent
Checks if the creative's source code contains the GDPR consent macro, which is required for GDPR-compliant ad delivery in applicable regions.
| Result | Condition |
|--------|-----------|
| **pass** | GDPR consent macro detected |
| **fail** | Macro not found |
Example response:
```json
{
"name": "Test_Display_GdprConsent",
"value": "Detected",
"valueFormatted": "Detected",
"valuePhrase": "This creative's code contains the GDPR consent macro.",
"result": "pass",
"attributes": []
}
```
---
### Test_Display_HeavyAdInterventions
Checks if the creative would trigger Chrome's heavy ad intervention, which removes ads that consume excessive CPU or network resources. Chrome considers an ad "heavy" if it uses more than 4 MB of network, more than 15 seconds of CPU in any 30-second window, or more than 60 seconds of CPU total.
| Result | Condition |
|--------|-----------|
| **pass** | Resource usage within Chrome's limits |
| **fail** | One or more thresholds exceeded |
Example response:
```json
{
"name": "Test_Display_HeavyAdInterventions",
"value": false,
"valueFormatted": "Not detected",
"valuePhrase": "Chrome heavy ad interventions not detected. Ads triggering heavy ad intervention are not accepted.",
"result": "pass",
"attributes": []
}
```
---
### Test_Display_LandingPage
Verifies that the ad's landing page URL resolves to a working page. Catches broken URLs, malformed click macros, and redirect failures before launch.
| Result | Condition |
|--------|-----------|
| **pass** | Landing page returns HTTP 200 |
| **fail** | Landing page returns an error or does not resolve |
Example response:
```json
{
"name": "Test_Display_LandingPage",
"value": true,
"valueFormatted": "Working",
"valuePhrase": "The landing page works. Response code must be 200.",
"result": "pass",
"attributes": [
{ "name": "Display_LandingPageResponseCode", "description": "Landing page HTTP code", "value": 200 }
]
}
```
**Attributes:** `Display_LandingPageResponseCode`
---
### Test_Display_LocalStorage
Checks if the creative uses browser local storage. Local storage persists data that cannot be cleared through standard cookie controls, making it a privacy concern.
| Result | Condition |
|--------|-----------|
| **pass** | No local storage usage detected |
| **fail** | Local storage objects detected |
Example response:
```json
{
"name": "Test_Display_LocalStorage",
"value": false,
"valueFormatted": "Not detected",
"valuePhrase": "Detected no local storage objects. Local storage objects are not allowed.",
"result": "pass",
"attributes": []
}
```
---
### Test_Display_MouseClick
Verifies that click tracking works via the click macro. This ensures the publisher can count clicks and attribute conversions.
| Result | Condition |
|--------|-----------|
| **pass** | Click tracking via macro is working |
| **fail** | Click tracking is not functioning |
Example response:
```json
{
"name": "Test_Display_MouseClick",
"value": true,
"valueFormatted": "Trackable",
"valuePhrase": "Click tracking works as expected. Click tracking via macro required.",
"result": "pass",
"attributes": [
{ "name": "Display_ClickTracking", "description": "Click is trackable", "value": true }
]
}
```
**Attributes:** `Display_ClickTracking`
---
### Test_Display_Ssl
Checks that all network requests made by the creative use HTTPS. Non-HTTPS requests can trigger mixed content warnings and be blocked by browsers.
| Result | Condition |
|--------|-----------|
| **pass** | All requests use HTTPS |
| **fail** | One or more non-HTTPS requests detected |
Example response:
```json
{
"name": "Test_Display_Ssl",
"value": 0,
"valueFormatted": "No issues",
"valuePhrase": "This ad is not making any non HTTPS requests. Only HTTPS allowed.",
"result": "pass",
"attributes": [
{ "name": "Display_NonHTTPSRequests", "description": "Non-HTTPS assets", "value": 0 }
]
}
```
**Attributes:** `Display_NonHTTPSRequests`
---
### Test_Display_TagLoadInitial
Measures the total file size of assets loaded during the initial load phase (before the browser's `load` event). Keeping initial load lightweight ensures the ad doesn't compete with page content for bandwidth.
| Result | Condition |
|--------|-----------|
| **pass** | Initial load size within limit |
| **fail** | Initial load size exceeds limit |
Example response:
```json
{
"name": "Test_Display_TagLoadInitial",
"value": "35091",
"valueFormatted": "34 kb",
"valuePhrase": "This ad is loading 34 kb within the initial load phase. The maximum allowed is 150 kb.",
"result": "pass",
"attributes": [
{ "name": "Display_RemoteLoadInitial", "description": "Initial load bytes (post exclusions)", "value": 35091 }
]
}
```
**Attributes:** `Display_RemoteLoadInitial`
---
### Test_Display_TagLoadSubload
Measures the total file size of assets loaded during the subload phase (after the browser's `load` event). Subload has a higher budget than initial load since the page has already rendered.
| Result | Condition |
|--------|-----------|
| **pass** | Subload size within limit |
| **fail** | Subload size exceeds limit |
Example response:
```json
{
"name": "Test_Display_TagLoadSubload",
"value": "906",
"valueFormatted": "1 kb",
"valuePhrase": "This ad is loading 1 kb within the subload phase. The maximum allowed is 300 kb.",
"result": "pass",
"attributes": [
{ "name": "Display_RemoteLoadSubload", "description": "Subload bytes (post exclusions)", "value": 906 }
]
}
```
**Attributes:** `Display_RemoteLoadSubload`
---
### Test_Display_VisualStart
Measures how quickly the creative renders visible content. Slow-loading ads get scrolled past before they are seen, directly impacting viewability metrics and campaign performance.
| Result | Condition |
|--------|-----------|
| **pass** | Renders within the threshold (e.g., 0.8 seconds) |
| **fail** | Takes longer than the threshold |
Example response:
```json
{
"name": "Test_Display_VisualStart",
"value": "664",
"valueFormatted": "0.66 seconds",
"valuePhrase": "The median visual start time was 0.66 seconds. Ads must render something visible within 0.8 seconds.",
"result": "pass",
"attributes": [
{ "name": "Display_VisualStartTime", "description": "Time to visual start in milliseconds", "value": 664 }
]
}
```
**Attributes:** `Display_VisualStartTime`
---
## Video tests
This section lists every video test that Advalidation can run against a creative. Each test checks one aspect of the video or VAST tag and returns `pass` or `fail`.
### Mutually exclusive tests
Some video tests come in pairs -- the ad specification determines which variant is active. You will only see one from each pair in a scan response.
| Either | Or | Setting |
|--------|----|---------|
| `Test_Video_Resolution` | `Test_Video_AspectRatio` | Dimensions: exact resolution vs. aspect ratio |
| `Test_Video_TotalBitrate` | `Test_Video_AudioBitrate` + `Test_Video_VideoBitrate` | Bitrate: total container vs. separate audio/video tracks |
### Video file tests
These tests check the properties of video media files. They appear on hosted video scans and VAST child media file scans.
#### Test_Video_AspectRatio
Checks if the video's aspect ratio matches one of the accepted ratios defined in the ad specification.
| Result | Condition |
|--------|-----------|
| **pass** | Aspect ratio matches an accepted value |
| **fail** | Aspect ratio does not match any accepted value |
Example response:
```json
{
"name": "Test_Video_AspectRatio",
"value": "16:9",
"valueFormatted": "16:9 (640x360)",
"valuePhrase": "The aspect ratio is 16:9. The resolution is 640x360. Supported aspect ratio is: 4:3.",
"result": "fail",
"attributes": [
{
"name": "Video_AspectRatioString",
"description": "Aspect ratio",
"value": "16:9"
}
]
}
```
> Mutually exclusive with `Test_Video_Resolution`. The ad specification determines whether dimensions are checked as aspect ratio or exact resolution.
**Attributes:** `Video_AspectRatioString`
---
#### Test_Video_AudioAverage
Checks if the average audio loudness is within the allowed range. Uses the R128/LKFS loudness standard.
| Result | Condition |
|--------|-----------|
| **pass** | Average loudness within the specified range |
| **fail** | Average loudness outside the allowed range |
Example response:
```json
{
"name": "Test_Video_AudioAverage",
"value": -23,
"valueFormatted": "-23.00 LUFS",
"valuePhrase": "Average audio volume is within allowed limits: -23.00 LUFS. Audio average must be within -23 +/- 1 LUFS.",
"result": "pass",
"attributes": [
{
"name": "Audio_VolumeAverage",
"description": "Audio R128 volume average",
"value": -23
}
]
}
```
**Attributes:** `Audio_VolumeAverage`
---
#### Test_Video_AudioBitrate
Checks if the audio track bitrate is within the allowed range.
| Result | Condition |
|--------|-----------|
| **pass** | Audio bitrate within the specified range |
| **fail** | Audio bitrate outside the allowed range |
Example response:
```json
{
"name": "Test_Video_AudioBitrate",
"value": 128,
"valueFormatted": "128 kbps",
"valuePhrase": "The bitrate of the audio track is 128 kbps. The audio bitrate must be between 192 kbps and 192 kbps.",
"result": "fail",
"attributes": [
{
"name": "Audio_BitRate",
"description": "Audio track bit rate",
"value": 128
}
]
}
```
> Mutually exclusive with `Test_Video_TotalBitrate`. The ad specification determines whether bitrate is checked as separate audio/video tracks or as a total container bitrate.
**Attributes:** `Audio_BitRate`
---
#### Test_Video_AudioChannels
Checks the number of audio channels in the video file.
| Result | Condition |
|--------|-----------|
| **pass** | Channel count matches an accepted value |
| **fail** | Channel count does not match any accepted value |
Example response:
```json
{
"name": "Test_Video_AudioChannels",
"value": 2,
"valueFormatted": "2 channels",
"valuePhrase": "The number of audio channels are 2. Number of audio channels supported are: 0.",
"result": "fail",
"attributes": [
{
"name": "Audio_Channels",
"description": "Audio channels",
"value": true
}
]
}
```
> Audio channel requirements are configured in the Advalidation web interface, not through the API.
**Attributes:** `Audio_Channels`
---
#### Test_Video_AudioCodec
Checks if the audio track codec matches one of the accepted codecs.
| Result | Condition |
|--------|-----------|
| **pass** | Audio codec matches an accepted value |
| **fail** | Audio codec does not match any accepted value |
Example response:
```json
{
"name": "Test_Video_AudioCodec",
"value": "AAC",
"valueFormatted": "AAC",
"valuePhrase": "The audio codec is AAC.",
"result": "pass",
"attributes": [
{
"name": "Audio_Codec",
"description": "Audio track codec",
"value": "AAC"
}
]
}
```
**Attributes:** `Audio_Codec`
---
#### Test_Video_AudioPeak
Checks if the true peak audio level is within the allowed maximum.
| Result | Condition |
|--------|-----------|
| **pass** | Peak level within the allowed maximum |
| **fail** | Peak level exceeds the allowed maximum |
Example response:
```json
{
"name": "Test_Video_AudioPeak",
"value": -11.8,
"valueFormatted": "-11.80 dBTP",
"valuePhrase": "The peak audio volume is -11.80 dBTP. The allowed true-peak maximum is -1 dBTP.",
"result": "pass",
"attributes": [
{
"name": "Audio_VolumePeak",
"description": "Audio volume peak",
"value": -11.8
}
]
}
```
**Attributes:** `Audio_VolumePeak`
---
#### Test_Video_AudioSampleRate
Checks if the audio sample rate matches one of the accepted values.
| Result | Condition |
|--------|-----------|
| **pass** | Sample rate matches an accepted value |
| **fail** | Sample rate does not match any accepted value |
Example response:
```json
{
"name": "Test_Video_AudioSampleRate",
"value": 48,
"valueFormatted": "48 kHz",
"valuePhrase": "The audio sample rate is 48 kHz.",
"result": "pass",
"attributes": [
{
"name": "Audio_SamplingRate",
"description": "Audio sampling rate",
"value": 48
}
]
}
```
**Attributes:** `Audio_SamplingRate`
---
#### Test_Video_Boxing
Detects letterboxing (black bars top/bottom) or pillarboxing (black bars left/right) in the video. Boxing indicates the video content doesn't fill the declared resolution.
| Result | Condition |
|--------|-----------|
| **pass** | No boxing detected |
| **fail** | Letterboxing or pillarboxing detected |
Example response:
```json
{
"name": "Test_Video_Boxing",
"value": 0,
"valueFormatted": "None detected",
"valuePhrase": "None detected. Letterboxing not allowed.",
"result": "pass",
"attributes": []
}
```
---
#### Test_Video_ChromaSubsampling
Checks if the chroma subsampling format matches one of the accepted values.
| Result | Condition |
|--------|-----------|
| **pass** | Chroma subsampling matches an accepted value |
| **fail** | Chroma subsampling does not match any accepted value |
Example response:
```json
{
"name": "Test_Video_ChromaSubsampling",
"value": "4:2:0",
"valueFormatted": "4:2:0",
"valuePhrase": "Chroma subsampling is 4:2:0. Supported options are: 4:4:4, 4:2:2, 4:2:0 and 4:1:1.",
"result": "pass",
"attributes": [
{
"name": "Video_ChromaSubsampling",
"description": "Video chroma subsampling",
"value": "4:2:0"
}
]
}
```
**Attributes:** `Video_ChromaSubsampling`
---
#### Test_Video_Codec
Checks if the video codec matches one of the accepted codecs.
| Result | Condition |
|--------|-----------|
| **pass** | Video codec matches an accepted value |
| **fail** | Video codec does not match any accepted value |
Example response:
```json
{
"name": "Test_Video_Codec",
"value": "H264",
"valueFormatted": "H264",
"valuePhrase": "The video codec is H264.",
"result": "pass",
"attributes": [
{
"name": "Video_Codec",
"description": "Video codec",
"value": "H264"
}
]
}
```
**Attributes:** `Video_Codec`
---
#### Test_Video_ContainerFormat
Checks if the video container format matches one of the accepted formats.
| Result | Condition |
|--------|-----------|
| **pass** | Container format matches an accepted value |
| **fail** | Container format does not match any accepted value |
Example response:
```json
{
"name": "Test_Video_ContainerFormat",
"value": "MP4",
"valueFormatted": "MP4",
"valuePhrase": "The container format is MP4.",
"result": "pass",
"attributes": [
{
"name": "Video_ContainerFormat",
"description": "Video container format",
"value": "MP4"
}
]
}
```
**Attributes:** `Video_ContainerFormat`
---
#### Test_Video_DuplicateFrames
Detects repeated duplicate frames in the video, which can indicate encoding issues or artificially inflated duration.
| Result | Condition |
|--------|-----------|
| **pass** | No repeated duplicate frames detected |
| **fail** | Repeated duplicate frames detected |
Example response:
```json
{
"name": "Test_Video_DuplicateFrames",
"value": 0,
"valueFormatted": "Not detected",
"valuePhrase": "Repeated duplicate frames not detected. Repeated duplicated frames not allowed.",
"result": "pass",
"attributes": []
}
```
> Duplicate frame detection is configured in the Advalidation web interface, not through the API.
---
#### Test_Video_Duration
Checks if the video duration is within the allowed range. Reports both video track and audio track durations.
| Result | Condition |
|--------|-----------|
| **pass** | Duration within the allowed range |
| **fail** | Duration outside the allowed range |
Example response:
```json
{
"name": "Test_Video_Duration",
"value": "30.03",
"valueFormatted": "30 seconds",
"valuePhrase": "Video track duration is 30 seconds (30.03). Audio track duration is 30 seconds (30.03).",
"result": "pass",
"attributes": [
{
"name": "Video_PlayTime",
"description": "Duration of video in milliseconds",
"value": 30.03
}
]
}
```
**Attributes:** `Video_PlayTime`
---
#### Test_Video_Filesize
Checks if the video file size is within the allowed range.
| Result | Condition |
|--------|-----------|
| **pass** | File size within the allowed range |
| **fail** | File size outside the allowed range |
Example response:
```json
{
"name": "Test_Video_Filesize",
"value": 1258496,
"valueFormatted": "1229 kb",
"valuePhrase": "The size of this video file is 1229 kb. The filesize must be above 500 kB.",
"result": "pass",
"attributes": [
{
"name": "File_Length",
"description": "Video file size",
"value": 1258496
}
]
}
```
**Attributes:** `File_Length`
---
#### Test_Video_Fps
Checks if the video frame rate matches one of the accepted values.
| Result | Condition |
|--------|-----------|
| **pass** | Frame rate matches an accepted value |
| **fail** | Frame rate does not match any accepted value |
Example response:
```json
{
"name": "Test_Video_Fps",
"value": 23.976,
"valueFormatted": "23.976 FPS",
"valuePhrase": "The framerate is 23.976 FPS.",
"result": "pass",
"attributes": [
{
"name": "Video_FrameRate",
"description": "Frame rate",
"value": 23.976
}
]
}
```
**Attributes:** `Video_FrameRate`
---
#### Test_Video_Interlace
Detects interlaced frames in the video by analyzing the interlace frame ratio. Interlaced content causes visual artifacts on progressive displays.
| Result | Condition |
|--------|-----------|
| **pass** | Interlace ratio within allowed limits |
| **fail** | Interlace ratio exceeds the threshold |
Example response:
```json
{
"name": "Test_Video_Interlace",
"value": 29,
"valueFormatted": "29%",
"valuePhrase": "The interlace frame ratio is 29%. Video must not be interlaced.",
"result": "fail",
"attributes": []
}
```
> Interlace detection is configured in the Advalidation web interface, not through the API.
---
#### Test_Video_Resolution
Checks if the video resolution matches one of the accepted resolutions defined in the ad specification.
| Result | Condition |
|--------|-----------|
| **pass** | Resolution matches an accepted value |
| **fail** | Resolution does not match any accepted value |
Example response:
```json
{
"name": "Test_Video_Resolution",
"value": "640x360",
"valueFormatted": "640x360 (16:9)",
"valuePhrase": "The resolution is 640x360. The aspect ratio is 16:9. Supported resolutions are: 640x480, 800x600 and 1920x1080.",
"result": "fail",
"attributes": [
{
"name": "Video_Height",
"description": "Video pixel height",
"value": 360
},
{
"name": "Video_Width",
"description": "Video pixel width",
"value": 640
}
]
}
```
> Mutually exclusive with `Test_Video_AspectRatio`. The ad specification determines whether dimensions are checked as exact resolution or aspect ratio.
**Attributes:** `Video_Height`, `Video_Width`
---
#### Test_Video_ScanType
Checks if the video scan type (progressive or interlaced) matches one of the accepted values.
| Result | Condition |
|--------|-----------|
| **pass** | Scan type matches an accepted value |
| **fail** | Scan type does not match any accepted value |
Example response:
```json
{
"name": "Test_Video_ScanType",
"value": "progressive",
"valueFormatted": "progressive",
"valuePhrase": "Scan type of the video is progressive. Supported scan types are: interlaced and progressive.",
"result": "pass",
"attributes": [
{
"name": "Video_ScanType",
"description": "Scan type (progressive/interlaced)",
"value": "progressive"
}
]
}
```
**Attributes:** `Video_ScanType`
---
#### Test_Video_Timecode
Checks if the video contains timecode information and whether it matches accepted formats.
| Result | Condition |
|--------|-----------|
| **pass** | Timecode matches an accepted format, or any format is accepted |
| **fail** | Timecode does not match any accepted format |
Example response:
```json
{
"name": "Test_Video_Timecode",
"value": null,
"valueFormatted": "Missing",
"valuePhrase": "This video contains no timecode information. Any timecode combination is supported..",
"result": "pass",
"attributes": []
}
```
---
#### Test_Video_TotalBitrate
Checks if the total container bitrate (audio + video combined) is within the allowed range.
| Result | Condition |
|--------|-----------|
| **pass** | Total bitrate within the allowed range |
| **fail** | Total bitrate outside the allowed range |
Example response:
```json
{
"name": "Test_Video_TotalBitrate",
"value": 913,
"valueFormatted": "913 kbps",
"valuePhrase": "The total bitrate is 913 kbps. The total bitrate must be between 100 kbps and 2000 kbps.",
"result": "pass",
"attributes": [
{
"name": "General_BitRate",
"description": "Total (container) bit rate",
"value": 913
}
]
}
```
> Mutually exclusive with `Test_Video_AudioBitrate` and `Test_Video_VideoBitrate`. The ad specification determines whether bitrate is checked as a total container value or as separate audio/video tracks.
**Attributes:** `General_BitRate`
---
#### Test_Video_VastPropertyDiscrepancy
Compares properties declared in the VAST XML (such as codec, dimensions, and bitrate) against the actual properties of the downloaded video file. Discrepancies between declared and actual values are flagged.
| Result | Condition |
|--------|-----------|
| **pass** | No discrepancies between declared and actual properties |
| **fail** | One or more discrepancies detected |
Example response:
```json
{
"name": "Test_Video_VastPropertyDiscrepancy",
"value": 0,
"valueFormatted": "0 discrepancies",
"valuePhrase": "Detected 0 discrepancies between the XML declaration and the properties of the actual video file. Must align.",
"result": "pass",
"attributes": []
}
```
> This test only appears on VAST child media file scans, not on hosted video scans or VAST parent scans. It requires both VAST XML declarations and actual file properties to compare.
---
#### Test_Video_VideoBitrate
Checks if the video track bitrate is within the allowed range.
| Result | Condition |
|--------|-----------|
| **pass** | Video bitrate within the allowed range |
| **fail** | Video bitrate outside the allowed range |
Example response:
```json
{
"name": "Test_Video_VideoBitrate",
"value": 800,
"valueFormatted": "800 kbps",
"valuePhrase": "The bitrate of the video track is 800 kbps. The video bitrate must be between 1000 kbps and 1500 kbps.",
"result": "fail",
"attributes": [
{
"name": "Video_BitRate",
"description": "Video track bit rate",
"value": 800
}
]
}
```
> Mutually exclusive with `Test_Video_TotalBitrate`. The ad specification determines whether bitrate is checked as separate audio/video tracks or as a total container bitrate.
**Attributes:** `Video_BitRate`
---
### VAST tag tests
These tests validate the VAST XML structure and tag behavior. They appear on VAST parent scans and variation scans.
#### Test_Video_AdVerificationTags
Detects ad verification vendor tags in the VAST response and checks whether they comply with the ad specification policy.
| Result | Condition |
|--------|-----------|
| **pass** | Tags comply with the policy, or no tags detected when none are allowed |
| **fail** | Tags violate the ad specification policy |
Example response:
```json
{
"name": "Test_Video_AdVerificationTags",
"value": null,
"valueFormatted": "None",
"valuePhrase": " Only monitoring tags allowed.",
"result": "pass",
"attributes": []
}
```
---
#### Test_Video_VastApiProperties
Checks for VPAID or other API frameworks in the VAST response and validates against the ad specification's framework policy.
| Result | Condition |
|--------|-----------|
| **pass** | Detected frameworks match the policy, or none detected when none are required |
| **fail** | Framework requirements not met |
Example response:
```json
{
"name": "Test_Video_VastApiProperties",
"value": 0,
"valueFormatted": "VPAID not detected",
"valuePhrase": "Detected no frameworks. Supported framework is: VPAID javascript.",
"result": "pass",
"attributes": []
}
```
---
#### Test_Video_VastConnectionRules
Validates the VAST tag's external connections against an approved vendor list defined in the ad specification. All URLs referenced in the VAST XML are checked.
| Result | Condition |
|--------|-----------|
| **pass** | All connections match approved vendors |
| **fail** | One or more connections violate the rules |
Example response:
```json
{
"name": "Test_Video_VastConnectionRules",
"value": 6,
"valueFormatted": "6 violations",
"valuePhrase": "This ad violates the ad specification rules for external connections.",
"result": "fail",
"attributes": []
}
```
---
#### Test_Video_VastCreativeCount
Counts the number of `` elements in the VAST XML. Multi-creative VAST responses can cause unexpected ad behavior.
| Result | Condition |
|--------|-----------|
| **pass** | Creative count within the allowed limit |
| **fail** | Too many creatives in the VAST response |
Example response:
```json
{
"name": "Test_Video_VastCreativeCount",
"value": 1,
"valueFormatted": 1,
"valuePhrase": "The VAST XML contains 1 creative. Multi creative VAST not allowed.",
"result": "pass",
"attributes": []
}
```
---
#### Test_Video_VastDurationDiscrepancy
Checks whether the duration declared in the VAST XML matches the actual duration of the media files. Discrepancies can cause player issues.
| Result | Condition |
|--------|-----------|
| **pass** | No duration discrepancies detected |
| **fail** | Duration discrepancies found between XML and media files |
Example response:
```json
{
"name": "Test_Video_VastDurationDiscrepancy",
"value": 1,
"valueFormatted": "None",
"valuePhrase": "No duration discrepancies detected. Must not vary.",
"result": "pass",
"attributes": []
}
```
---
#### Test_Video_VastNonHttps
Checks if the VAST XML contains any non-HTTPS URLs. Mixed content can be blocked by browsers and cause tracking failures.
| Result | Condition |
|--------|-----------|
| **pass** | All URLs use HTTPS |
| **fail** | One or more non-HTTPS URLs detected |
Example response:
```json
{
"name": "Test_Video_VastNonHttps",
"value": 0,
"valueFormatted": "No issues",
"valuePhrase": "This ad is making requests to 0 non HTTPS URLs. Only HTTPS allowed.",
"result": "pass",
"attributes": []
}
```
---
#### Test_Video_VastRequiredMediaFiles
Checks if the VAST XML contains all required media file definitions as specified in the ad specification. Media file definitions match on codec, dimensions, and bitrate.
| Result | Condition |
|--------|-----------|
| **pass** | All required media file definitions matched |
| **fail** | One or more required definitions not matched |
Example response:
```json
{
"name": "Test_Video_VastRequiredMediaFiles",
"value": 1,
"valueFormatted": "1 definition not matched",
"valuePhrase": "1 media files definition not matched, all of the definitions required",
"result": "fail",
"attributes": []
}
```
---
#### Test_Video_VastSkipDetection
Checks if the VAST tag includes skip functionality and whether it matches the ad specification's skip policy.
| Result | Condition |
|--------|-----------|
| **pass** | Skip behavior matches the policy |
| **fail** | Skip behavior does not match the policy |
Example response:
```json
{
"name": "Test_Video_VastSkipDetection",
"value": null,
"valueFormatted": "Not skippable",
"valuePhrase": "The VAST skippable property is: not skippable. Ad must not be skippable.",
"result": "pass",
"attributes": []
}
```
---
#### Test_Video_VastValidateXml
Validates the VAST XML against the VAST schema. Catches syntax errors, missing required elements, and structural issues.
| Result | Condition |
|--------|-----------|
| **pass** | VAST XML is valid |
| **fail** | VAST XML contains validation errors |
Example response:
```json
{
"name": "Test_Video_VastValidateXml",
"value": 1,
"valueFormatted": "Valid",
"valuePhrase": "The XML of this VAST file is valid. The VAST XML syntax must be valid.",
"result": "pass",
"attributes": []
}
```
---
#### Test_Video_VastVariations
Detects whether a VAST URL returns different content on subsequent loads. Variations indicate the tag serves rotating or dynamic content.
| Result | Condition |
|--------|-----------|
| **pass** | Variation count within the allowed limit |
| **fail** | Variation count exceeds the allowed limit |
Example response:
```json
{
"name": "Test_Video_VastVariations",
"value": 1,
"valueFormatted": 1,
"valuePhrase": "The VAST URL returns 1 variation. Varying VAST responses not allowed.",
"result": "pass",
"attributes": []
}
```
> When multiple variations are detected, the creative's `sourceType` becomes `video-url-varying-vast` and the `nbVariations` field appears on the creative object. This field is only present on varying VAST creatives -- it is absent from all other source types.
---
#### Test_Video_VastVersion
Checks if the VAST version matches one of the accepted versions.
| Result | Condition |
|--------|-----------|
| **pass** | VAST version matches an accepted value |
| **fail** | VAST version does not match any accepted value |
Example response:
```json
{
"name": "Test_Video_VastVersion",
"value": "2.0",
"valueFormatted": "VAST 2.0",
"valuePhrase": "The VAST version is: 2.0.",
"result": "fail",
"attributes": [
{
"name": "Video_VASTVersion",
"description": "VAST version",
"value": "2.0"
}
]
}
```
**Attributes:** `Video_VASTVersion`