Skip to main content

WAI Extension: Ad Presentation

Mirrored from the canonical text at commit b1681667 ().

Status: Draft. Registers the record wai.live.ad_presentation: a player’s signed claim that it presented one impression of a live-ads break, with what it measured while it did. The claim, WPC1, names the impression by the break, the session and the slot of a wai.video.ssai receipt of the break form; it lists the creative’s media objects the player presented, by content hash, and spans of the creative’s media time with the fraction of pixels in view and whether the ad was audible; and it may carry a device attestation, a Privacy Pass token (RFC 9577) whose challenge binds it to this one impression. It is signed by the playback session’s own key, from which the session the receipt names is derived. An ad policy of version 2 can require a claim for each impression, a viewability rule, an audible run and an attestation; then a receipt’s spend counts only the impressions whose claim verifies, and the room verifier holds that spend to the decider grant’s ceiling. No new transport. The reference implementation is the presentation module of wai-rs’s live_ads (feature live_ads); the corpus is live-conformance/presentation/, and its second implementation is live-conformance/ad_presentation_verify.py. Keywords MUST, MUST NOT, SHOULD, MAY are RFC 2119/8174.

Texts read for this revision, each at its source:

  • Viewable Ad Impression Measurement Guidelines (Desktop), Version 2.0, 18 August 2015, in full. Its video requirements are word for word those of its Version 1.0, 30 June 2014. It numbers no section, so it is cited by heading and printed page.
  • Mobile Viewable Ad Impression Measurement Guidelines, 28 June 2016: its video section only (pp. 10–11).
  • Cross-Media Audience Measurement Standards (Phase I Video), September 2019: its executive summary and §2.1.1, §2.1.5, §2.2.1, §2.3.1 and §4.1.1, which hold all of its viewability, duration and audio text. The rest of it was not read.
  • Server-Side Ad Insertion and OTT Guidance, August 2021, in full.
  • Digital Video Impression Measurement Guidelines, Version 1.1, June 2018: §3.0, §4.1, §4.4 and §4.5.
  • Video Ad Serving Template (VAST) 4.3, December 2022: §1.1 to §1.2.1, §2.3.4, §2.4 to §2.4.3, §3.4.4, §3.5 to §3.5.3, §3.14 to §3.14.2, §3.16 to §3.17.5 and §6. Its other sections were not read.
  • Secure Interactive Media Interface Definition (SIMID) 1.2: §1 to §3.5.1, §4.2.10, §4.2.11, §4.4.5, §4.4.7, §8.1 and §8.4.
  • Open Measurement SDK – OMID API, Version 1.5, June 2024: its sections on the impression, the media events, volumeChange, playerStateChange, geometryChange, the ad view and its reasons.
  • Open Measurement Device Attestation Implementation Guidance, Version 1.0, November 2025, in full. Not read: the native SDKs that implement it, and any text of the exchange between a player and a verifier’s URL; no public text of that exchange was found.
  • RFC 9576, RFC 9577 and RFC 9578 (June 2024), in full, and RFC 9474 §5. RFC 8017’s RSASSA-PSS is used as RFC 9578 §6.4 cites it, and this revision’s implementation of it is checked against the token of RFC 9578 Appendix A.2’s first test vector.

A section number of one of these texts is that revision’s.

1. Scope

In scope: the session key and the session it names (§3); the creative digest (§4); the presentation claim, its spans, its signature and its rules (§5); the viewability rules, the qualifying run and the quartile a claim reaches (§6); the device attestation a claim carries and how it is bound and verified (§7); the presentation fields of an ad policy (§8); each claim’s verdict, each impression’s count and the spend that counts (§9); and what a player measures and how (§10).

Out of scope: a transport, and how a claim reaches the decider or a buyer; how a player obtains a token from an attester and its issuer (the issuance protocols of RFC 9578, run by a platform); the detection of invalid traffic, which stays statistical and the measurer’s (§13); attention, and anything a sensor of the viewer would measure; and the stitched form of wai.video.ssai, whose receipts name no session of a room.

Relationship to the core spec: wai.live.ad_presentation is a record (SPEC §5): a signed record a WAI tool emits and verifies. It names no payload format and is not dispatchable.

Relationship to live-ads: this extension adds the presentation fields of an ad policy of version 2 (its §3.1, here §8), the session a session key names (its §2, here §3), and the spend its §9.3 step 12 counts under a policy that requires claims (here §9.3). Every other rule of live-ads stands, and a policy of version 1 is verified as before.

Relationship to jwp-receipts: a reconstruct-session claim (its §2.4.12) is a sink’s record of a title it reconstructed, signed by its decoder host’s key, and “not evidence that a person watched”. A presentation claim is a player’s record of one ad impression, made for the billing rule of §9; it states what the player measured and is no more evidence that a person watched (§13).

2. Terms

Integers of the records here are little-endian unless this text says big-endian, as live-ads §2 has them; a digest is SHA-256 (FIPS 180-4), 32 bytes. Ed25519 is RFC 8032’s, verified strictly (SPEC §7 Signatures): a key that fails step 1 or 2 is bad_key, and a signature that fails a later step is bad_signature. A long field is a u16 length and that many bytes.

3. The session key

Under a policy that requires claims (§8), a player MUST make a fresh Ed25519 key for each playback session of a room, from its platform’s random generator, and MUST NOT use it for another session or another room, or derive it from an identifier of the viewer, the device or an account. It presents the key’s public part with each asset-list request of the session (live-ads §7), by a means this revision does not fix.

The session a session key names in a room is the first 16 bytes of SHA-256 over wai:ad-presentation-session (ASCII) and 0x01, the room id (32 bytes) and the key (32 bytes). Under such a policy the decider MUST give each receipt of the session that session (live-ads §8.2 session), and a claim is counted only when its key names its session (§9.2 step 4). A player computes the session itself; the decider never chooses it.

The session is a function of a key made for this room alone, so it links nothing across rooms; live-ads §12’s rule that a session not be derived from a viewer’s key is met, since the session key is not one.

4. The creative digest

A creative is its media objects in presentation order: each object’s content hash, SHA-256 of the object’s bytes as served, and its presentation duration in milliseconds (a u32, at least 1). The object at index j holds the creative’s media time from the sum of the durations before it, s, to s + its duration.

The creative digest is SHA-256 over wai:ad-creative (ASCII) and 0x01, the objects’ count (u32), and for each object in order its content hash and its duration (u32).

Under a policy that requires claims, an ad’s creative_hash in the pod, in the receipt and in the asset list’s URIs (live-ads §7.2, §7.3) is its creative digest, and its duration_ms the sum of its objects’ durations. A creative’s URI then names the very bytes a player presents, object by object.

5. The presentation claim

5.1 Layout

offsetsizefieldmeaning
04magicWPC1 (ASCII)
41version1 in this revision
532roomthe room id
3732cuethe break’s digest (live-ads §4.4)
6916sessionthe session (§3)
851slotthe impression’s slot, 0 to 15
8632creative_hashthe creative presented: its creative digest (§4)
11832playerthe session key (§3), which signs the claim
1501geometrywhat the visible fractions are measured over: 1 ad, the ad’s own pixels, measured; 2 screen, a full-screen output to a television, whose pixels are assumed in view; 3 player, the player’s area, measured, where the ad’s own pixels are not known
1514framesu32: the frames rendered over the spans, where the platform exposes a count; 4 294 967 295 where it does not
1552nu16: the presented objects’ count
15732 × nobjectsthe content hashes (§4) of the creative’s objects the spans touch, in order (§9.2 step 6)
…2mu16: the spans’ count
…11 × mspans§5.2
…longattestationa token (§7.1), or no bytes
…64sig§5.3

5.2 Spans

A span is 11 bytes:

offsetsizefieldmeaning
04start_msu32: where it starts, in the creative’s media time
44length_msu32: its length, at least 1
82visible_bpu16: the least fraction of the pixels in view the player observed over it, in basis points (§10)
101flagsbit 0 audible: the ad was audible throughout it; bit 1 resumed: playback stopped between the span before it and this one

A span’s end is start_ms + length_ms, computed without overflow. The spans are in presentation order, and no media time is in two of them: a player that presents a stretch again states it once. A span whose start is after the end of the span before it follows a skip, and is a resumption.

5.3 Digest and signature

The digest of a claim is SHA-256 over its bytes from magic through attestation. The signature is Ed25519 by player over wai:ad-presentation-claim (ASCII) and 0x01, followed by the bytes the digest covers.

5.4 Reading and rules

A reader reads exactly the bytes it is given as one claim, in the order of §5.1, and refuses it claim_malformed when:

  1. its magic is not WPC1 or its version not 1;
  2. the bytes end before a field the layout requires, or there are bytes after sig;
  3. slot is above 15, or geometry is not 1, 2 or 3;
  4. n is 0 and m is not, or m is 0 and n is not;
  5. a span’s length_ms is 0, its visible_bp above 10 000 or its flags above 3;
  6. the first span has resumed set; or a later span starts before the end of the span before it, or after that end without resumed set.

6. Viewability and the quartile

6.1 The rules

A policy names one of two viewability rules (§8). Both require two continuous seconds; they differ in the pixels.

viewabilityrulea span qualifies whenadmits geometrytext
1videovisible_bp is at least 5 0001, 3Viewable Ad Impression Measurement Guidelines (Desktop) 2.0, “Requirements for Viewable Video Advertising Impressions” (p. 8); the 2016 mobile guidelines’ video section states the same time
2cross-mediavisible_bp is 10 0001, 2, 3Cross-Media Audience Measurement Standards (Phase I Video) §2.2.1 and §4.1.1

and, under a policy whose presentation sets audible, the span is audible too. A span of a geometry the rule does not admit never qualifies.

6.2 The qualifying run

Over a claim’s spans in order, a span continues the one before it when it starts at that span’s end and does not have resumed set. A run is a sequence of qualifying spans each of which, after the first, continues the one before it; its length is the sum of their length_ms. A claim’s run is the length of its longest run, and its viewable time the sum of length_ms over its qualifying spans. A claim is viewable when its run is at least 2 000 ms.

A pause, a stall or a skip between two spans ends a run, so two stretches of 1.5 s in view are not two continuous seconds. That spans state no media time twice is how a run holds only “unduplicated content”.

6.3 The quartile

The coverage of a claim’s spans is 0 when the first does not start at 0; otherwise it is the end of the longest prefix of the spans in which each starts at the end of the one before (a pause does not end it, a skip does). With D the impression’s duration_ms and C the coverage, the quartile a claim reaches is:

These are quartiles_reached’s values (live-ads §8.2): 1 started, then a quarter, a half, three quarters and all of it, at the marks VAST 4.3 §3.14.1 names for firstQuartile, midpoint, thirdQuartile and complete (“The creative played continuously for at least 25% of the total duration at normal speed”, and so on). A claim with a span has begun to play, which is when an impression is counted (Digital Video Impression Measurement Guidelines 1.1 §4.1: “begins to play”).

7. Device attestation

7.1 The token

A claim’s attestation is a token of RFC 9577 §2.2.1:

struct {
    uint16_t token_type;
    uint8_t nonce[32];
    uint8_t challenge_digest[32];
    uint8_t token_key_id[Nid];
    uint8_t authenticator[Nk];
} Token;

in its TLS presentation, big-endian. A policy names one of RFC 9578’s two token types, each of Nid 32: 0x0001, VOPRF(P-384, SHA-384), privately verifiable, Nk 48 (§5, §8.2.1 there); and 0x0002, Blind RSA (2048-bit), publicly verifiable, Nk 256 (§6, §8.2.2 there). A token reads when it is exactly 98 + Nk bytes for the type its first two bytes name, and that type is one of the two.

A token attests what its issuer’s attester checked of the device that asked for it (RFC 9576 §3): that a device of a kind the attester vouches for asked. It attests nothing of the rendering or of what was in view.

7.2 The challenge

The TokenChallenge a claim’s token answers is RFC 9577 §2.1.1’s:

struct {
    uint16_t token_type;
    opaque issuer_name<1..2^16-1>;
    opaque redemption_context<0..32>;
    opaque origin_info<0..2^16-1>;
} TokenChallenge;

with token_type and issuer_name the policy’s token_type and issuer (§8), redemption_context the 32-byte redemption context below, and origin_info empty: in its TLS presentation, the type as a big-endian u16, the issuer’s length as a big-endian u16 and its bytes, the byte 32 and the context, and two zero bytes. The redemption context is SHA-256 over wai:ad-presentation-attest (ASCII) and 0x01, room, cue, session, slot (one byte), creative_hash and player, the claim’s own.

The challenge is a function of the impression and the session key alone, each known before the ad plays, so a party that issues it as a Privacy Pass origin (RFC 9577 §2.1) issues these bytes, and every verifier recomputes them. A token answers one impression of one session key and no other: lifted onto another claim, its challenge_digest is not that claim’s. This is the Open Measurement Device Attestation Implementation Guidance’s “Against one impression ID, only one attestation request may be counted” (§5.1), made a property of the token. A TokenChallenge with no origin_info scopes the token to no origin (RFC 9577 §2.1.1.3), which the redemption context makes unnecessary.

7.3 The issuer’s verifier

A token is verified by its issuer’s verifier. A verifier holds, for each issuer it trusts, the issuer’s name, its token type and its keys, from its issuer directory (RFC 9578 §4); a key’s token_key_id is SHA-256 of its encoding (for type 0x0002, the DER SubjectPublicKeyInfo of §6.5 there; for 0x0001, SerializeElement(pkI) of §5.5).

The verifier requires, too, that the token’s challenge_digest is SHA-256 of the TokenChallenge of §7.2: what WAI checks itself. Double spending (RFC 9577 §2.2.2) needs no nonce ledger here: a token answers one impression, and an impression counts once (§9.2 step 5).

8. The policy’s presentation fields

An ad policy of version 2 (live-ads §3.1) carries, after creative_base and before sig:

sizefieldmeaning
1presentationbit 0 claim: an impression counts only when its claim verifies; bit 1 attest: the claim carries a token that verifies (§7); bit 2 audible: a qualifying span is audible too (§6.1)
1viewability0 none; 1 the video rule; 2 the cross-media rule (§6.1)
2token_typeu16: the token type of §7.1 when attest is set, otherwise 0
longissuerthe issuer’s name, the TokenChallenge’s issuer_name (§7.2), when attest is set; otherwise no bytes

A policy of version 2 is refused ad_policy_malformed (live-ads §3.3) when presentation sets a bit above bit 2; sets attest or audible without claim; has viewability above 2, or 0 with claim set, or not 0 with claim clear; or, with attest set, has a token_type other than 1 or 2 or an empty issuer, or with it clear, a token_type other than 0 or an issuer that is not empty. A policy of version 2 with presentation 0 is verified as a policy of version 1.

9. Verification

9.1 Inputs

Under a policy that requires claims, an ads verifier (live-ads §9.1) is given, too:

Under a policy that does not, it reads none of them.

9.2 Each claim

The claims are read once, before the receipts (steps 1 and 2), and the rest of each claim’s steps run when the verifier reaches the receipt whose impression it names, after that receipt’s live-ads §9.3 steps 1 to 11. Each claim gets one verdict, presented or the reason of the first step it fails:

  1. It reads (§5.4): claim_malformed.
  2. Its signature verifies under player (§5.3): bad_key, bad_signature.
  3. Its room, cue and session are a receipt’s, among the receipts that passed steps 1 to 11, and that receipt has an impression of its slot: claim_no_decision. A claim that no such receipt names when the verifier has read every receipt gets this verdict then.
  4. Its player names its session (§3): claim_key_unbound.
  5. No claim before it in the list passed steps 1 to 4 for the same impression: claim_duplicate. The first such claim is the impression’s own claim, whatever its verdict; steps 6 to 9 are for it alone.
  6. The asset. Its creative_hash is the impression’s; the verifier holds a creative whose creative digest that is (§4), and the sum of that creative’s durations is the impression’s duration_ms; every span’s end is at most that duration_ms; and objects is exactly the content hashes of the creative’s objects that the spans touch, in the creative’s order, where a span touches the object of media time s to e when it starts before e and ends after s: claim_asset_mismatch.
  7. The quartile. The quartile it reaches (§6.3), with D the impression’s duration_ms, is the impression’s quartiles_reached: claim_quartile_mismatch.
  8. Viewability. It is viewable (§6.2) under the policy’s rule: claim_not_viewable.
  9. Attestation, when the policy sets attest: attestation is not empty (attestation_absent); it reads as a token (§7.1) of the policy’s token_type, its challenge_digest is SHA-256 of the TokenChallenge of §7.2, and its issuer’s verifier accepts it under the policy’s issuer (§7.3): attestation_invalid.

An impression counts when its own claim is presented. An impression with no own claim is stated claim_absent; one whose own claim was refused is stated with that claim’s reason.

9.3 The spend that counts

Under a policy that requires claims, a receipt’s presented spend is the sum of price_micros over its impressions that count, and live-ads §9.3 step 12 holds the presented spend of this receipt and every receipt before it, together, to the decider grant’s ceiling, in place of their billable spend. A receipt’s own rules (live-ads §8.5) are unchanged: its budget_spent_micros is still the decider’s billable spend, signed, and the verifier states both.

So a stripped, redirected or spoofed beacon counts nothing: an impression counts when a claim the session key signed is presented, and no other party holds that key. The decider’s quartiles_reached must be the quartile the player’s own claim reaches (step 7), so buyer and seller reconcile one figure from the same signed bytes.

9.4 What a verification states

With accept, beside live-ads §9.4, the verifier states:

The claims’ facts state each claim’s geometry through its bytes; a buyer that weighs a measurement over the player’s area (geometry 3) or a television assumption (geometry 2) reads it there.

10. The player

A player that makes claims MUST:

A player that measures through an Open Measurement integration reads the same facts from it: the ad view’s percentageInView (0 to 100, so visible_bp is 100 times it) and its reasons, the mediaPlayerVolume and deviceVolume of volumeChange, and the media events (OMID API 1.5). An interactive layer of SIMID 1.2 does not measure: “SIMID should not be set up to measure viewability” (§3.5).

The reference producer is web/ads/presentation.js of the browser client joule-moq-web: a recorder that turns observations (a visible fraction from an IntersectionObserver over the video element, the page’s visibility and focus, the element’s volume and muted state, its current time, and its playback-quality frame counts) into spans by the rules above, and writes and signs the claim with the platform’s Ed25519. Its tests drive it with observation sequences and require the claims of the corpus, byte for byte. An IntersectionObserver measures the element’s box against the viewport, not what other elements obstruct; a page that can obstruct its player states geometry 3 or measures obstruction itself.

11. Reasons

The codes this extension adds, each with where it is raised:

reasonraised at
claim_malformed§5.4, §9.2 step 1
claim_no_decision§9.2 step 3
claim_key_unbound§9.2 step 4
claim_duplicate§9.2 step 5
claim_asset_mismatch§9.2 step 6
claim_quartile_mismatch§9.2 step 7
claim_not_viewable§9.2 step 8
attestation_absent§9.2 step 9
attestation_invalid§9.2 step 9
claim_absent§9.2, an impression with no own claim

A claim also gets live-events’ bad_key and bad_signature (§9.2 step 2), and a policy live-ads’ ad_policy_malformed (§8). These verdicts never refuse a room: a room whose receipts verify is accepted, and its claims’ verdicts decide what counts. A room is refused only as live-ads §9 refuses it, funds_exceeded among them when the presented spend passes the ceiling.

12. Conformance

live-conformance/presentation/ holds the corpus, written from fixed seeds by wai_ad_presentation_vectors gen: vectors.json, and each accepted room’s HLS tags as a file of its own, as live-ads §11 writes them. vectors.json holds the creatives and the issuer the verifier holds; the public keys of the session keys; the first test vector of RFC 9578 Appendix A.2 (its pkI, TokenChallenge and token), whose issuer key signs the corpus’s tokens; and:

A conforming verifier verifies RFC 9578’s own token under its key, and gives every room the outcome, the reason, the place and everything else the corpus states, each claim its verdict and facts, and every tag file byte for byte. wai_ad_presentation_vectors verify and the test tests/ad_presentation.rs check that every file regenerates byte for byte, that wai-rs gives every case its outcome, that every reason of §11 appears, and that §11 lists exactly the reference’s codes. live-conformance/ad_presentation_verify.py is the second implementation, written from this text: it shares no code with wai-rs; it verifies each room, policy and receipt with the live-ads corpus’s second implementation (live_ads_verify.py), each Ed25519 signature with WAI’s strict rule (claim-conformance/_ed25519.py), and RSASSA-PSS with an implementation of its own over the standard library; and it mutates copies of the corpus and requires every copy to fail. The reference producer’s tests (§10) require the corpus’s claims from observation sequences.

13. Security and privacy considerations