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.
Firing
Section titled “Firing”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.
When an ad cannot play
Section titled “When an ad cannot play”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.
Request parameters
Section titled “Request parameters”| 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 |
Response and retries
Section titled “Response and retries”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.
Offline buffering
Section titled “Offline buffering”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.Draining a buffer in bulk
Section titled “Draining a buffer in bulk”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:batchContent-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.
Building an entry
Section titled “Building an entry”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.
Limits
Section titled “Limits”| 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.
Response
Section titled “Response”{ "accepted": 2, "rejected": [ {"index": 1, "reason": "invalid: unknown creative event"} ]}accepted— fires handed to the pipeline. Like a single fire’s200, this is not a delivery receipt: an expiredasidis accepted here and dropped afterwards.rejected— entries that were malformed, by their position infires. These are the same problems a GET would answer400to, so log and drop them; never retry one.reasonis for your log, not for branching on.
Everything not listed in rejected is counted in accepted.
What to do with each outcome
Section titled “What to do with each outcome”| 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.