Skip to content

Reporting exposure

Report a playback event by firing the URL the server stamped on the ad, appending the fire-time facts. There’s no need to build the URL from scratch; the player fires what the server handed it. The /v1alpha2/track path is illustrative; the real host, path, and query params live inside each <Impression> and <Tracking> element of the VAST.

Fire each <Impression> and <Tracking> the ad’s VAST contains, at the lifecycle moment its event name denotes. Don’t hardcode a fixed set; the server controls which events are present. If it stamps more later, the player’s loop fires them with no code change.

Lifecycle moment Event Stamped today
Creative first renders ae=impression, ce=creativeView, ce=start impression, creativeView, start
25% / 50% / 75% of video playback ce=firstQuartile, ce=midpoint, ce=thirdQuartile video only
Play completes (end / Duration) ce=complete complete

The VAST stamps impression, creativeView, start, and complete on every creative; video also carries the three quartiles. Image and HTML5 have no timeline, so they omit the quartiles and fire impression/start on first render and complete when their <Duration> elapses (see Creative formats).

The player does not need to worry about how these become billable impressions; that reconciliation happens server-side. Fire what was stamped, at its moment.

Every play fires afresh, including repeated offline plays. The stamped URLs are reusable, not single-use. Each time a stored ad plays again (offline rotation, no new /ads request), fire its impression (and start, complete) again with a fresh ets. One /ads response can yield many impressions. Each play is a distinct impression, keyed by ets (occurrence time). The server does not derive plays from the token alone, so a player that fires once per token under-counts every replay.

Each <Ad> also carries an <Error> URL. If the player cannot play the ad, fire this URL one time. For example, the creative file is missing or times out. Do not fire the <Impression> or <Tracking> URLs of that ad.

Replace the [ERRORCODE] macro in the URL with the VAST error code. Use 401 for a file that is not found and 402 for a timeout. You do not need to append ets. In a batch entry, send the code as error_code.

The server counts the codes in the two tables below. It counts any other code as other. The codes and their meanings come from section 2.3.6.3 (VAST Error Codes Table) of the IAB Tech Lab VAST 4.2 specification. The text here is shorter than the specification.

Players use these codes most:

Code Meaning
400 General linear error. The player cannot show the linear ad.
401 The player cannot find the media file at its URI.
402 The media file URI timed out.
403 The player supports none of the media files in the response.
405 The player found a supported media file but cannot show it. For example, the codec is not supported.
900 Undefined error. Use it if no other code applies.

These are the other codes that the server counts:

Code Meaning
100 XML parsing error.
101 VAST schema validation error.
102 The player does not support the VAST version of the response.
200 Trafficking error. The player received an ad type that it does not expect or cannot play.
201 The player expects a different linearity.
202 The player expects a different duration.
203 The player expects a different size.
204 The ad category is required but is not given.
205 The category of the inline ad is blocked by the BlockedAdCategories of the Wrapper.
206 The ad break is shortened. The ad is not served.
300 General Wrapper error.
301 The VAST URI in a Wrapper timed out or is not available.
302 The Wrapper limit is reached. Too many Wrappers were received and no InLine response.
303 No VAST response after one or more Wrappers.
304 The InLine ad did not show within the time limit.
406 A mezzanine file is required but is not given. The ad is not served.
407 The mezzanine file is downloading for the first time. The ad is not served until the download and transcode are complete.
408 Conditional ad rejected. This code is deprecated.
409 The interactive unit in the InteractiveCreativeFile node did not run.
410 The verification unit in the Verification node did not run.
411 The mezzanine file does not meet the required specification. The ad is not served.
500 General NonLinearAds error.
501 The NonLinear creative does not fit the display area.
502 The player cannot fetch the NonLinear resource.
503 The player found no NonLinear resource of a supported type.
600 General CompanionAds error.
601 The Companion creative does not fit the display area.
602 The player cannot show a required Companion.
603 The player cannot fetch the Companion resource.
604 The player found no Companion resource of a supported type.
901 General VPAID error.
902 General InteractiveCreativeFile error.

An error is never a play. We do not bill it and we do not count it as delivery.

Param Set by Present Notes
asid server (stamped) always The ad serving id (also emitted as <AdServingId>). Fire verbatim.
ae / ce server (stamped) always Which event fired.
sts server (stamped) always Server clock at decision time. Opaque; fire verbatim. Anchors drift correction.
ets the player (appends) every fire RFC3339 occurrence time. Not needed on an error.
error_code the player (macro) errors VAST error code. Replaces [ERRORCODE].

dvactid is not sent on fires. The player provides it on /ads, where it is captured and attributed to that ad’s events automatically.

Event markers. The server stamps exactly one per URL:

Marker Values
ae impression
ce creativeView, start, firstQuartile, midpoint, thirdQuartile, complete, error

Fire each URL as an HTTP GET. A well-formed fire returns 200 OK with an empty body, meaning “accepted for processing.” The server resolves and records it asynchronously, so a 200 is not a guarantee the event was stored (an expired or unknown token is accepted, then dropped server-side). Don’t gate playback on the response.

A 400 means the request was malformed (missing asid, missing ets on a fire that is not an error, or an unrecognized event marker): the URL was relayed wrong, so log it and drop. Treat 5xx and 429 as transient and retry with backoff. A 429 means the edge rate-limited your IP.

Buffer the whole fire (the stamped URL plus the ets the player would have appended); nothing about it changes between buffering and replay. Play replays and retries are safe: the server derives each play’s identity from the ad, the marker and the ets, so a play that landed is counted once however often it arrives.

Play a stored ad; append &ets=2026-06-05T09:15:18Z to each stamped URL.
If offline, buffer the whole fire; each fire keeps its own ets.
On reconnect, drain the buffer — in bulk (below) or one GET at a time.

A day of offline playout is thousands of fires. Sending each as its own GET works, but a whole site reconnecting at once turns that into a stampede. POST /v1alpha2/track:batch takes many buffered fires in one request:

POST /v1alpha2/track:batch
Content-Type: application/json
{
"fires": [
{"asid": "01M1P240J9...", "ae": "impression", "sts": "2026-06-05T09:15:00Z", "ets": "2026-06-05T09:15:18Z"},
{"asid": "01M1P240J9...", "ce": "start", "sts": "2026-06-05T09:15:00Z", "ets": "2026-06-05T09:15:18Z"},
{"asid": "01M1P240J9...", "ce": "complete", "sts": "2026-06-05T09:15:00Z", "ets": "2026-06-05T09:15:28Z"}
]
}

This is for the offline drain only. While online, keep firing each stamped URL individually as it happens — that is what VAST asks for, and an <Impression> is a URI, not a batch.

Each entry is one fire, and means exactly what the same fire means as a GET. Build it by splitting the buffered URL’s query string into an object of its parameters — the parameter name is the field name:

https://<host>/v1alpha2/track?asid=01M1P240J9...&ce=start&sts=2026-06-05T09:15:00Z&ets=2026-06-05T09:15:18Z
└──────────────────────────── becomes ────────────────────────────┘
{"asid": "01M1P240J9...", "ce": "start", "sts": "2026-06-05T09:15:00Z", "ets": "2026-06-05T09:15:18Z"}

Copy every parameter through, including any the player does not recognize; the server may add one, and dropping it silently loses what it carried. Never rename, reformat, drop or compute a parameter — the same rule as relaying a URL verbatim.

Order does not matter: the server groups the batch itself. Send a flat list and leave the buffer’s own structure alone.

Fires per request 250
Over the limit 400 — the whole request is refused, nothing is processed

A batch is never partly truncated, so a 200 always accounts for every entry you sent. Note that browsers cap a fetch(keepalive) body at 64 KiB, which is roughly where 250 entries land — a reason to keep to the limit rather than negotiate it.

{
"accepted": 2,
"rejected": [
{"index": 1, "reason": "invalid: unknown creative event"}
]
}
  • accepted — fires handed to the pipeline. Like a single fire’s 200, this is not a delivery receipt: an expired asid is accepted here and dropped afterwards.
  • rejected — entries that were malformed, by their position in fires. These are the same problems a GET would answer 400 to, so log and drop them; never retry one. reason is for your log, not for branching on.

Everything not listed in rejected is counted in accepted.

Response Action
200 Clear the whole batch from the buffer. Log anything in rejected and drop those too.
4xx, but not 429 The batch itself was malformed and will never succeed. Drop it and log.
429 Transient. The edge rate-limited your IP. Treat it like 5xx.
5xx, timeout, network error Transient. Leave the whole batch queued and resend it on the next reconnect.

Resending the whole batch after a failure is the intended behaviour, and it is safe. A lost response is indistinguishable from a request that never arrived, so a player cannot know which entries landed — and does not need to, because a replayed play is counted once.

For the operational concerns around draining a long offline buffer (clock drift, late events, reconnect stampede, and buffer bounds), see Operating offline.