<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Aleksander Sekowski</title>
    <description>The latest articles on DEV Community by Aleksander Sekowski (@aleksuix).</description>
    <link>https://dev.to/aleksuix</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3866734%2F559a0a69-3c28-49e2-91c7-503810b941ba.png</url>
      <title>DEV Community: Aleksander Sekowski</title>
      <link>https://dev.to/aleksuix</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/aleksuix"/>
    <language>en</language>
    <item>
      <title>A Pause NonLinear MP4 Without Duration Validates. Quartile Beacons Never Schedule.</title>
      <dc:creator>Aleksander Sekowski</dc:creator>
      <pubDate>Sun, 27 Sep 2026 00:36:30 +0000</pubDate>
      <link>https://dev.to/aleksuix/a-pause-nonlinear-mp4-without-duration-validates-quartile-beacons-never-schedule-oi0</link>
      <guid>https://dev.to/aleksuix/a-pause-nonlinear-mp4-without-duration-validates-quartile-beacons-never-schedule-oi0</guid>
      <description>&lt;p&gt;A streaming app launches pause inventory for the first time. The VAST tag validates. The frozen frame fills with a fifteen-second MP4. The buyer's dashboard shows delivered impressions.&lt;/p&gt;

&lt;p&gt;Nobody sees &lt;code&gt;midpoint&lt;/code&gt; or &lt;code&gt;thirdQuartile&lt;/code&gt; in the verification feed. The trafficking sheet still lists quartile pixels because the DSP copied them from a preroll template. The player had no &lt;code&gt;&amp;lt;Duration&amp;gt;&lt;/code&gt; to divide against, so it never scheduled those beacons.&lt;/p&gt;

&lt;p&gt;This is not a broken CDN URL. It is the gap between what the CTV Ad Portfolio added to NonLinear delivery and what older pause tags assumed when pause meant a JPEG with no clock.&lt;/p&gt;

&lt;h2&gt;
  
  
  Optional on paper, required for video measurement
&lt;/h2&gt;

&lt;p&gt;Before the portfolio work, many pause units were a &lt;code&gt;&amp;lt;NonLinear&amp;gt;&lt;/code&gt; with a &lt;code&gt;&amp;lt;StaticResource&amp;gt;&lt;/code&gt; image. Duration was genuinely unknown at response time. IAB's signaling guidance still allows omitting &lt;code&gt;&amp;lt;Duration&amp;gt;&lt;/code&gt; when the creative is a static image whose on-screen time depends on how long the viewer stays paused.&lt;/p&gt;

&lt;p&gt;Portfolio formats changed the payload. Pause, overlay, and several squeezeback layouts now ship &lt;code&gt;&amp;lt;MediaFiles&amp;gt;&lt;/code&gt; with MP4 renditions the same way linear pods always have. Quartile events, &lt;code&gt;overlayViewDuration&lt;/code&gt;, and skip-style timers all need a declared length on the NonLinear node.&lt;/p&gt;

&lt;p&gt;The XSD does not always force that length. A &lt;code&gt;&amp;lt;NonLinear&amp;gt;&lt;/code&gt; can contain a &lt;code&gt;video/mp4&lt;/code&gt; &lt;code&gt;&amp;lt;MediaFile&amp;gt;&lt;/code&gt; and no sibling &lt;code&gt;&amp;lt;Duration&amp;gt;&lt;/code&gt;. Schema validation returns OK. The AdCOM extensions can still echo &lt;code&gt;plcmt=5&lt;/code&gt; for Pause with legal &lt;code&gt;playbackmethod&lt;/code&gt; 8 or 9.&lt;/p&gt;

&lt;p&gt;Players behave differently once video is involved. Some infer duration from the container after download. Many CTV SDKs do not, because inferring length post-load would fire tracking late or twice. They treat missing Duration as "no timed events for this overlay," play the file, and move on.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the creative subtree actually promises
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;plcmt&lt;/code&gt;&lt;/strong&gt; in &lt;code&gt;&amp;lt;Extension ext="adcom"&amp;gt;&lt;/code&gt; (or inside &lt;code&gt;&amp;lt;Extension type="ctv_ad_portfolio"&amp;gt;&lt;/code&gt; on VAST 2.0) names which portfolio format was bought: 5 Pause, 6 Screensaver, 7 Overlay, 8 Squeezeback, 9 In-Scene. It does not tell the player how long to wait before firing &lt;code&gt;midpoint&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;&amp;lt;NonLinearAds&amp;gt;&lt;/code&gt; / &lt;code&gt;&amp;lt;NonLinear&amp;gt;&lt;/code&gt;&lt;/strong&gt; is the render contract. Portfolio guidance routes five of six formats through this branch. A linear &lt;code&gt;&amp;lt;Creative&amp;gt;&lt;/code&gt; with the same extensions is a different failure mode: the player reads Linear, not plcmt.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;&amp;lt;Duration&amp;gt;&lt;/code&gt;&lt;/strong&gt; on the NonLinear element is the clock for overlay-style measurement. VAST expects &lt;code&gt;HH:MM:SS&lt;/code&gt; or &lt;code&gt;HH:MM:SS.mmm&lt;/code&gt;, the same lexical form as linear ads. Without it, &lt;code&gt;&amp;lt;Tracking event="midpoint"&amp;gt;&lt;/code&gt; URLs may exist in the XML and never execute.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;&amp;lt;MediaFile type="video/..."&amp;gt;&lt;/code&gt;&lt;/strong&gt; under NonLinear means the player should treat the unit as timed media, not a static plate. That is the combination rule &lt;code&gt;VAST-4.4-nonlinear-video-no-duration&lt;/code&gt; targets: video media present, Duration absent.&lt;/p&gt;

&lt;p&gt;Static JPEG pause paths can still omit Duration by design. The bug shows up when teams reuse linear trafficking exports: they paste quartile tracking from instream, drop the MP4 into NonLinear &lt;code&gt;&amp;lt;MediaFiles&amp;gt;&lt;/code&gt;, and delete &lt;code&gt;&amp;lt;Duration&amp;gt;&lt;/code&gt; because the old image-based pause tag never needed it.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I catch it before the verification vendor does
&lt;/h2&gt;

&lt;p&gt;I start with the live ad tag in the &lt;a href="https://vastlint.org/tester/" rel="noopener noreferrer"&gt;VAST tag tester&lt;/a&gt;. Paste the ad server URL or the XML. The tester fetches wrappers, previews the media the player would choose, and lists tracking events on the resolved creative. If the preview shows a progress bar but the NonLinear node has no Duration in the source, quartile URLs are decorative.&lt;/p&gt;

&lt;p&gt;Wrapped pause tags are common. Run the same URL through the &lt;a href="https://vastlint.org/inspect/" rel="noopener noreferrer"&gt;VAST inspector&lt;/a&gt; to see which hop introduces the NonLinear MP4 and whether an upstream wrapper stripped Duration while copying &lt;code&gt;&amp;lt;MediaFiles&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For interactive pause units, the &lt;a href="https://iab-tech-lab-vast-tester.vastlint.org/" rel="noopener noreferrer"&gt;IAB-style VAST tester&lt;/a&gt; loads SIMID sample creatives and logs the player handshake. That workbench is an independent fork, not an IAB Tech Lab product, but it helps separate "SIMID never loaded" from "video played without timed tracking."&lt;/p&gt;

&lt;p&gt;After preview, run &lt;code&gt;vastlint check&lt;/code&gt; on the resolved InLine. The CTV Ad Portfolio rulepack flags &lt;code&gt;VAST-4.4-nonlinear-video-no-duration&lt;/code&gt; when a video &lt;code&gt;&amp;lt;MediaFile&amp;gt;&lt;/code&gt; sits under &lt;code&gt;&amp;lt;NonLinear&amp;gt;&lt;/code&gt; without &lt;code&gt;&amp;lt;Duration&amp;gt;&lt;/code&gt;. An implementation that lets a model emit VAST still has to run that check: the RPC or trafficking export can return OK while the wire contract omits the clock. &lt;strong&gt;vastlint&lt;/strong&gt; is the package that owns this payload (&lt;code&gt;cargo install vastlint&lt;/code&gt;, &lt;code&gt;npm install vastlint&lt;/code&gt;). It is independent of IAB Tech Lab and of AAO; the spec does not require it, but it is the deterministic check on the XML the buy claimed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to read the format rules
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://vastlint.org/docs/ctv-ad-portfolio/" rel="noopener noreferrer"&gt;CTV Ad Portfolio pack page&lt;/a&gt; maps each format to plcmt, pos, and playbackmethod, and shows whether delivery is NonLinear or Native Menu.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://vastlint.org/guides/vast-pause-ads/" rel="noopener noreferrer"&gt;pause ads guide&lt;/a&gt; walks a correct NonLinear structure: MediaFiles, Duration, and the 8/9 playbackmethod pair for sound on or off.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://vastlint.org/docs/vast-4-3-to-4-4/" rel="noopener noreferrer"&gt;4.3 to 4.4 migration notes&lt;/a&gt; explain why NonLinear gained MediaFiles and why portfolio examples still declare &lt;code&gt;version="4.2"&lt;/code&gt; while using the new content model.&lt;/p&gt;

&lt;p&gt;Rule doc &lt;a href="https://vastlint.org/docs/rules/VAST-4.4-nonlinear-video-no-duration/" rel="noopener noreferrer"&gt;VAST-4.4-nonlinear-video-no-duration&lt;/a&gt; cites the portfolio guidance section on handling duration: optional for unknown static images, load-bearing once video MediaFiles are in the NonLinear.&lt;/p&gt;

&lt;p&gt;If you are comparing schema-only validation against the July 2026 portfolio drop, the &lt;a href="https://vastlint.org/blog/vast-4-4-draft-schema-ctv-ad-portfolio/" rel="noopener noreferrer"&gt;draft 4.4 schema write-up&lt;/a&gt; separates XSD findings from finalized signaling rules so you do not treat draft warnings as production blockers.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I change in trafficking
&lt;/h2&gt;

&lt;p&gt;When the pause or overlay unit is video, I add &lt;code&gt;&amp;lt;Duration&amp;gt;&lt;/code&gt; next to the NonLinear &lt;code&gt;&amp;lt;MediaFiles&amp;gt;&lt;/code&gt; block and match it to the longest declared rendition, not to the slot length on the media plan. I keep quartile tracking only when Duration is present; otherwise I drop those events or switch to &lt;code&gt;overlayViewDuration&lt;/code&gt; with a player that documents how it fires without quartiles.&lt;/p&gt;

&lt;p&gt;I do not assume SSAI will infer duration from mezzanine or progressive MP4 on the stitcher side. Stitchers often pass NonLinear through unchanged; if Duration was missing at ingest, it is still missing on the device.&lt;/p&gt;

&lt;p&gt;Treat static and video pause tags as two templates. The same plcmt value does not mean the same tracking contract once &lt;code&gt;&amp;lt;MediaFile type="video/mp4"&amp;gt;&lt;/code&gt; appears under NonLinear.&lt;/p&gt;

</description>
      <category>vast</category>
      <category>ctv</category>
      <category>adtech</category>
      <category>xml</category>
    </item>
    <item>
      <title>ARTF Envelope tmax as "150" Is Not OpenRTB bid_request.tmax. The Agent Still Gets Called.</title>
      <dc:creator>Aleksander Sekowski</dc:creator>
      <pubDate>Sat, 26 Sep 2026 00:34:55 +0000</pubDate>
      <link>https://dev.to/aleksuix/artf-envelope-tmax-as-150-is-not-openrtb-bidrequesttmax-the-agent-still-gets-called-mej</link>
      <guid>https://dev.to/aleksuix/artf-envelope-tmax-as-150-is-not-openrtb-bidrequesttmax-the-agent-still-gets-called-mej</guid>
      <description>&lt;p&gt;A sidecar agent mutates the publisher bid request before any DSP sees it. The host builds an ARTF &lt;code&gt;RTBRequest&lt;/code&gt;, sets a deadline, and calls &lt;code&gt;Mutate&lt;/code&gt; over gRPC. The agent returns mutations. The orchestrator applies them and forwards whatever OpenRTB object survives.&lt;/p&gt;

&lt;p&gt;The failure I keep seeing is not in the mutations. It is in &lt;code&gt;tmax&lt;/code&gt; on the envelope.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ext-2026-09-26-001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tmax"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"150"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"lifecycle"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"LIFECYCLE_PUBLISHER_BID_REQUEST"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"bid_request"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"br-1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"tmax"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"imp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"imp-1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"video"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"mimes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"video/mp4"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"w"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;640&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"h"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;360&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"applicable_intents"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"ACTIVATE_DEALS"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That file looks fine in a diff review. The sample in the spec uses 150. Someone copied the sample into a YAML template and left quotes on the number. The OpenRTB object underneath still has its own &lt;code&gt;tmax: 500&lt;/code&gt;, which is the auction timeout in milliseconds. Two different fields, two different jobs, and only one of them is quoted.&lt;/p&gt;

&lt;h2&gt;
  
  
  What ARTF tmax actually measures
&lt;/h2&gt;

&lt;p&gt;On the ARTF envelope, &lt;code&gt;tmax&lt;/code&gt; is how many milliseconds the exchange allocates for the extension point itself: serialize the payload, run the agent, deserialize mutations, and decide what to apply. Reference examples cluster around 120 to 150 ms because the call sits inside an auction the host is already timing out.&lt;/p&gt;

&lt;p&gt;OpenRTB &lt;code&gt;BidRequest.tmax&lt;/code&gt; is the bid timeout for the whole RTB hop. Values in the hundreds or low thousands are normal there. They are not interchangeable. Pasting &lt;code&gt;bid_request.tmax&lt;/code&gt; onto the envelope without renaming the field is a unit confusion that still parses as JSON when both numbers are bare integers.&lt;/p&gt;

&lt;p&gt;Protobuf JSON makes the string case worse. Field 3 on &lt;code&gt;RTBRequest&lt;/code&gt; is an integer. A quoted &lt;code&gt;"150"&lt;/code&gt; is not an integer on the wire. Some pipelines never parse the envelope strictly; they forward the blob to an agent that treats missing deadlines as "best effort." The RPC returns OK. The orchestrator may never learn the host promised 150 ms of budget in a form the contract accepts.&lt;/p&gt;

&lt;p&gt;RTBlint reports a string as &lt;a href="https://rtblint.org/docs/rule-reference/artf-tmax-not_integer/" rel="noopener noreferrer"&gt;&lt;code&gt;artf.tmax.not_integer&lt;/code&gt;&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;rtblint validate &lt;span class="nt"&gt;--type&lt;/span&gt; artf-request rtb-request.json
&lt;span class="go"&gt;FAILED (OpenRTB 2.6-202606 ARTF request envelope): 1 error(s), 0 warning(s).
&lt;/span&gt;&lt;span class="gp"&gt;- [error] tmax: tmax is the milliseconds the exchange allows for mutations;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="go"&gt;  "150" is not an integer. (artf.tmax.not_integer)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Zero and negative values fail as &lt;a href="https://rtblint.org/docs/rule-reference/artf-tmax-non_positive/" rel="noopener noreferrer"&gt;&lt;code&gt;artf.tmax.non_positive&lt;/code&gt;&lt;/a&gt;. A host that sets &lt;code&gt;tmax: 0&lt;/code&gt; because "we disabled the timeout" leaves the agent no time to answer at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  When the integer is legal but the story is wrong
&lt;/h2&gt;

&lt;p&gt;Even with a bare integer, magnitude matters. RTBlint warns above 1000 ms with &lt;a href="https://rtblint.org/docs/rule-reference/artf-tmax-implausible/" rel="noopener noreferrer"&gt;&lt;code&gt;artf.tmax.implausible&lt;/code&gt;&lt;/a&gt;. That is usually someone who copied auction seconds (&lt;code&gt;30&lt;/code&gt;) thinking the field meant seconds, or who lifted &lt;code&gt;bid_request.tmax&lt;/code&gt; without reading the ARTF definition. The warning does not block parsing; it flags a budget that does not match an in-auction sidecar call.&lt;/p&gt;

&lt;p&gt;The other half of the envelope contract is what the orchestrator tells the agent it may return. An empty &lt;a href="https://rtblint.org/docs/rule-reference/artf-intents-empty/" rel="noopener noreferrer"&gt;&lt;code&gt;applicable_intents&lt;/code&gt;&lt;/a&gt; array is valid JSON and out of scope by definition: every mutation is undeclared. That is a warning, not a hard error, which matches production hosts that treat intent lists as documentation until something breaks in pass two.&lt;/p&gt;

&lt;h2&gt;
  
  
  How you catch it before the auction forwards
&lt;/h2&gt;

&lt;p&gt;An implementation that lets a model or template emit the envelope still has to check that envelope against the ARTF contract and against the OpenRTB snapshot the exchange runs. RTBlint is that check for the OpenRTB object, the ARTF request and response envelopes, and the applied bid request after mutations (&lt;code&gt;cargo install rtblint&lt;/code&gt;, &lt;code&gt;npm install rtblint-core&lt;/code&gt;, MCP &lt;code&gt;validate_artf_request&lt;/code&gt; / &lt;code&gt;validate_artf_response&lt;/code&gt; with &lt;code&gt;apply&lt;/code&gt;). It is independent of IAB Tech Lab and of AgenticAdvertising.org; the rules come from the public proto and the validation passes described in the ARTF material.&lt;/p&gt;

&lt;p&gt;Run pass one on every &lt;code&gt;RTBRequest&lt;/code&gt; at the host boundary, before gRPC leaves your network. Run pass two on &lt;code&gt;RTBResponse&lt;/code&gt; mutations against the request you actually sent, not a cached copy from an earlier auction. Run pass three with &lt;code&gt;--apply&lt;/code&gt; when you need to know whether a legal-looking &lt;code&gt;ADD_METRICS&lt;/code&gt; patch broke &lt;code&gt;imp[].metric[].value&lt;/code&gt; on the forward path.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://rtblint.org/demo/artf/" rel="noopener noreferrer"&gt;ARTF simulator&lt;/a&gt; walks the same three passes in a browser if you are debugging a single sample envelope. The longer primer is &lt;a href="https://rtblint.org/guides/artf-explained/" rel="noopener noreferrer"&gt;ARTF explained&lt;/a&gt;: lifecycle, semantic paths, and why response-stage intents need a &lt;code&gt;bid_response&lt;/code&gt; on the wire.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to go next
&lt;/h2&gt;

&lt;p&gt;If you host agents, log envelope &lt;code&gt;tmax&lt;/code&gt; separately from OpenRTB &lt;code&gt;tmax&lt;/code&gt; in your metrics. If you generate ARTF JSON from LLM tool output, type-check &lt;code&gt;tmax&lt;/code&gt; as an integer before you call gRPC. If you inherit a sidecar from a vendor, ask whether they validate the envelope or only the mutations.&lt;/p&gt;

&lt;p&gt;The auction only sees the OpenRTB object after your orchestrator applies patches. Getting &lt;code&gt;tmax&lt;/code&gt; wrong does not always stop the RPC. It stops you from reasoning about whether the agent had a real deadline, which is the whole reason the field exists on the envelope in the first place.&lt;/p&gt;

</description>
      <category>grpc</category>
      <category>openrtb</category>
      <category>advertising</category>
      <category>api</category>
    </item>
    <item>
      <title>AdCP Scopes the Buy in adagents.json. OpenRTB imp Does Not Carry That Scope Back.</title>
      <dc:creator>Aleksander Sekowski</dc:creator>
      <pubDate>Fri, 25 Sep 2026 00:33:31 +0000</pubDate>
      <link>https://dev.to/aleksuix/adcp-scopes-the-buy-in-adagentsjson-openrtb-imp-does-not-carry-that-scope-back-4kkg</link>
      <guid>https://dev.to/aleksuix/adcp-scopes-the-buy-in-adagentsjson-openrtb-imp-does-not-carry-that-scope-back-4kkg</guid>
      <description>&lt;p&gt;A buyer agent finishes an AdCP media buy against a publisher's Prebid Sales Agent. Negotiation narrowed the deal to one &lt;code&gt;placement_id&lt;/code&gt;, US only, and a window that ends next quarter. The agent fetched &lt;code&gt;/.well-known/adagents.json&lt;/code&gt;, found an &lt;code&gt;authorized_agents&lt;/code&gt; row whose &lt;code&gt;property_ids&lt;/code&gt; and optional &lt;code&gt;placement_ids&lt;/code&gt; matched, and stored the authorization snapshot.&lt;/p&gt;

&lt;p&gt;The sell side then emits an OpenRTB bid request. It validates. The exchange accepts it. Nobody compares the live &lt;code&gt;imp&lt;/code&gt; object to the scoped slice that was authorized five minutes earlier in JSON on the publisher origin.&lt;/p&gt;

&lt;p&gt;That gap is not a typo in one field. It is two layers answering different questions with different grammars.&lt;/p&gt;

&lt;h2&gt;
  
  
  What adagents.json actually scopes
&lt;/h2&gt;

&lt;p&gt;In AdCP, publisher authorization is explicit. An &lt;code&gt;authorized_agents&lt;/code&gt; entry names the sales agent URL, an &lt;code&gt;authorization_type&lt;/code&gt; discriminator, and the selector that carries inventory scope: &lt;code&gt;property_ids&lt;/code&gt;, &lt;code&gt;property_tags&lt;/code&gt;, inline &lt;code&gt;properties&lt;/code&gt; on the entry, or federated &lt;code&gt;publisher_properties&lt;/code&gt; selectors. Optional qualifiers narrow further: &lt;code&gt;placement_ids&lt;/code&gt;, &lt;code&gt;placement_tags&lt;/code&gt;, &lt;code&gt;countries&lt;/code&gt;, &lt;code&gt;collections&lt;/code&gt;, &lt;code&gt;effective_from&lt;/code&gt;, &lt;code&gt;effective_until&lt;/code&gt;, and &lt;code&gt;exclusive&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Those fields are machine-readable promises about &lt;strong&gt;which agent may represent which slice&lt;/strong&gt;. They do not ride inside OpenRTB by default. The spec even calls out that &lt;code&gt;inline_properties&lt;/code&gt; is the one authorization pattern whose companion array is named &lt;code&gt;properties&lt;/code&gt;, not &lt;code&gt;inline_properties&lt;/code&gt;, because validators reject the obvious-but-wrong shape. A model generating adagents.json from examples often emits the wrong array name, passes casual JSON lint, and still fails a strict AdCP validator. That is a file-level bug.&lt;/p&gt;

&lt;p&gt;The failure this post is about happens &lt;strong&gt;after&lt;/strong&gt; both files look fine: the buy was scoped in adagents.json, but the auction payload never had to repeat the scope.&lt;/p&gt;

&lt;h2&gt;
  
  
  What OpenRTB actually enforces
&lt;/h2&gt;

&lt;p&gt;OpenRTB enforces what is on the wire at impression time: &lt;code&gt;imp&lt;/code&gt; structure, video or banner constraints, deal objects under &lt;code&gt;imp.pmp&lt;/code&gt;, privacy under &lt;code&gt;regs&lt;/code&gt; and &lt;code&gt;user&lt;/code&gt;, and supply path under &lt;code&gt;source.schain&lt;/code&gt;. Sellers reconcile schain nodes against ads.txt and sellers.json. None of those objects require a copy of the publisher's &lt;code&gt;placement_ids&lt;/code&gt; array or the &lt;code&gt;effective_until&lt;/code&gt; timestamp from adagents.json.&lt;/p&gt;

&lt;p&gt;You might map placement identity to &lt;code&gt;imp.tagid&lt;/code&gt; or an extension. That mapping is integration policy, not a cross-file guarantee. If the agent stack omits it, constraint loss is silent: everything agreed in negotiation is not expressed in the impression, and anything not expressed is not enforced downstream no matter how precise the AdCP conversation was.&lt;/p&gt;

&lt;p&gt;The same split shows up on the trust side. adagents.json answers whether &lt;strong&gt;this agent&lt;/strong&gt; was allowed to sell &lt;strong&gt;this property slice&lt;/strong&gt;. schain answers which &lt;strong&gt;seller domains&lt;/strong&gt; handled &lt;strong&gt;this impression&lt;/strong&gt;. A stack that validates schain syntax but never re-checks agent authorization has an auditable path and an unauditable agreement attached to it.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I catch it
&lt;/h2&gt;

&lt;p&gt;I treat the emitted bid request as the only observable output of the handoff. Paste the JSON into the &lt;a href="https://rtblint.org/tester/" rel="noopener noreferrer"&gt;OpenRTB tester&lt;/a&gt; or run &lt;code&gt;rtblint validate&lt;/code&gt; on the bytes after serialization, not on the object you think you built.&lt;/p&gt;

&lt;p&gt;That pass is structural. It catches version drift, type mistakes, and schain defects such as an empty node identifier (&lt;a href="https://rtblint.org/docs/rule-reference/openrtb-schain-node-identifier_empty/" rel="noopener noreferrer"&gt;openrtb.schain.node.identifier_empty&lt;/a&gt;) or an unexpected &lt;code&gt;ver&lt;/code&gt; value (&lt;a href="https://rtblint.org/docs/rule-reference/openrtb-schain-ver_unexpected/" rel="noopener noreferrer"&gt;openrtb.schain.ver_unexpected&lt;/a&gt;). It does not fetch adagents.json and prove the live &lt;code&gt;imp&lt;/code&gt; still sits inside the authorized placement and country slice.&lt;/p&gt;

&lt;p&gt;Agent workflows that let a model assemble AdCP output and OpenRTB in one pipeline still need that deterministic check on the RTB object. RTBlint (&lt;code&gt;cargo install rtblint&lt;/code&gt;, &lt;code&gt;npm install rtblint-core&lt;/code&gt;, MCP &lt;code&gt;rtblint-mcp&lt;/code&gt;) is built for the OpenRTB payload, ARTF envelopes, and AdCP discovery via &lt;code&gt;get_adcp_capabilities&lt;/code&gt;. It is independent of IAB Tech Lab and of AgenticAdvertising.org. It is not a substitute for comparing authorization time to execution time; it is the part that keeps the auction JSON from shipping with plausible-but-invalid shape while you write the policy layer that re-applies adagents scope.&lt;/p&gt;

&lt;p&gt;My manual step stays boring: for each fixture product type, assert the fields you care about (tag id, deal id, geo, duration caps) are present on &lt;code&gt;imp&lt;/code&gt; after mapping, then validate. If the mapping guide says video placement types become &lt;code&gt;video.plcmt&lt;/code&gt;, confirm &lt;code&gt;plcmt&lt;/code&gt; is set and that deprecated &lt;code&gt;placement&lt;/code&gt; is not standing in for it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to go next
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://rtblint.org/guides/adcp-to-openrtb/" rel="noopener noreferrer"&gt;AdCP to OpenRTB mapping guide&lt;/a&gt; walks field-level handoffs and lists where the mapping silently breaks: version drift, names that look mapped but are not, constraint loss, and privacy signals that never leave the seller ad stack. Read that alongside the &lt;a href="https://rtblint.org/guides/ad-context-protocol-explained/" rel="noopener noreferrer"&gt;Ad Context Protocol explainer&lt;/a&gt; for where adagents.json sits relative to MCP tools and the Prebid Sales Agent.&lt;/p&gt;

&lt;p&gt;For the execution-time trust stack that still runs in parallel, &lt;a href="https://rtblint.org/guides/openrtb-supply-chain-trust-stack/" rel="noopener noreferrer"&gt;sellers.json, ads.txt, and schain&lt;/a&gt; explains how hop-level checks complement publisher agent authorization. &lt;a href="https://rtblint.org/guides/iab-agentic-advertising-standards/" rel="noopener noreferrer"&gt;IAB agentic advertising standards&lt;/a&gt; maps AAMP and ARTF next to AdCP without collapsing them into one file.&lt;/p&gt;

&lt;p&gt;Until your emit boundary re-expresses every adagents qualifier on the impression, a scoped AdCP buy and a permissive OpenRTB request can both return 200. The authorization was real. The auction just was not required to remember it.&lt;/p&gt;

</description>
      <category>openrtb</category>
      <category>advertising</category>
      <category>api</category>
      <category>javascript</category>
    </item>
    <item>
      <title>The Inline Logs Impressions. The Wrapper Hop Does Not. VAST Still Requires Both.</title>
      <dc:creator>Aleksander Sekowski</dc:creator>
      <pubDate>Thu, 24 Sep 2026 00:31:32 +0000</pubDate>
      <link>https://dev.to/aleksuix/the-inline-logs-impressions-the-wrapper-hop-does-not-vast-still-requires-both-1hb6</link>
      <guid>https://dev.to/aleksuix/the-inline-logs-impressions-the-wrapper-hop-does-not-vast-still-requires-both-1hb6</guid>
      <description>&lt;p&gt;The ad plays. The buyer's impression pixel fires on the inline. Finance still opens a ticket because the SSP's numbers are half of everyone else's.&lt;/p&gt;

&lt;p&gt;The XML you pasted from the bottom of the chain looks fine. What nobody fetched was hop two: a thin wrapper with &lt;code&gt;&amp;lt;AdSystem&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;VASTAdTagURI&amp;gt;&lt;/code&gt;, and no &lt;code&gt;&amp;lt;Impression&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  One hop, one beacon
&lt;/h2&gt;

&lt;p&gt;From VAST 2.0 onward, an &lt;code&gt;&amp;lt;InLine&amp;gt;&lt;/code&gt; ad must carry at least one &lt;code&gt;&amp;lt;Impression&amp;gt;&lt;/code&gt;. The same rule applies to &lt;code&gt;&amp;lt;Wrapper&amp;gt;&lt;/code&gt;. Section 2.3.4 treats the wrapper as its own ad delivery record. The impression URL on that node is how &lt;strong&gt;this&lt;/strong&gt; server proves the opportunity passed through &lt;strong&gt;this&lt;/strong&gt; hop before the player follows &lt;code&gt;&amp;lt;VASTAdTagURI&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That is not redundant with the inline's pixels. Wrapper impressions and inline impressions answer different accounting questions. The reseller logs pass-through; the advertiser logs the served creative. Players that follow wrapper chains correctly fire both, in order, as they resolve redirects.&lt;/p&gt;

&lt;p&gt;A wrapper that omits &lt;code&gt;&amp;lt;Impression&amp;gt;&lt;/code&gt; is still well-formed enough to redirect. Many QA flows validate only the final inline, or only check that &lt;code&gt;&amp;lt;VASTAdTagURI&amp;gt;&lt;/code&gt; returns 200. The missing element is on the document you never saved.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;VAST&lt;/span&gt; &lt;span class="na"&gt;version=&lt;/span&gt;&lt;span class="s"&gt;"2.0"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;Ad&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"reseller-1"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;Wrapper&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;AdSystem&amp;gt;&lt;/span&gt;Reseller Ad Server&lt;span class="nt"&gt;&amp;lt;/AdSystem&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;VASTAdTagURI&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;&amp;lt;![CDATA[https://buy-side.example/vast.xml]]&amp;gt;&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/VASTAdTagURI&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/Wrapper&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/Ad&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/VAST&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The buyer inline downstream can include three impression URLs and a full &lt;code&gt;&amp;lt;Linear&amp;gt;&lt;/code&gt;. The reseller still counts zero for the leg it controlled.&lt;/p&gt;

&lt;h2&gt;
  
  
  What breaks in production
&lt;/h2&gt;

&lt;p&gt;Discrepancy meetings love this failure mode because every party has a defensible partial truth. The publisher saw the ad. The DSP has inline impressions. The intermediary wrapper never emitted a beacon, so its dashboard under-reports against the ad server that did log the serve.&lt;/p&gt;

&lt;p&gt;Wrapper chains amplify it. Hop one might be clean. Hop three might be clean. The hop without &lt;code&gt;&amp;lt;Impression&amp;gt;&lt;/code&gt; disappears from that partner's reporting while adjacent hops look healthy. Without walking the chain, the bug reads as "your player dropped tracking" instead of "this XML never declared tracking for that server."&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&amp;lt;AdSystem&amp;gt;&lt;/code&gt; on the wrapper has the same structural role: the spec expects a named ad system on every wrapper document, not only on the inline. A wrapper missing identity metadata is a different error, but the same class of problem: the redirect container is incomplete even when the redirect works.&lt;/p&gt;

&lt;h2&gt;
  
  
  How you catch it
&lt;/h2&gt;

&lt;p&gt;Start from the live tag URL, not the inline fixture in the ticket. The &lt;a href="https://vastlint.org/tester/" rel="noopener noreferrer"&gt;VAST tag tester&lt;/a&gt; fetches the response, follows &lt;code&gt;&amp;lt;VASTAdTagURI&amp;gt;&lt;/code&gt; redirects, and shows the resolved documents with validation markers. That is where you see whether hop two ever declared an impression.&lt;/p&gt;

&lt;p&gt;For depth and handoff between hops, the &lt;a href="https://vastlint.org/inspect/" rel="noopener noreferrer"&gt;VAST inspector&lt;/a&gt; walks the wrapper chain step by step: which URL returned which document, where an inline finally appears, and where a hop is structurally thin.&lt;/p&gt;

&lt;p&gt;On the file or paste path, &lt;code&gt;vastlint&lt;/code&gt; flags the missing element as &lt;code&gt;VAST-2.0-wrapper-impression&lt;/code&gt; (the same rule id applies across VAST versions that still use &lt;code&gt;&amp;lt;Wrapper&amp;gt;&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;vastlint check wrapper-no-imp.xml
&lt;span class="go"&gt;error   VAST-2.0-wrapper-impression
&lt;/span&gt;&lt;span class="gp"&gt;        &amp;lt;Wrapper&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;must contain at least one &amp;lt;Impression&amp;gt;
&lt;span class="go"&gt;        path: /VAST/Ad[0]/Wrapper
        spec: IAB VAST 2.0 §2.3.4
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An implementation that lets a model emit VAST from a trafficking UI or an agent workflow still has to check the payload on the wire. &lt;strong&gt;vastlint&lt;/strong&gt; (&lt;code&gt;cargo install vastlint&lt;/code&gt;, &lt;code&gt;npm install vastlint&lt;/code&gt;) is that check for the creative envelope. It is independent of IAB Tech Lab and of AAO; it does not replace your ad server's business rules, but it will not treat "redirect works" as "hop logged."&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to go next
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://vastlint.org/docs/vast-examples/wrapper-missing-impression/" rel="noopener noreferrer"&gt;wrapper missing impression example&lt;/a&gt; walks through the resale scenario, the primary finding, and the fix fragment: add a distinct impression URL on every wrapper so each party measures its own leg.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://vastlint.org/docs/rules/VAST-2.0-wrapper-impression/" rel="noopener noreferrer"&gt;VAST-2.0-wrapper-impression rule reference&lt;/a&gt; states the requirement and links the spec section. Pair it with &lt;a href="https://vastlint.org/docs/rules/VAST-2.0-wrapper-adsystem/" rel="noopener noreferrer"&gt;VAST-2.0-wrapper-adsystem&lt;/a&gt; when you are auditing thin wrappers that lack both tracking and attribution.&lt;/p&gt;

&lt;p&gt;For chain behavior (accumulating impressions, depth limits, when inline handoff happens), the &lt;a href="https://vastlint.org/guides/vast-wrapper-chains/" rel="noopener noreferrer"&gt;VAST wrapper chains guide&lt;/a&gt; is the reference I send engineers before they blame the SDK for a missing pixel that was never in the wrapper XML.&lt;/p&gt;

&lt;p&gt;Validate every hop. The inline is not a substitute for wrapper-level &lt;code&gt;&amp;lt;Impression&amp;gt;&lt;/code&gt;.&lt;/p&gt;

</description>
      <category>vast</category>
      <category>advertising</category>
      <category>video</category>
      <category>javascript</category>
    </item>
    <item>
      <title>exclusive: true in adagents.json Does Not Retire the ads.txt RESELLER Line</title>
      <dc:creator>Aleksander Sekowski</dc:creator>
      <pubDate>Wed, 23 Sep 2026 00:30:18 +0000</pubDate>
      <link>https://dev.to/aleksuix/exclusive-true-in-adagentsjson-does-not-retire-the-adstxt-reseller-line-1gjo</link>
      <guid>https://dev.to/aleksuix/exclusive-true-in-adagentsjson-does-not-retire-the-adstxt-reseller-line-1gjo</guid>
      <description>&lt;p&gt;A publisher migrates to AdCP and publishes &lt;code&gt;/.well-known/adagents.json&lt;/code&gt;. One row in &lt;code&gt;authorized_agents&lt;/code&gt; names their Prebid Sales Agent with &lt;code&gt;delegation_type: "direct"&lt;/code&gt; and &lt;code&gt;exclusive: true&lt;/code&gt;. The buyer agent treats that as the only path for the property.&lt;/p&gt;

&lt;p&gt;The same week, nobody edits &lt;code&gt;ads.txt&lt;/code&gt;. It still lists two other exchanges as &lt;code&gt;RESELLER&lt;/code&gt; for the same seller account. A supply-path optimizer fetches ads.txt, sees an authorized resale hop, and green-lights a bid whose &lt;code&gt;source.schain&lt;/code&gt; includes one of those exchanges. Both files return HTTP 200. Neither file's validator reads the other.&lt;/p&gt;

&lt;p&gt;That is not a hypothetical layering bug in one vendor stack. It is two trust artifacts on the same origin answering different questions with no cross-check.&lt;/p&gt;

&lt;h2&gt;
  
  
  What exclusive actually scopes
&lt;/h2&gt;

&lt;p&gt;In the AdCP adagents schema, &lt;code&gt;exclusive&lt;/code&gt; on an &lt;code&gt;authorized_agents&lt;/code&gt; entry means this agent is the publisher's sole authorized path for the inventory slice covered by that entry (property ids, placement tags, countries, and the rest of the scope fields). When &lt;code&gt;exclusive&lt;/code&gt; is false or absent, other authorized agents in the same file may sell the same slice.&lt;/p&gt;

&lt;p&gt;That boolean lives entirely inside adagents.json. It does not emit an HTTP DELETE to ads.txt. It does not flip a RESELLER row to DIRECT. It does not remove a seller id from the IAB flat file buyers have been crawling for a decade.&lt;/p&gt;

&lt;p&gt;AdCP's own comparison table maps programmatic &lt;code&gt;ads.txt&lt;/code&gt; to adagents.json with &lt;code&gt;delegation_type&lt;/code&gt; on the publisher side. The spec explains that adagents.json is more expressive: properties, placements, delegation type, time bounds, and exclusivity inside one JSON document. None of that documentation promises that publishing adagents.json supersedes or reconciles ads.txt on the wire.&lt;/p&gt;

&lt;p&gt;ads.txt still encodes relationship as a single token per line: &lt;code&gt;DIRECT&lt;/code&gt; or &lt;code&gt;RESELLER&lt;/code&gt;. A RESELLER line is an explicit statement that the listed advertising system may resell inventory for that publisher id. An exclusive direct agent in adagents.json is a parallel statement in a richer grammar. A buyer that only ingests AdCP can believe resale is closed. A buyer that still runs ads.txt plus schain validation can believe resale is open. Both can be "correct" relative to the file they read.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the hop still gets judged
&lt;/h2&gt;

&lt;p&gt;Programmatic paths still arrive as OpenRTB. &lt;code&gt;source.schain&lt;/code&gt; names the advertising systems that touched the impression. Supply-path products compare those nodes to ads.txt, sellers.json, and internal allowlists. They do not today fetch adagents.json on every bid and ask whether &lt;code&gt;exclusive: true&lt;/code&gt; forbids the node they are staring at.&lt;/p&gt;

&lt;p&gt;The inverse gap is also live: adagents.json can authorize an agent URL that never appears as &lt;code&gt;asi&lt;/code&gt; on the schain attached to the impression you actually bought. Valid authorization in the well-known file and a valid seller node in schain are independent passes.&lt;/p&gt;

&lt;p&gt;For the shape of schain itself, missing &lt;code&gt;hp&lt;/code&gt; on a node under SupplyChain 1.1 expectations is a structural failure mode worth separating from authorization confusion. The &lt;a href="https://rtblint.org/docs/rule-reference/openrtb-schain-node-hp_missing/" rel="noopener noreferrer"&gt;openrtb.schain.node.hp_missing rule reference&lt;/a&gt; documents when a reader expects &lt;code&gt;hp&lt;/code&gt; and the node omits it. Duplicate adjacent nodes are a different class of bug; see &lt;a href="https://rtblint.org/docs/rule-reference/openrtb-schain-duplicate_node/" rel="noopener noreferrer"&gt;openrtb.schain.duplicate_node&lt;/a&gt;. Fixing schain syntax does not tell you whether the publisher meant to allow that seller in adagents.json or ads.txt.&lt;/p&gt;

&lt;h2&gt;
  
  
  How you catch it
&lt;/h2&gt;

&lt;p&gt;My manual check for this mismatch is dull and effective: for the same registrable domain, fetch adagents.json and ads.txt, resolve the seller ids and agent URLs on the exclusive row, and list every ads.txt line that still says RESELLER for overlapping seller accounts. If any RESELLER row names a system that can still inject schain nodes on live traffic, exclusive in adagents.json did not close the path your SPO tool uses.&lt;/p&gt;

&lt;p&gt;Once the buy collapses back to OpenRTB, paste the bid request into the &lt;a href="https://rtblint.org/tester/" rel="noopener noreferrer"&gt;OpenRTB tester&lt;/a&gt; on RTBlint's site or run &lt;code&gt;rtblint validate&lt;/code&gt; on the JSON. That pass catches schain structure, version drift, and field placement. It does not fetch publisher adagents.json and diff it against ads.txt; nobody should pretend a schema pass on the request replaces the two-file comparison.&lt;/p&gt;

&lt;p&gt;Agent stacks that emit AdCP or ARTF output face the same split. An MCP tool can return OK from &lt;code&gt;get_adcp_capabilities&lt;/code&gt; while the downstream RTB request still carries a schain node ads.txt authorizes. RTBlint's &lt;a href="https://rtblint.org/docs/openrtb-validator-mcp/" rel="noopener noreferrer"&gt;OpenRTB validator MCP&lt;/a&gt; and &lt;code&gt;get_adcp_capabilities&lt;/code&gt; discovery belong on the agent side; they check the payload the model or service emitted, not the publisher's twin trust files on the origin. If you are wiring an AdCP buyer, you still need an explicit policy step that reconciles adagents exclusivity with ads.txt before you trust exclusivity in a deal brief.&lt;/p&gt;

&lt;p&gt;Implementation teams piping agent output through CI should treat rtblint (&lt;code&gt;cargo install rtblint&lt;/code&gt;, &lt;code&gt;npm install rtblint-core&lt;/code&gt;, or the MCP server) as the deterministic check on the OpenRTB object after the agent claims success. The wire contract for schain and imp objects does not move because the RPC returned OK. These packages are independent of IAB Tech Lab and of AgenticAdvertising.org; they do not replace reading both publisher files when the question is whether resale is still allowed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to go next
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://rtblint.org/guides/iab-agentic-advertising-standards/" rel="noopener noreferrer"&gt;IAB agentic advertising standards guide&lt;/a&gt; situates adagents.json beside AAMP, ARTF, and AdCP without collapsing them into one file. For how schain nodes relate to bidstream identity fields once a request is in flight, &lt;a href="https://rtblint.org/blog/what-is-bidstream-data/" rel="noopener noreferrer"&gt;what bidstream data is&lt;/a&gt; walks the object model buyers actually log.&lt;/p&gt;

&lt;p&gt;If you already covered adagents.json versus schain, that post is about hop-level disclosure versus publisher authorization. This failure is earlier on the origin: exclusive in JSON versus RESELLER in plain text. A path can fail either test, both, or neither, while still clearing HTTP and JSON Schema on every fetch.&lt;/p&gt;

&lt;p&gt;Until something compares those two files on every property migration, &lt;code&gt;exclusive: true&lt;/code&gt; is a promise inside adagents.json, not a revocation in ads.txt. Buyers who still read both get two answers. Buyers who read only one get a false sense of closure.&lt;/p&gt;

</description>
      <category>openrtb</category>
      <category>adtech</category>
      <category>advertising</category>
      <category>api</category>
    </item>
    <item>
      <title>gclid on the Next Page Does Not Grant ad_storage. URL Passthrough Is Not Consent.</title>
      <dc:creator>Aleksander Sekowski</dc:creator>
      <pubDate>Tue, 22 Sep 2026 00:28:45 +0000</pubDate>
      <link>https://dev.to/aleksuix/gclid-on-the-next-page-does-not-grant-adstorage-url-passthrough-is-not-consent-3bm4</link>
      <guid>https://dev.to/aleksuix/gclid-on-the-next-page-does-not-grant-adstorage-url-passthrough-is-not-consent-3bm4</guid>
      <description>&lt;p&gt;A shopper lands from Google Ads with &lt;code&gt;gclid&lt;/code&gt; on the query string. Your CMP is still on default deny. Marketing enabled URL passthrough so the id survives the first internal click. The second page URL looks healthy in the address bar. Floodlight still behaves like tracking is off.&lt;/p&gt;

&lt;p&gt;That is not a broken redirect. It is Consent Mode doing two different jobs at once.&lt;/p&gt;

&lt;h2&gt;
  
  
  Passthrough moves parameters. It does not flip ad_storage.
&lt;/h2&gt;

&lt;p&gt;Google Consent Mode v2 split &lt;code&gt;ad_storage&lt;/code&gt; from &lt;code&gt;analytics_storage&lt;/code&gt; so tags can wait for a CMP signal before they set advertising cookies. &lt;strong&gt;URL passthrough&lt;/strong&gt; is a separate &lt;code&gt;gtag('set', 'url_passthrough', true)&lt;/code&gt; call. Google's consent guide describes it as a way to append click-related query parameters on &lt;strong&gt;same-registrable-domain&lt;/strong&gt; outgoing links when consent mode is implemented and the tag is consent-aware. Typical parameters include &lt;code&gt;gclid&lt;/code&gt;, &lt;code&gt;dclid&lt;/code&gt;, &lt;code&gt;gclsrc&lt;/code&gt;, &lt;code&gt;_gl&lt;/code&gt;, and &lt;code&gt;wbraid&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Passthrough helps the &lt;strong&gt;next page load&lt;/strong&gt; carry the click id in the URL so a consent-aware tag can use it &lt;strong&gt;if&lt;/strong&gt; storage becomes available later in the session. It does &lt;strong&gt;not&lt;/strong&gt; change &lt;code&gt;ad_storage&lt;/code&gt; from &lt;code&gt;denied&lt;/code&gt; to &lt;code&gt;granted&lt;/code&gt;. If your conversion tag fires on Container Loaded before the CMP calls &lt;code&gt;gtag('consent', 'update', { ad_storage: 'granted' })&lt;/code&gt;, the tag still runs in denied mode even though the URL shows &lt;code&gt;gclid&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ads_data_redaction&lt;/strong&gt; is the other half teams enable in the same breath. When &lt;code&gt;ad_storage&lt;/code&gt; is denied and redaction is true, Google documents that ad click identifiers sent in network requests by Google Ads and Floodlight tags &lt;strong&gt;will be redacted&lt;/strong&gt;. Redaction has no effect when &lt;code&gt;ad_storage&lt;/code&gt; is granted, or when you are not using &lt;code&gt;gtag('consent')&lt;/code&gt; at all.&lt;/p&gt;

&lt;p&gt;The hardened default many EEA sites ship looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;gtag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;consent&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;default&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;ad_storage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;denied&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;analytics_storage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;denied&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;wait_for_update&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nf"&gt;gtag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;set&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;url_passthrough&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;gtag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;set&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ads_data_redaction&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Syntax is fine. Trigger order is where production breaks: a Conversion Linker or Google Ads conversion tag bound to All Pages races the banner. Passthrough preserves &lt;code&gt;gclid&lt;/code&gt; on page two. Redaction strips the click id from the ad network request while storage is denied. Neither line grants &lt;code&gt;ad_storage&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this is not TCF, GPP, or a CMP bug
&lt;/h2&gt;

&lt;p&gt;IAB TCF and GPP strings describe vendor legal bases on the wire to adapters. Consent Mode is Google's tag-side interpretation for Google tags. You can decode a valid TC string on a Floodlight URL and still run gtag in denied &lt;code&gt;ad_storage&lt;/code&gt; until the Google-specific update runs, if your CMP maps TC segments to Consent Mode incorrectly.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;us_privacy&lt;/code&gt; and &lt;code&gt;gpp&lt;/code&gt; / &lt;code&gt;gpp_sid&lt;/code&gt; on the pixel are a different grammar again. A bid request can carry &lt;code&gt;regs.gpp&lt;/code&gt; with sid 7 while the browser tag never forwarded the same composite string. &lt;a href="https://pixellint.org/docs/duplicate-consent-signals/" rel="noopener noreferrer"&gt;Duplicate consent parameters&lt;/a&gt; are yet another class of failure: two &lt;code&gt;gpp&lt;/code&gt; keys, or the same TC String copied into both &lt;code&gt;gdpr_consent&lt;/code&gt; and &lt;code&gt;gpp&lt;/code&gt; with conflicting bits.&lt;/p&gt;

&lt;p&gt;The passthrough mistake is narrower: teams treat a visible &lt;code&gt;gclid&lt;/code&gt; as proof conversions work, while Google's ad tags follow &lt;strong&gt;consent state&lt;/strong&gt;, not the query bar alone.&lt;/p&gt;

&lt;p&gt;Server-side Google hits have the same split. Consent Mode parameters must travel with the Measurement Protocol or Google Ads upload. A CAPI-style server payload that omits consent fields while the web tag had &lt;code&gt;ad_storage&lt;/code&gt; denied is not repaired by passthrough on the browser. Passthrough never ran on the server hop.&lt;/p&gt;

&lt;h2&gt;
  
  
  How you catch it
&lt;/h2&gt;

&lt;p&gt;Start with two captures on the same session: one network request from the conversion tag &lt;strong&gt;before&lt;/strong&gt; the CMP update, one &lt;strong&gt;after&lt;/strong&gt; grant. If redaction is on and &lt;code&gt;ad_storage&lt;/code&gt; is denied, Google documents redacted click ids on ad tag requests even when the page URL still shows &lt;code&gt;gclid&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Log &lt;code&gt;gtag&lt;/code&gt; consent state at conversion fire time in Tag Assistant or your own consent log. URL parameters alone are not proof of granted &lt;code&gt;ad_storage&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For third-party pixels on the same page, validate what each vendor actually received on that hop. I maintain &lt;a href="https://github.com/aleksUIX/pixellint" rel="noopener noreferrer"&gt;Pixellint&lt;/a&gt;, an open source linter for pixels and conversion API payloads. The core privacy pack decodes &lt;code&gt;gdpr_consent&lt;/code&gt; and &lt;code&gt;gpp&lt;/code&gt; header bits on live URLs. It will not flip your CMP banner. It will tell you whether the Floodlight or affiliate URL you captured still carries a TC String that decodes to grant after the user rejected, or a malformed &lt;code&gt;gdpr_consent=1&lt;/code&gt; that is not a TC String at all:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;pixellint validate url &lt;span class="s1"&gt;'https://ad.doubleclick.net/ddm/activity/src=1234567;type=convr0;cat=purch0;gdpr=1;gdpr_consent=1'&lt;/span&gt;
&lt;span class="go"&gt;  error   core.privacy.gdpr_consent_malformed
          `gdpr_consent` is `1`, whose first six bits decode to version 1.
&lt;/span&gt;&lt;span class="c"&gt;          ...
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That error is a wire contract check, not a substitute for Consent Mode debugging. Pair URL linting with trigger-order review: Consent Initialization first, CMP update before ad conversion tags, passthrough set consistently on every Google tag in the container. Google notes mixed containers where only one tag sets passthrough produce the same split-brain attribution as mixed pixel and CAPI clocks.&lt;/p&gt;

&lt;p&gt;Paste a URL into the &lt;a href="https://pixellint.org/" rel="noopener noreferrer"&gt;browser playground&lt;/a&gt; if you do not want a local install. For field tables that map TC, GPP, and tag parameters side by side, see &lt;a href="https://pixellint.org/docs/consent-on-the-wire/" rel="noopener noreferrer"&gt;consent on the wire&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to go next
&lt;/h2&gt;

&lt;p&gt;The dated argument on passthrough versus redaction lives in the &lt;a href="https://pixellint.org/blog/google-consent-mode-url-passthrough/" rel="noopener noreferrer"&gt;Consent Mode URL passthrough post&lt;/a&gt;. The &lt;a href="https://pixellint.org/docs/google-consent-mode/" rel="noopener noreferrer"&gt;Google Consent Mode v2 contract page&lt;/a&gt; lists &lt;code&gt;ad_storage&lt;/code&gt;, &lt;code&gt;analytics_storage&lt;/code&gt;, &lt;code&gt;ad_user_data&lt;/code&gt;, and &lt;code&gt;ad_personalization&lt;/code&gt; and repeats that Consent Mode is not a CMP.&lt;/p&gt;

&lt;p&gt;If your stack also forwards IAB signals into OpenRTB, read the &lt;a href="https://pixellint.org/docs/gpp/" rel="noopener noreferrer"&gt;GPP guide&lt;/a&gt; for header type 3 and section ids. EU inventory still belongs on &lt;code&gt;gdpr&lt;/code&gt; plus &lt;code&gt;gdpr_consent&lt;/code&gt;, not a TC String pasted into &lt;code&gt;gpp&lt;/code&gt;. OpenRTB &lt;code&gt;regs.gpp&lt;/code&gt; and &lt;code&gt;regs.gpp_sid&lt;/code&gt; must agree with what the pixel sent; mismatched section ids are a separate hop problem from passthrough, but the same QA habit applies: compare auction capture to fired tag URL on one page view.&lt;/p&gt;

&lt;p&gt;For regulatory transparency fields arriving on bid requests, &lt;a href="https://rtblint.org/blog/dsa-transparency-2026/" rel="noopener noreferrer"&gt;DSA transparency in OpenRTB&lt;/a&gt; is the server-side counterpart to consent strings on tags. Fix Google tag order first: default deny, passthrough and redaction as set calls, CMP update before conversion tags, then verify granted state on the tag that actually sends ad network requests.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;url_passthrough&lt;/code&gt; keeps click ids on your URLs across same-domain navigations. It does not grant &lt;code&gt;ad_storage&lt;/code&gt;. With &lt;code&gt;ads_data_redaction&lt;/code&gt; true and storage denied, Google ad tags redact click identifiers on network requests anyway.&lt;/p&gt;

&lt;p&gt;A visible &lt;code&gt;gclid&lt;/code&gt; after an internal click is continuity on your site. Google conversion attribution follows consent state at fire time. Audit trigger order, log consent at conversion, and lint the non-Google pixels on the same page so you are not debugging the wrong contract.&lt;/p&gt;

&lt;p&gt;Pixellint is independent of Google and IAB. Rule ids cite their docs because that is where the requirements live, not because this is an official tool.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>google</category>
      <category>privacy</category>
      <category>analytics</category>
    </item>
    <item>
      <title>Mezzanine-Only SIMID Validates for SSAI. Nobody Runs the Interactive Iframe.</title>
      <dc:creator>Aleksander Sekowski</dc:creator>
      <pubDate>Mon, 21 Sep 2026 21:27:41 +0000</pubDate>
      <link>https://dev.to/aleksuix/mezzanine-only-simid-validates-for-ssai-nobody-runs-the-interactive-iframe-3e32</link>
      <guid>https://dev.to/aleksuix/mezzanine-only-simid-validates-for-ssai-nobody-runs-the-interactive-iframe-3e32</guid>
      <description>&lt;p&gt;A CTV campaign ships SIMID for the expandable unit. Trafficking exports VAST 4.1 with a high-bitrate &lt;code&gt;&amp;lt;Mezzanine&amp;gt;&lt;/code&gt;, an HLS &lt;code&gt;&amp;lt;MediaFile delivery="streaming"&amp;gt;&lt;/code&gt;, and &lt;code&gt;&amp;lt;InteractiveCreativeFile apiFramework="SIMID" type="text/html"&amp;gt;&lt;/code&gt;. The SSAI pipeline ingests the mezzanine, stitches the pod, and the stream plays. The interactive layer never appears. No VAST error code fires because every element the schema cares about is present.&lt;/p&gt;

&lt;p&gt;The gap is not missing SIMID markup. It is which &lt;code&gt;&amp;lt;MediaFile&amp;gt;&lt;/code&gt; delivery mode survives on the client after server-side insertion.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Mezzanine and progressive actually mean
&lt;/h2&gt;

&lt;p&gt;VAST 4.1 added &lt;code&gt;&amp;lt;Mezzanine&amp;gt;&lt;/code&gt; so ad stitching platforms can transcode from a single high-quality source (&lt;a href="https://iabtechlab.com/standards/vast/" rel="noopener noreferrer"&gt;VAST 4.1 §1.1.2&lt;/a&gt;). The mezzanine is the input to the stitcher. It is not a promise that the end device receives a progressive MP4 it can hand to a SIMID-capable player alongside an iframe.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&amp;lt;MediaFile delivery="streaming"&amp;gt;&lt;/code&gt; is the correct shape for HLS or DASH after stitch. &lt;code&gt;&amp;lt;MediaFile delivery="progressive"&amp;gt;&lt;/code&gt; is the shape a client player downloads and decodes locally. SIMID 1.0 §3.4 still requires a real video or audio &lt;code&gt;&amp;lt;MediaFile&amp;gt;&lt;/code&gt; sibling to the interactive file because the iframe does not replace linear media. On SSAI inventory the stitcher consumes the mezzanine and emits a stream. The device player may never see a progressive file at all.&lt;/p&gt;

&lt;p&gt;When the only &lt;code&gt;&amp;lt;MediaFile&amp;gt;&lt;/code&gt; in the tag is &lt;code&gt;delivery="streaming"&lt;/code&gt; and the mezzanine sits next to it, the document satisfies many XSD checks and the &lt;a href="https://vastlint.org/docs/simid-rules/SIMID-1.0-simid-mediafile-required/" rel="noopener noreferrer"&gt;&lt;code&gt;SIMID-1.0-simid-mediafile-required&lt;/code&gt;&lt;/a&gt; rule (there is a media node). The SSAI-specific failure is different: there is no progressive asset for a client-side player to attach SIMID to after the break is already in the manifest. The interactive HTML URL is in the XML. Nothing in the SSAI path loads it.&lt;/p&gt;

&lt;p&gt;That is what &lt;a href="https://vastlint.org/docs/simid-rules/SIMID-1.0-simid-ssai-no-client/" rel="noopener noreferrer"&gt;&lt;code&gt;SIMID-1.0-simid-ssai-no-client&lt;/code&gt;&lt;/a&gt; flags at info severity: linear SIMID with a mezzanine present and no progressive &lt;code&gt;&amp;lt;MediaFile&amp;gt;&lt;/code&gt;. It is easy to dismiss as informational until you realize the tag was authored for dynamic ad insertion, not for a browser that could have run SIMID on a downloaded MP4.&lt;/p&gt;

&lt;p&gt;A minimal broken shape looks like the fixture vastlint uses:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;MediaFiles&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;MediaFile&lt;/span&gt; &lt;span class="na"&gt;delivery=&lt;/span&gt;&lt;span class="s"&gt;"streaming"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"application/x-mpegURL"&lt;/span&gt; &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"1920"&lt;/span&gt; &lt;span class="na"&gt;height=&lt;/span&gt;&lt;span class="s"&gt;"1080"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;![CDATA[https://cdn.example.com/ad.m3u8]]&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/MediaFile&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;Mezzanine&lt;/span&gt; &lt;span class="na"&gt;delivery=&lt;/span&gt;&lt;span class="s"&gt;"progressive"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"video/mp4"&lt;/span&gt; &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"1920"&lt;/span&gt; &lt;span class="na"&gt;height=&lt;/span&gt;&lt;span class="s"&gt;"1080"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;![CDATA[https://cdn.example.com/ad-mezzanine.mp4]]&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/Mezzanine&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;InteractiveCreativeFile&lt;/span&gt; &lt;span class="na"&gt;apiFramework=&lt;/span&gt;&lt;span class="s"&gt;"SIMID"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"text/html"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;![CDATA[https://creative.example.com/simid.html]]&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/InteractiveCreativeFile&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/MediaFiles&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fix for mixed SSAI and SIMID is operational, not a one-line attribute swap: keep a progressive &lt;code&gt;&amp;lt;MediaFile&amp;gt;&lt;/code&gt; in the tag for client players that still execute SIMID after stitch, or stop selling SIMID on pure SSAI paths where no client runtime exists (the same constraint that killed VPAID in the stitcher). The &lt;a href="https://vastlint.org/docs/vpaid-rules/" rel="noopener noreferrer"&gt;VPAID rule pack&lt;/a&gt; documents why script and iframe APIs fail server-side; SIMID is sandboxed, but it still needs a client.&lt;/p&gt;

&lt;h2&gt;
  
  
  How you catch it
&lt;/h2&gt;

&lt;p&gt;Paste the live tag URL into the &lt;a href="https://vastlint.org/tester/" rel="noopener noreferrer"&gt;VAST tag tester&lt;/a&gt;. It fetches the chain, previews the linear media, and lists tracking URLs so you can see whether the break is empty or only the video layer rendered. For wrapped tags, run the same URL through the &lt;a href="https://vastlint.org/inspect/" rel="noopener noreferrer"&gt;VAST inspector&lt;/a&gt; hop by hop. A mezzanine-only SIMID shape on the final InLine still breaks interactivity even when the wrapper hops are clean.&lt;/p&gt;

&lt;p&gt;SIMID handshake debugging belongs in the &lt;a href="https://iab-tech-lab-vast-tester.vastlint.org/" rel="noopener noreferrer"&gt;IAB-style VAST tester&lt;/a&gt;. That workbench loads sample creatives, logs host transport, and exercises &lt;code&gt;postMessage&lt;/code&gt; the way a spec-compliant player would. It is an independent fork, not an IAB Tech Lab product. If the workbench never enters SIMID mode on a pasted tag but the MP4 or HLS preview plays, delivery mode and &lt;code&gt;apiFramework&lt;/code&gt; are the first suspects before you open the HTML creative.&lt;/p&gt;

&lt;p&gt;CLI check on the fixture above:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;vastlint check simid-ssai.xml
&lt;span class="go"&gt;simid-ssai.xml  VAST 4.1
&lt;/span&gt;&lt;span class="gp"&gt;  info     Linear SIMID creative has a &amp;lt;Mezzanine&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;but no progressive
&lt;span class="gp"&gt;           &amp;lt;MediaFile&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; an SSAI stitcher cannot execute the SIMID iframe
&lt;span class="go"&gt;           SIMID-1.0-simid-ssai-no-client
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Info severity does not mean optional on SIMID campaigns. It means the schema alone will not save you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to go next
&lt;/h2&gt;

&lt;p&gt;If the buy is CTV and the trafficking template still emits VPAID next to SIMID, read the &lt;a href="https://vastlint.org/docs/vast-examples/vpaid-deprecated-ctv/" rel="noopener noreferrer"&gt;VPAID-on-CTV example&lt;/a&gt;: deprecated script media and SSAI are incompatible for different reasons, but the revenue symptom is the same (video without the interactive or verification layer you sold). VAST 4.2 added &lt;a href="https://vastlint.org/docs/simid-rules/SIMID-1.0-simid-interactive-start/" rel="noopener noreferrer"&gt;&lt;code&gt;interactiveStart&lt;/code&gt;&lt;/a&gt; tracking for the moment SIMID takes control; a tag that never mounts SIMID will not fire that event either, which shows up as a measurement gap rather than a parser failure.&lt;/p&gt;

&lt;p&gt;vastlint is independent of IAB Tech Lab. The rule ids above cite SIMID and VAST because that is where the requirements live, not because this is an official tool.&lt;/p&gt;

</description>
      <category>vast</category>
      <category>simid</category>
      <category>advertising</category>
      <category>javascript</category>
    </item>
    <item>
      <title>mincpmpersec Without poddur Validates as JSON. OpenRTB Prices Dynamic Pods.</title>
      <dc:creator>Aleksander Sekowski</dc:creator>
      <pubDate>Mon, 21 Sep 2026 21:26:40 +0000</pubDate>
      <link>https://dev.to/aleksuix/mincpmpersec-without-poddur-validates-as-json-openrtb-prices-dynamic-pods-4foe</link>
      <guid>https://dev.to/aleksuix/mincpmpersec-without-poddur-validates-as-json-openrtb-prices-dynamic-pods-4foe</guid>
      <description>&lt;p&gt;A CTV pre-roll template ships &lt;code&gt;"mincpmpersec": 5.0&lt;/code&gt; on every video imp. There is no &lt;code&gt;poddur&lt;/code&gt;, no &lt;code&gt;maxseq&lt;/code&gt;, no sibling imp with a matching &lt;code&gt;podid&lt;/code&gt;. The request parses. The exchange forwards it. A buyer's pod optimizer reads five dollars per second and bids as if the slot were part of a dynamic pod.&lt;/p&gt;

&lt;p&gt;It is one linear ad in a fixed break. OpenRTB 2.6 defines &lt;code&gt;mincpmpersec&lt;/code&gt; as a price floor for the &lt;strong&gt;dynamic&lt;/strong&gt; portion of a video ad pod, relative to the duration of bids an advertiser may submit. Without &lt;code&gt;poddur&lt;/code&gt; or &lt;code&gt;maxseq&lt;/code&gt;, that float is pod vocabulary on inventory that never declared a pod.&lt;/p&gt;

&lt;p&gt;IAB's Redefining Media Types (RMT) standard plans to classify impressions from fields that already exist on the bid request, including ad format via &lt;code&gt;plcmt&lt;/code&gt;, &lt;code&gt;linearity&lt;/code&gt;, and &lt;code&gt;mtype&lt;/code&gt;. Pod economics use a parallel cluster: &lt;code&gt;poddur&lt;/code&gt;, &lt;code&gt;maxseq&lt;/code&gt;, &lt;code&gt;mincpmpersec&lt;/code&gt;, &lt;code&gt;slotinpod&lt;/code&gt;. When a trafficking snippet copies the per-second floor from a pod template onto every linear imp, format classifiers and pod bidders inherit conflicting stories from the same JSON object.&lt;/p&gt;

&lt;h2&gt;
  
  
  What mincpmpersec actually binds to
&lt;/h2&gt;

&lt;p&gt;OpenRTB added pod bidding fields in 2.6. The ones that matter for this bug:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;poddur&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Total seconds advertisers may fill in a &lt;strong&gt;dynamic&lt;/strong&gt; pod (or the dynamic half of a hybrid pod)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;maxseq&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Maximum number of ads served into a &lt;strong&gt;dynamic&lt;/strong&gt; pod&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;mincpmpersec&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Minimum CPM &lt;strong&gt;per second&lt;/strong&gt; for the dynamic portion, relative to bid duration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;slotinpod&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Guaranteed slot position when the seller can commit to it&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;minduration&lt;/code&gt; and &lt;code&gt;maxduration&lt;/code&gt; bound individual creatives. &lt;code&gt;poddur&lt;/code&gt; bounds the whole break. &lt;code&gt;mincpmpersec&lt;/code&gt; is not a generic CPM-per-second knob for linear video; the spec text ties it to dynamic pod filling.&lt;/p&gt;

&lt;p&gt;A valid dynamic pod shape includes context, for example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"video"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mimes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"video/mp4"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"protocols"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"poddur"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"maxseq"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mincpmpersec"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;5.0&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The broken shape I see in the wild drops the pod container and keeps the floor:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"video"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mimes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"video/mp4"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"protocols"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"minduration"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"maxduration"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mincpmpersec"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;5.0&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Schema validation often passes because &lt;code&gt;mincpmpersec&lt;/code&gt; is an optional float on Video. Nothing in JSON Schema requires &lt;code&gt;poddur&lt;/code&gt; when &lt;code&gt;mincpmpersec&lt;/code&gt; is present.&lt;/p&gt;

&lt;h2&gt;
  
  
  How you catch it
&lt;/h2&gt;

&lt;p&gt;I maintain &lt;a href="https://rtblint.org" rel="noopener noreferrer"&gt;RTBlint&lt;/a&gt;, an open source OpenRTB linter. Semantic checks run after schema validation. The pod warning is explicit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;rtblint validate &lt;span class="nt"&gt;--type&lt;/span&gt; request @bid.json
&lt;span class="go"&gt;  warning openrtb.video.pod.mincpmpersec_without_pod_context
&lt;/span&gt;&lt;span class="gp"&gt;          mincpmpersec is meant for the dynamic portion of a video ad pod;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;it is present
&lt;span class="go"&gt;          without poddur or maxseq, which normally accompany a dynamic pod.
          imp[0].video.mincpmpersec
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rule id: &lt;code&gt;openrtb.video.pod.mincpmpersec_without_pod_context&lt;/code&gt;. It is a warning, not a hard error, because a single imp can represent an entire dynamic pod with no sibling imps to correlate (OpenRTB allows that shape when &lt;code&gt;poddur&lt;/code&gt; or &lt;code&gt;maxseq&lt;/code&gt; is present). The failure mode here is the opposite: pod pricing language without any pod duration or sequence cap.&lt;/p&gt;

&lt;p&gt;Paste the imp into the &lt;a href="https://rtblint.org/docs/openrtb/" rel="noopener noreferrer"&gt;OpenRTB docs hub&lt;/a&gt; and compare against the dated snapshot you actually validate (&lt;code&gt;2.6-202606&lt;/code&gt; versus &lt;code&gt;2.6-202303&lt;/code&gt; changes pod-adjacent fields over time). Version drift on pod objects is easy to miss when only the top-level &lt;code&gt;openrtb&lt;/code&gt; version string moves.&lt;/p&gt;

&lt;p&gt;For live and event inventory, the same request often carries &lt;code&gt;rqddurs&lt;/code&gt; arrays for dead-air control. The &lt;a href="https://rtblint.org/blog/live-event-ad-insertion-openrtb/" rel="noopener noreferrer"&gt;live event ad insertion walkthrough&lt;/a&gt; documents the mutual exclusivity between &lt;code&gt;rqddurs&lt;/code&gt; and &lt;code&gt;minduration&lt;/code&gt;/&lt;code&gt;maxduration&lt;/code&gt;, plus this orphan &lt;code&gt;mincpmpersec&lt;/code&gt; pattern, because rehearsal traffic copies pod defaults from on-demand templates.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why RMT cares
&lt;/h2&gt;

&lt;p&gt;RMT's operational layer wants binary answers from fields sellers already populate inconsistently. Ad format is mapped to &lt;code&gt;plcmt&lt;/code&gt;, &lt;code&gt;linearity&lt;/code&gt;, and &lt;code&gt;mtype&lt;/code&gt;. That mapping assumes the video object describes one impression honestly.&lt;/p&gt;

&lt;p&gt;An orphan &lt;code&gt;mincpmpersec&lt;/code&gt; does not flip &lt;code&gt;plcmt&lt;/code&gt;, but it does tell downstream systems the impression participates in dynamic pod pricing. Buyers that rank opportunities by effective CPM per second will compute a floor that the seller never intended for a single-slot linear break. Reporting that joins bid requests to delivery logs will bucket the imp with podded CTV paths.&lt;/p&gt;

&lt;p&gt;Fix the template, not the bid: either add &lt;code&gt;poddur&lt;/code&gt; or &lt;code&gt;maxseq&lt;/code&gt; when the break is genuinely dynamic, or remove &lt;code&gt;mincpmpersec&lt;/code&gt; and use &lt;code&gt;bidfloor&lt;/code&gt; / &lt;code&gt;durfloors&lt;/code&gt; for linear floors. If you are migrating snapshots, read &lt;a href="https://rtblint.org/docs/openrtb-versions/" rel="noopener noreferrer"&gt;which OpenRTB dated train you are on&lt;/a&gt; before you diff pod fields against an old integration guide.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to go next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://rtblint.org/docs/what-rtblint-checks/" rel="noopener noreferrer"&gt;What RTBlint checks&lt;/a&gt; separates schema errors from semantic pod and privacy rules.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://rtblint.org/blog/delisted-ctv-apps-prebid-blocklist-gaps/" rel="noopener noreferrer"&gt;Delisted CTV apps and blocklist gaps&lt;/a&gt; is a reminder that CTV bid requests carry app identity fields that must agree with trust lists; pod mislabeling and bundle mismatches often show up in the same trafficking export.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Run pod-shaped imps through validation in CI before you enable a new ad-server build. The HTTP 200 from your exchange partner does not mean the Video object tells one story.&lt;/p&gt;

</description>
      <category>openrtb</category>
      <category>adtech</category>
      <category>video</category>
      <category>ctv</category>
    </item>
    <item>
      <title>Pinterest CAPI Requires event_id. Meta Only Warns on the Same Field.</title>
      <dc:creator>Aleksander Sekowski</dc:creator>
      <pubDate>Mon, 21 Sep 2026 21:25:10 +0000</pubDate>
      <link>https://dev.to/aleksuix/pinterest-capi-requires-eventid-meta-only-warns-on-the-same-field-3ek8</link>
      <guid>https://dev.to/aleksuix/pinterest-capi-requires-eventid-meta-only-warns-on-the-same-field-3ek8</guid>
      <description>&lt;p&gt;A thank-you page fires the browser Pinterest tag and a server POST to &lt;code&gt;api.pinterest.com/v5/ad_accounts/{id}/events&lt;/code&gt;. Ads Manager on Meta looks healthy. Pinterest reporting shows two checkouts for one order.&lt;/p&gt;

&lt;p&gt;The usual suspect is a duplicate GTM tag. The usual contract gap is &lt;code&gt;event_id&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"data"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"event_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"checkout"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"action_source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"web"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"event_time"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1770000000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"user_data"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"em"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"6a7a73766627eb611720883d5a11cc62b5bfee237b00a6658d78c50032ec4aee"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"client_ip_address"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"203.0.113.42"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"client_user_agent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Mozilla/5.0"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"custom_data"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"129.99"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"currency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"USD"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That body is structurally close to what many teams already send to Meta's Graph &lt;code&gt;events&lt;/code&gt; edge. It is not complete for Pinterest. &lt;a href="https://developers.pinterest.com/docs/track-conversions/track-conversions-in-the-api/" rel="noopener noreferrer"&gt;Pinterest documents &lt;code&gt;event_id&lt;/code&gt; on each event&lt;/a&gt; so the API can deduplicate against the Pinterest tag on the same conversion. Without it, the server event and the tag event are two rows.&lt;/p&gt;

&lt;p&gt;Meta is different on the same field. &lt;a href="https://developers.facebook.com/docs/marketing-api/conversions-api/parameters/server-event" rel="noopener noreferrer"&gt;Meta recommends &lt;code&gt;event_id&lt;/code&gt;&lt;/a&gt; for deduplication between the pixel and Conversions API. Missing &lt;code&gt;event_id&lt;/code&gt; on Meta is a warning, not a hard failure. The POST can still return 200 and the event can still land. Teams that wired dedup only because Meta's Events Manager nagged them often never add the field to the Pinterest branch of a shared template.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Pinterest actually requires
&lt;/h2&gt;

&lt;p&gt;Pinterest's v5 events payload wraps one or more events in &lt;code&gt;data[]&lt;/code&gt;. Each object needs &lt;code&gt;event_name&lt;/code&gt;, &lt;code&gt;action_source&lt;/code&gt;, &lt;code&gt;event_id&lt;/code&gt;, &lt;code&gt;event_time&lt;/code&gt;, and &lt;code&gt;user_data&lt;/code&gt; with at least one match key (&lt;code&gt;em&lt;/code&gt;, &lt;code&gt;hashed_maids&lt;/code&gt;, or IP plus user agent).&lt;/p&gt;

&lt;p&gt;&lt;code&gt;action_source&lt;/code&gt; for web is lowercase &lt;code&gt;web&lt;/code&gt;, not Meta's &lt;code&gt;website&lt;/code&gt; and not Snap's &lt;code&gt;WEB&lt;/code&gt;. Copying Meta's enum verbatim is a silent routing bug before dedup even matters.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;event_time&lt;/code&gt; is Unix seconds, ten digits, like Meta. Thirteen digits are milliseconds and belong on LinkedIn's clock, not Pinterest's.&lt;/p&gt;

&lt;p&gt;Standard event names are lowercase strings such as &lt;code&gt;checkout&lt;/code&gt;, &lt;code&gt;add_to_cart&lt;/code&gt;, and &lt;code&gt;page_visit&lt;/code&gt;. Meta's &lt;code&gt;Purchase&lt;/code&gt; string is not Pinterest's purchase event. That naming split is its own failure mode; the short version is that &lt;a href="https://pixellint.org/blog/pinterest-checkout-is-not-purchase/" rel="noopener noreferrer"&gt;checkout is not Purchase&lt;/a&gt; on the wire.&lt;/p&gt;

&lt;p&gt;For dedup specifically, mint one stable id per conversion (order id, checkout token, UUID generated once on the thank-you page) and send the same value on the Pinterest tag and the CAPI POST. Retries should reuse the id; new ids on retry look like new conversions.&lt;/p&gt;

&lt;h2&gt;
  
  
  How you catch it before finance does
&lt;/h2&gt;

&lt;p&gt;I maintain &lt;a href="https://github.com/aleksUIX/pixellint" rel="noopener noreferrer"&gt;Pixellint&lt;/a&gt;, an open source linter for pixels and conversion API payloads. The Pinterest Conversions API pack (&lt;code&gt;vendor/pinterest-conversions-api&lt;/code&gt;) contracts &lt;code&gt;data[]&lt;/code&gt; against Pinterest's published parameter docs.&lt;/p&gt;

&lt;p&gt;On the payload above, missing &lt;code&gt;event_id&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;pixellint validate json @pinterest-checkout.json
&lt;span class="go"&gt;rulepack: vendor/pinterest-conversions-api (vendor: pinterest)
  error   vendor.pinterest-conversions-api.body.event_id.missing
          `event_id` is missing. It identifies the event for deduplication
          against the Pinterest tag.
    fix:  Send a stable id per conversion, such as an order number.
    docs: https://developers.pinterest.com/docs/track-conversions/track-conversions-in-the-api/
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run the same file against Meta's pack and you get a warning, exit code 0:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;rulepack: vendor/meta-conversions-api (vendor: meta)
  warning vendor.meta-conversions-api.body.event_id.missing
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That asymmetry is why CI that only fails on Meta errors misses Pinterest. Lint both packs on the same canonical event object, or store one internal event and map fields at the vendor edge.&lt;/p&gt;

&lt;p&gt;Paste the body into the &lt;a href="https://pixellint.org/" rel="noopener noreferrer"&gt;browser playground&lt;/a&gt; if you do not want a local install. Point the CLI at a saved URL that hits &lt;code&gt;api.pinterest.com&lt;/code&gt; when you are testing the full path:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cargo &lt;span class="nb"&gt;install &lt;/span&gt;pixellint
&lt;span class="c"&gt;# or: npm install pixellint&lt;/span&gt;
pixellint validate json @pinterest-checkout.json &lt;span class="nt"&gt;--rulepack&lt;/span&gt; vendor/pinterest-conversions-api
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The field table and rule ids live on the &lt;a href="https://pixellint.org/packs/pinterest-conversions-api/" rel="noopener noreferrer"&gt;Pinterest Conversions API pack page&lt;/a&gt;. For how the img tag and the POST share clocks and names, read the &lt;a href="https://pixellint.org/docs/pinterest-tag-and-capi/" rel="noopener noreferrer"&gt;Pinterest tag and CAPI playbook&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to go next
&lt;/h2&gt;

&lt;p&gt;If one JSON blob fans out to Meta, Pinterest, Snap, and Reddit from the same Cloud Function, read &lt;a href="https://pixellint.org/docs/one-capi-json-cannot-serve-every-vendor/" rel="noopener noreferrer"&gt;one CAPI JSON cannot serve every vendor&lt;/a&gt;: the adapter belongs at the edge, not in a shared schema everyone "mostly" accepts.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pixellint.org/docs/timestamp-units/" rel="noopener noreferrer"&gt;Event time units&lt;/a&gt; maps seconds, milliseconds, microseconds, and ISO 8601 across vendors so you are not guessing from one sample payload. &lt;a href="https://pixellint.org/docs/conversions/" rel="noopener noreferrer"&gt;Conversions API payloads&lt;/a&gt; covers batch shapes, when to fire from the order server, and how dedup ids should survive retries.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;p&gt;Pinterest requires &lt;code&gt;event_id&lt;/code&gt; on every CAPI event for tag dedup. Meta recommends the same field but ingests without it. A shared worker that only enforces Meta's warnings will double-count on Pinterest while both endpoints return success. Send lowercase &lt;code&gt;web&lt;/code&gt;, lowercase &lt;code&gt;checkout&lt;/code&gt;, seconds on &lt;code&gt;event_time&lt;/code&gt;, and the same dedup id on the tag and the POST.&lt;/p&gt;

&lt;p&gt;Pixellint is independent of Pinterest and Meta. The rule ids cite their docs because that is where the requirements live, not because this is an official vendor tool.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>advertising</category>
      <category>api</category>
      <category>analytics</category>
    </item>
    <item>
      <title>VAST Extension type="pos" With a plcmt Child Still Validates. Stitchers Read type.</title>
      <dc:creator>Aleksander Sekowski</dc:creator>
      <pubDate>Sat, 19 Sep 2026 00:03:31 +0000</pubDate>
      <link>https://dev.to/aleksuix/vast-extension-typepos-with-a-plcmt-child-still-validates-stitchers-read-type-4j0a</link>
      <guid>https://dev.to/aleksuix/vast-extension-typepos-with-a-plcmt-child-still-validates-stitchers-read-type-4j0a</guid>
      <description>&lt;p&gt;A pause campaign ships. OpenRTB carried &lt;code&gt;video.plcmt=5&lt;/code&gt;. The VAST InLine looks fine in a schema validator. Format reporting still shows squeezeback geometry, or nothing at all, because one extension copied the wrong attribute onto the wrapper.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;Extension&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"pos"&lt;/span&gt; &lt;span class="na"&gt;ext=&lt;/span&gt;&lt;span class="s"&gt;"adcom"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;plcmt&amp;gt;&lt;/span&gt;5&lt;span class="nt"&gt;&amp;lt;/plcmt&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/Extension&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The integer &lt;code&gt;5&lt;/code&gt; is correct for Pause. The child tag name is correct for a placement subtype. The &lt;code&gt;type&lt;/code&gt; attribute says this block is a &lt;strong&gt;pos&lt;/strong&gt; signal. Anything that trusts &lt;code&gt;type&lt;/code&gt; before it reads children will treat the payload as screen position, not as plcmt, or it will drop the block when the inner tag does not match the declared signal.&lt;/p&gt;

&lt;p&gt;IAB Tech Lab finalized CTV Ad Portfolio signaling in July 2026. The VAST side round-trips four AdCOM enumerations inside &lt;code&gt;&amp;lt;Extension ext="adcom"&amp;gt;&lt;/code&gt;: &lt;code&gt;plcmt&lt;/code&gt;, &lt;code&gt;pos&lt;/code&gt;, &lt;code&gt;playbackmethod&lt;/code&gt;, and &lt;code&gt;attr&lt;/code&gt;. Each signal gets its own extension node. The &lt;code&gt;type&lt;/code&gt; attribute names which signal the extension carries. The child element repeats that name and holds the integer value.&lt;/p&gt;

&lt;p&gt;The canonical shape looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;Extension&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"plcmt"&lt;/span&gt; &lt;span class="na"&gt;ext=&lt;/span&gt;&lt;span class="s"&gt;"adcom"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;plcmt&amp;gt;&lt;/span&gt;5&lt;span class="nt"&gt;&amp;lt;/plcmt&amp;gt;&amp;lt;/Extension&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;Extension&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"pos"&lt;/span&gt; &lt;span class="na"&gt;ext=&lt;/span&gt;&lt;span class="s"&gt;"adcom"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;pos&amp;gt;&lt;/span&gt;7&lt;span class="nt"&gt;&amp;lt;/pos&amp;gt;&amp;lt;/Extension&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;Extension&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"playbackmethod"&lt;/span&gt; &lt;span class="na"&gt;ext=&lt;/span&gt;&lt;span class="s"&gt;"adcom"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;playbackmethod&amp;gt;&lt;/span&gt;9&lt;span class="nt"&gt;&amp;lt;/playbackmethod&amp;gt;&amp;lt;/Extension&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That pattern exists because the VAST response outlives the bid request. SSAI stitchers and measurement vendors downstream of the auction often never see OpenRTB. They learn what was sold from the creative envelope. Copy-paste from an older template, a merge conflict in a trafficking UI, or a single swapped attribute on export is enough to produce a tag where every integer is in range and the labels disagree.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why type and tag name are not redundant
&lt;/h2&gt;

&lt;p&gt;Schema validation cares about nesting and text content. It does not require &lt;code&gt;type="plcmt"&lt;/code&gt; when the only child is &lt;code&gt;&amp;lt;plcmt&amp;gt;&lt;/code&gt;. Many pipelines therefore green-light the mistake.&lt;/p&gt;

&lt;p&gt;Production consumers are not symmetric. Portfolio guidance treats &lt;code&gt;type&lt;/code&gt; as the selector. Rule &lt;code&gt;VAST-4.4-adcom-extension-type-mismatch&lt;/code&gt; flags exactly this case: the declared AdCOM signal in &lt;code&gt;type&lt;/code&gt; does not match the payload element name. The rule text notes that downstream stitchers key off &lt;code&gt;type&lt;/code&gt; to decide what to read.&lt;/p&gt;

&lt;p&gt;That is a different failure mode from inconsistent &lt;strong&gt;values&lt;/strong&gt; across signals. You can ship &lt;code&gt;plcmt=5&lt;/code&gt; with &lt;code&gt;playbackmethod=1&lt;/code&gt; and every extension wrapper can be structurally perfect while the integers still describe incompatible formats. Value-level mismatch is its own QA problem. The bug here is mislabeling at the wrapper: the pause integer rode inside a node advertised as geometry.&lt;/p&gt;

&lt;p&gt;A related defect is &lt;code&gt;type="loudness"&lt;/code&gt; or any string outside the four signal names, which triggers &lt;code&gt;VAST-4.4-adcom-extension-unknown-signal&lt;/code&gt;. The type-or-child swap is quieter because both strings are real AdCOM field names.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I catch it before traffic
&lt;/h2&gt;

&lt;p&gt;I start with the &lt;a href="https://vastlint.org/tester/" rel="noopener noreferrer"&gt;VAST tag tester&lt;/a&gt;. Paste the live ad tag URL or the resolved InLine XML. It fetches wrappers when needed, previews the NonLinear or video asset, and runs the portfolio rule pack alongside generic VAST checks. For a pause or overlay buy I confirm both render path and the AdCOM extensions in the same pass, not just that an MP4 bit rate exists.&lt;/p&gt;

&lt;p&gt;Portfolio tags often chain. When the defect might live in a wrapper hop, I run the same URL through the &lt;a href="https://vastlint.org/inspect/" rel="noopener noreferrer"&gt;VAST inspector&lt;/a&gt;. It walks each redirect, validates every hop, and shows where the InLine with extensions actually lands.&lt;/p&gt;

&lt;p&gt;For NonLinear SIMID and IAB sample layouts, I also open the &lt;a href="https://iab-tech-lab-vast-tester.vastlint.org/" rel="noopener noreferrer"&gt;IAB-style VAST tester&lt;/a&gt; (SIMID studio and sample creatives). That workbench is an independent fork, not an IAB Tech Lab product. Live URL QA still belongs on vastlint.org/tester/; wrapper debugging stays on the inspector.&lt;/p&gt;

&lt;p&gt;After browser QA I keep &lt;code&gt;vastlint check&lt;/code&gt; on fixtures in CI. The type mismatch warning survives automation better than eyeballing four nearly identical extension blocks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to read the contract
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://vastlint.org/docs/rules/VAST-4.4-adcom-extension-type-mismatch/" rel="noopener noreferrer"&gt;type mismatch rule reference&lt;/a&gt; shows the wrong and right markup side by side. Pair it with &lt;a href="https://vastlint.org/docs/rules/VAST-4.4-adcom-extension-unknown-signal/" rel="noopener noreferrer"&gt;unknown signal&lt;/a&gt; when &lt;code&gt;type&lt;/code&gt; is not one of the four AdCOM names at all.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://vastlint.org/docs/ctv-ad-portfolio/" rel="noopener noreferrer"&gt;CTV Ad Portfolio guide&lt;/a&gt; maps Pause, Screensaver, Overlay, Squeezeback, and In-Scene to the signal combinations each format allows. Use it when you need to verify that the integers inside correctly labeled extensions still belong to the same row in the table.&lt;/p&gt;

&lt;p&gt;Legacy players that never implemented VAST 4.x NonLinear still expect portfolio creative inside &lt;code&gt;&amp;lt;Extension type="ctv_ad_portfolio"&amp;gt;&lt;/code&gt;. That path binds by creative id and carries its own required MediaFiles rules. The &lt;a href="https://vastlint.org/blog/ctv-ad-portfolio-vast-2-0-extensions/" rel="noopener noreferrer"&gt;VAST 2.0 portfolio extensions write-up&lt;/a&gt; is the reference when your QA stack only understands 2.0-shaped XML. For how finalized signaling relates to the VAST 4.4 draft schema, see &lt;a href="https://vastlint.org/blog/ctv-ad-portfolio-signaling-vast-validation/" rel="noopener noreferrer"&gt;signaling versus schema validation&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Do not treat a converting draft-schema blog post as the only checklist. The failure mode in this article is visible on 4.2 tags today, which is where IAB's own portfolio examples still declare version.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing the loop with trafficking
&lt;/h2&gt;

&lt;p&gt;When format mix dashboards disagree with the deal line, grep the resolved InLine for &lt;code&gt;&amp;lt;Extension&lt;/code&gt; blocks before you reopen the player integration. One swapped &lt;code&gt;type&lt;/code&gt; attribute is enough for pause spend to inherit overlay position reporting, with no player error to paste into a ticket.&lt;/p&gt;

&lt;p&gt;Fix the wrapper, redeploy, and re-run the same URL in the tester so the extensions you see match the plcmt value the buyer actually purchased.&lt;/p&gt;

</description>
      <category>vast</category>
      <category>ctv</category>
      <category>xml</category>
      <category>advertising</category>
    </item>
    <item>
      <title>Publisher-Stage ARTF With bid_response on the Envelope Stages the Agent Wrong</title>
      <dc:creator>Aleksander Sekowski</dc:creator>
      <pubDate>Fri, 18 Sep 2026 00:01:47 +0000</pubDate>
      <link>https://dev.to/aleksuix/publisher-stage-artf-with-bidresponse-on-the-envelope-stages-the-agent-wrong-1mig</link>
      <guid>https://dev.to/aleksuix/publisher-stage-artf-with-bidresponse-on-the-envelope-stages-the-agent-wrong-1mig</guid>
      <description>&lt;p&gt;An SSP-side ARTF host labels the extension point &lt;code&gt;LIFECYCLE_PUBLISHER_BID_REQUEST&lt;/code&gt;, keeps yesterday's &lt;code&gt;bid_response&lt;/code&gt; on the envelope for debugging, and forwards &lt;code&gt;ACTIVATE_DEALS&lt;/code&gt; mutations against the publisher &lt;code&gt;bid_request&lt;/code&gt;. gRPC returns OK. The agent believes no buyer has answered yet. The orchestrator is staring at a seatbid it never told the agent existed.&lt;/p&gt;

&lt;p&gt;That is not a deal-activation bug in the agent. It is a reused envelope whose lifecycle name and its OpenRTB members disagree.&lt;/p&gt;

&lt;p&gt;The IAB Tech Lab &lt;a href="https://iabtechlab.com/standards/artf/" rel="noopener noreferrer"&gt;Agentic Real Time Framework&lt;/a&gt; (ARTF) puts a containerized agent beside an SSP, exchange, or DSP. The host sends an &lt;code&gt;RTBRequest&lt;/code&gt; with a &lt;code&gt;lifecycle&lt;/code&gt; value, a &lt;code&gt;tmax&lt;/code&gt; budget in milliseconds, the OpenRTB object under mutation, and &lt;code&gt;applicable_intents&lt;/code&gt;. The agent returns &lt;code&gt;Mutation&lt;/code&gt; messages with semantic paths. The orchestrator applies what it accepts and only then forwards the auction.&lt;/p&gt;

&lt;p&gt;Publisher-stage calls run before any DSP has answered. Response-stage calls expect a &lt;code&gt;bid_response&lt;/code&gt; member when intents such as &lt;code&gt;BID_SHADE&lt;/code&gt; need seat and bid ids. The lifecycle enum names the stage. It does not delete extra members you left on the JSON from an earlier hop.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the stage label actually promises
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;LIFECYCLE_PUBLISHER_BID_REQUEST&lt;/code&gt; means the extension point sits on the outbound publisher request. No DSP answer belongs on that wire yet. If &lt;code&gt;bid_response&lt;/code&gt; is present anyway, RTBlint reports &lt;a href="https://rtblint.org/docs/rule-reference/artf-lifecycle-payload_unexpected/" rel="noopener noreferrer"&gt;&lt;code&gt;artf.lifecycle.payload_unexpected&lt;/code&gt;&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ep-1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tmax"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"lifecycle"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"LIFECYCLE_PUBLISHER_BID_REQUEST"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"bid_request"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"a-1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"imp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"imp-1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"banner"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"w"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"h"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;250&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"bid_response"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"a-1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"seatbid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"seat"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dsp-1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"bid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"bid-abc"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"impid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"imp-1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;2.5&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The envelope declares publisher bid request stage and carries a full &lt;code&gt;bid_response&lt;/code&gt;. At that stage no DSP should have answered. The validator treats it as a warning, not a hard error, because a private host might deliberately pass prior context. For a default orchestrator, the warning is the point: the agent's model of where it sits in the auction is wrong even when protobuf JSON parses cleanly.&lt;/p&gt;

&lt;p&gt;The inverse failure, response stage without &lt;code&gt;bid_response&lt;/code&gt;, is &lt;a href="https://rtblint.org/docs/rule-reference/artf-lifecycle-payload_mismatch/" rel="noopener noreferrer"&gt;&lt;code&gt;artf.lifecycle.payload_mismatch&lt;/code&gt;&lt;/a&gt;. That one blocks shading paths that need a bid id the envelope never included. Together they are the same contract: lifecycle tells the agent which intents make sense; the carried OpenRTB members have to match that story.&lt;/p&gt;

&lt;p&gt;Reusing one JSON blob across hops is how both show up in production. A team logs the full auction state, copies the object into the next ARTF fixture, and forgets to drop &lt;code&gt;bid_response&lt;/code&gt; when the lifecycle moves back to publisher stage. gRPC still accepts the bytes.&lt;/p&gt;

&lt;h2&gt;
  
  
  tmax on the same envelope is a separate unit trap
&lt;/h2&gt;

&lt;p&gt;ARTF &lt;code&gt;tmax&lt;/code&gt; is milliseconds budgeted for the mutation RPC inside the auction, not the OpenRTB bid timeout copied from &lt;code&gt;bid_request.tmax&lt;/code&gt;. Reference samples use roughly 120 to 150 ms. Values above 1000 ms trigger &lt;a href="https://rtblint.org/docs/rule-reference/artf-tmax-implausible/" rel="noopener noreferrer"&gt;&lt;code&gt;artf.tmax.implausible&lt;/code&gt;&lt;/a&gt;: the extension point runs inside an exchange timeout, and a budget measured in seconds usually means someone pasted an auction limit without converting units.&lt;/p&gt;

&lt;p&gt;That warning does not fail the envelope by itself. It pairs badly with a mislabeled lifecycle: you can burn seconds of agent time on a call that still carries the wrong stage label and a ghost &lt;code&gt;bid_response&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  How you catch it before the orchestrator applies the wrong mutations
&lt;/h2&gt;

&lt;p&gt;I maintain &lt;a href="https://rtblint.org" rel="noopener noreferrer"&gt;RTBlint&lt;/a&gt;, an open source OpenRTB and ARTF linter. Envelope checks run before mutation checks because lifecycle and intents define what the response is allowed to propose.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;rtblint validate &lt;span class="nt"&gt;--type&lt;/span&gt; artf-request rtb-request.json
&lt;span class="go"&gt;FAILED (OpenRTB 2.6-202606 ARTF request envelope): 0 error(s), 1 warning(s).
- [warning] bid_response: lifecycle is LIFECYCLE_PUBLISHER_BID_REQUEST, which runs
  before any DSP has answered, yet the envelope carries a bid_response.
  (artf.lifecycle.payload_unexpected)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the document never parses, nothing downstream runs. Truncated fixtures and log line limits show up as &lt;a href="https://rtblint.org/docs/rule-reference/artf-payload-invalid_json/" rel="noopener noreferrer"&gt;&lt;code&gt;artf.payload.invalid_json&lt;/code&gt;&lt;/a&gt; before lifecycle logic executes.&lt;/p&gt;

&lt;p&gt;Pass two on responses pairs each mutation against &lt;code&gt;applicable_intents&lt;/code&gt; and semantic paths in the payloads actually carried. Pass three applies accepted mutations and revalidates OpenRTB so field-level damage appears on the request the exchange will see.&lt;/p&gt;

&lt;p&gt;The same rules run in the &lt;a href="https://rtblint.org/demo/artf/" rel="noopener noreferrer"&gt;ARTF simulator&lt;/a&gt;, MCP tools, and gRPC on &lt;code&gt;openadtech.rtblint.v1&lt;/code&gt;. ARTF already mandates gRPC for the extension point; validating on that channel beats discovering stage skew only after deals activate against the wrong document.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to read the rest of the surface
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://rtblint.org/guides/artf-explained/" rel="noopener noreferrer"&gt;ARTF explained&lt;/a&gt; walks the request and response members without assuming you read the PDF first. The &lt;a href="https://rtblint.org/docs/" rel="noopener noreferrer"&gt;docs index&lt;/a&gt; links every ARTF rule id, including lifecycle, tmax, and mutation path checks, if you want stable ids in CI.&lt;/p&gt;

&lt;p&gt;RTBlint is independent of IAB Tech Lab. The rule ids above come from the public ARTF proto and the v1.0 document; they exist because lifecycle and payloads are contracts you can violate while the RPC still returns OK.&lt;/p&gt;

</description>
      <category>grpc</category>
      <category>openrtb</category>
      <category>adtech</category>
      <category>api</category>
    </item>
    <item>
      <title>Raw fbclid in user_data.fbc Is Not Meta's _fbc Cookie. The CAPI POST Can Still Return 200.</title>
      <dc:creator>Aleksander Sekowski</dc:creator>
      <pubDate>Thu, 17 Sep 2026 01:00:18 +0000</pubDate>
      <link>https://dev.to/aleksuix/raw-fbclid-in-userdatafbc-is-not-metas-fbc-cookie-the-capi-post-can-still-return-200-25n4</link>
      <guid>https://dev.to/aleksuix/raw-fbclid-in-userdatafbc-is-not-metas-fbc-cookie-the-capi-post-can-still-return-200-25n4</guid>
      <description>&lt;p&gt;A paid social checkout posts &lt;code&gt;Purchase&lt;/code&gt; to the Meta Conversions API. Graph returns 200. Events Manager lists the event. Click-through attribution still shows nothing tied to the ad.&lt;/p&gt;

&lt;p&gt;The access token is fine. The clock is in seconds. Email is hashed. The bug is the click id field.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"user_data"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"em"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"a1b2c3...64 hex..."&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"fbc"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"IwAR0abcdefGhijklmnop"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;That&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;string&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;is&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;`fbclid`&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;query&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;parameter&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;landing&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;URL,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;copied&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;server-side&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;order&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;record.&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Meta's&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="err"&gt;customer&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;information&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;parameters&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="err"&gt;(https://developers.facebook.com/docs/marketing-api/conversions-api/parameters/customer-information-parameters)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;document&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;`fbc`&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;as&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;`_fbc`&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;browser&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;cookie,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;not&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;naked&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;click&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;token.&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;The&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;documented&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;shape&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;is&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;four&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;dot-separated&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;segments:&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;fb.{subdomain_index}.{creation_time}.{fbclid}&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
Example from a real cookie line:

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;fb.1.1758300000.IwAR0abcdef&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;
A bare `IwAR…` token fails that contract even when it is the same characters Meta put on the ad click. Matching expects the cookie format Meta generates when `fbclid` hits a first-party page and something (Meta Pixel, Conversions API tag helper, or your own setter) writes `_fbc` with a creation timestamp.

&lt;/span&gt;&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="c"&gt;# Why the landing hop matters&lt;/span&gt;
&lt;span class="go"&gt;
`fbclid` lives on the inbound ad URL. `_fbc` lives in first-party storage after that URL is processed. If the shopper lands on a intermediate domain, if the tag loads after your server already captured the query string, or if Safari ITP blocks the write, your order service may only see `fbclid` in the referrer log. Forwarding that raw value as `user_data.fbc` is a common server-side shortcut. It is not what Meta documents for CAPI.

The failure mode is quiet. HTTP succeeds. Parameter names match the sample JSON. Events Manager does not flash a schema error for every malformed `fbc`. You notice only when campaign reports stay blind to clicks you know happened.

More detail on the hop is in [Meta _fbc is empty unless fbclid hits the landing page](https://pixellint.org/blog/meta-fbc-needs-fbclid-at-landing/).

&lt;/span&gt;&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="c"&gt;# What the linter checks&lt;/span&gt;
&lt;span class="go"&gt;
I maintain [Pixellint](https://github.com/aleksUIX/pixellint), an open source linter for pixels and conversion API payloads. The Meta Conversions API pack contracts `user_data.fbc` against Meta's documented regex shape when the field is present:

&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
console&lt;br&gt;
$ pixellint validate json @purchase.json&lt;br&gt;
rulepack: vendor/meta-conversions-api (vendor: meta)&lt;br&gt;
  error   vendor.meta-conversions-api.body.user_data.fbc.invalid&lt;br&gt;
          &lt;code&gt;user_data.fbc&lt;/code&gt; is &lt;code&gt;IwAR0abcdefGhijklmnop&lt;/code&gt;, which does not match&lt;br&gt;
          &lt;code&gt;^fb\.[0-9]\.[0-9]+\..+$&lt;/code&gt;. Meta documents the click id cookie as&lt;br&gt;
          &lt;code&gt;fb.${subdomain_index}.${creation_time}.${fbclid}&lt;/code&gt;.&lt;br&gt;
    fix:  Send the &lt;code&gt;_fbc&lt;/code&gt; cookie verbatim, or rebuild it in the documented format.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
Paste the body into the [playground](https://pixellint.org/) if you do not want a local install. Artifacts you test may be stored; see [privacy](https://pixellint.org/privacy/).

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
sh&lt;br&gt;
cargo install pixellint&lt;/p&gt;

&lt;h1&gt;
  
  
  or: npm install pixellint
&lt;/h1&gt;

&lt;p&gt;pixellint validate json @purchase.json --rulepack vendor/meta-conversions-api&lt;/p&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;


The field table and every rule id live on the [Meta Conversions API pack page](https://pixellint.org/packs/meta-conversions-api/). For how `fbc` and `fbp` differ from hashed email and from click ids on other vendors, read [Identity for ads](https://pixellint.org/docs/identity/).

## Fix the pipe, not just the string

Correct CAPI wiring forwards the `_fbc` cookie value from the browser session that converted. If you must rebuild server-side, Meta's parameter docs describe setting `_fbc` when `fbclid` is present on the landing URL: subdomain index (usually `1` on www), creation time in **seconds**, then the `fbclid` value. Do not reuse millisecond clocks from other vendors; that is a different class of bug covered in [event time units](https://pixellint.org/docs/timestamp-units/).

Browser-side, confirm the pixel or tag actually runs on the landing URL where `fbclid` appears, not only on checkout. [Facebook Pixel Helper vs Network](https://pixellint.org/docs/facebook-pixel-debugger/) is the split between what the extension shows and what the network tab proves. When Events Manager looks healthy but production matching is empty, walk [Conversions API not working](https://pixellint.org/docs/conversions-api-not-working/) before you rotate tokens.

Also scan for `test_event_code` left on live traffic. That routes events to the test tool, not production reporting. It is a separate silent miss documented in Meta's API guides and flagged as `vendor.meta-conversions-api.testing.test_event_code_present` in the same pack.

## Where to go next

[What is a conversion API](https://pixellint.org/docs/what-is-a-conversion-api/) is the envelope: match keys, clocks, and dedup ids across vendors. [Conversion API validator](https://pixellint.org/docs/conversion-api-validator/) is the CI-shaped entry point when you lint POST bodies on every deploy.

Pixellint is independent of Meta. The rule ids above cite Meta's Conversions API docs because that is where the requirements live, not because this is an official tool.

**Summary:** Send `_fbc` verbatim, or build `fb.1.{seconds}.{fbclid}` after `fbclid` hits first-party landing storage. Raw `fbclid` in `user_data.fbc` is not the documented click cookie, and a 200 from Graph does not prove click matching worked.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

</description>
      <category>javascript</category>
      <category>facebook</category>
      <category>analytics</category>
      <category>advertising</category>
    </item>
  </channel>
</rss>
