In July we found two sets of HTML5 banners that had served about a million impressions a week for three weeks with zero clicks. Not low. Zero. They had passed upload, they rendered in the preview, they were live on real placements. Nothing about them looked broken.

The bundles had no click wiring at all. The creative tool's export had a manifest with "clickTags": null, and nobody had injected the ad server's click library before upload. A banner like that is a picture. It serves, it counts impressions, and it cannot be clicked.

Why nothing catches it

  • Upload does not check for click wiring. The ad server accepts any valid HTML5 bundle.
  • Preview renders the animation, which is what people look at. Nobody clicks the preview.
  • Live serving is fine. Impressions count, pacing is on target, dashboards are green.
  • A CTR of 0.00% sits in a column nobody reads daily, on a row among hundreds.

Ours slipped through because the bundles were repacked and uploaded through the API directly, bypassing the upload script that normally injects the click layer. The script's own log had no entries for those dates, which is how we traced it. One process change, one missing step, three weeks of paid impressions with no way to click.

What a wired banner looks like

For Adform, the bundle needs the DHTML library and a click handler that reads the clickTAG the ad server passes in. Practically, the HTML contains something like Adform.DHTML or dhtml.getVar, or the manifest carries a populated lowercase clicktags object. A manifest that says "clickTags": null, which is what some creative tools export by default, does not count. Other ad servers have their own equivalents; the principle is identical.

One trap: a naive check like "does the file mention clicktag" gives false positives, because some creative tools ship their own runtime that contains the word. Ours did, and the guard waved the broken bundles through.

The check we run after every upload now

  1. Download the live asset back from the ad server. Not the local file: the thing that is actually serving.
  2. Assert the wiring is present in the served HTML or manifest.
  3. Render the bundle in headless Chromium with the ad server's click function stubbed, click it, and assert the clickTAG URL opens.
  4. Only then mark the upload complete. A warning means someone clicks the live preview by hand before sign-off.

The rule now sits in the agent's playbook for HTML5 uploads, in capital letters: never upload through raw API calls, always through the script, always run the verification gate after. The memory file for the incident records the date, the sets, and the false-positive guard, so the next agent session that touches a banner upload starts already knowing. The upload script does this automatically and marks the upload failed if it cannot confirm the click. On top of that, a daily sweep looks at every banner uploaded in the last eight days across all active campaigns and alerts on any bundle without wiring. The day we built the sweep it caught a second set: 324 banners across 18 languages and 18 sizes, shipped the same way.

How to find the ones already live

Pull banner-level stats and flag anything with more than a few hundred impressions and zero clicks. Account CTR on display is usually around 0.05 to 0.1%, so a banner with 5,000 impressions and no clicks is a fault, not bad luck. Two exceptions that legitimately show zero: video assets and redirect or tag-based banners where the click is counted elsewhere.

Then scope the repair by upload set, not by the flagged list. Stats only show banners that delivered. A 320x50 sibling from the same broken batch, with 40 impressions, will not appear in the flag list and will stay broken. Search for the set by name across every campaign and size and check all of them.

Fixing them without re-trafficking

In Adform the broken banners can be repaired in place: inject the click library and handler, upload the new HTML asset, and swap it onto the existing banner. The banner keeps its ID, tags and rotator position, so nothing downstream changes and the campaign keeps serving. Repairing 300-odd banners this way took an afternoon. Re-trafficking them would have taken a week.