> ## Documentation Index
> Fetch the complete documentation index at: https://docs.permutive.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Tracking Video Ad Impressions

> Capture video ad impressions from your player as Permutive events, and understand which ad identifiers you can rely on.

## Overview

Recording which ads a user actually saw lets you build audiences from ad exposure rather than content consumption alone. Typical use cases:

* **Retargeting** — build a cohort of everyone who saw a particular campaign, and target them with a follow-up.
* **Suppression** — stop serving a campaign to users who have already seen it enough times.
* **Sequential messaging** — serve creative B only to users who have already seen creative A.
* **Measurement** — attribute on-site actions back to prior ad exposure.

All of these depend on capturing an identifier that reliably tells you *which* ad was shown. That turns out to be the hard part, and it is what most of this guide is about.

> [!NOTE]
>
> **Before you start**
>
> * A custom event schema for your video ad event must exist in your workspace. See [Creating & Updating Event Schema](https://docs.permutive.com/guides/connectivity/events/create-update-event-schema.md), and the [canonical video ad event schema](https://docs.permutive.com/sdks/ctv/video-tracking.md#ad-event-properties) for the standard `VideoAdView` properties.
> * You need access to your video player's ad integration code, so you can read ad metadata at impression time.
> * Confirm how your ad server hosts video creatives (see [Which identifier should you use](#which-identifier-should-you-use)) — this determines which identifiers are available to you.

> [!NOTE]
>
> **Scope.** This guide covers **client-side video**, where the [Google IMA SDK](https://developers.google.com/interactive-media-ads) requests and renders the ad in the user's browser or app.
>
> It does not cover display advertising, which uses different mechanisms, or server-side ad insertion (SSAI/DAI), where the ad is stitched into the video stream before it reaches the player and different metadata is available.
>
> Examples throughout use **Google Ad Manager**, since it is the most common setup, but the underlying behaviour applies to any ad server serving VAST through the IMA SDK.

## How Video Ad Tracking Works

When a video ad plays, four things happen in sequence:

**1. The player requests an ad**

Your player asks the ad server for an ad to fill the break.

**2. The ad server responds with VAST**

VAST (Video Ad Serving Template) is an XML document describing the ad. It comes in two forms. An **inline** response contains the ad itself, including the video file. A **wrapper** response contains no video file, only a pointer to another ad server that will supply one.

**3. The SDK follows the chain**

If the response is a wrapper, the IMA SDK requests the next URL, and repeats until it reaches an inline response. A single ad can involve several hops. Each hop is a separate ad server with its own identifiers for the same ad.

**4. You read the metadata and record an event**

The SDK exposes the assembled ad metadata through the [`google.ima.Ad`](https://developers.google.com/interactive-media-ads/docs/sdks/html5/client-side/reference/js/google.ima.Ad) object. You read the identifiers you need and send them to Permutive as a custom event.

The wrapper chain in step three is where most tracking problems originate. **Every hop has its own creative ID, and they are unrelated numbers.** The identifier you get back depends on which hop the SDK reads from.

## The Identifiers Available to You

The `google.ima.Ad` object exposes these methods at impression time:

| Method | Returns | Which hop |
| - | - | - |
| `getAdId()` | The ad's ID | Innermost (inline) ad |
| `getCreativeId()` | The creative's ID | Innermost (inline) ad |
| `getAdSystem()` | The name of the ad server that served the inline ad | Innermost (inline) ad |
| `getDealId()` | The deal / programmatic agreement ID | The deal, where one applies |
| `getWrapperAdIds()` | Array of ad IDs, one per wrapper | Every wrapper in the chain |
| `getWrapperCreativeIds()` | Array of creative IDs, one per wrapper | Every wrapper in the chain |
| `getWrapperAdSystems()` | Array of ad server names, one per wrapper | Every wrapper in the chain |
| `getUniversalAdIds()` | Registry-issued creative identifiers, where the advertiser set them | Innermost (inline) ad |

> [!WARNING]
>
> **`getCreativeId()` always reads the innermost ad, not your ad server.**
>
> If your ad server hosts the video file, the innermost ad *is* your ad server's, and `getCreativeId()` returns the ID you see in your own UI.
>
> If your ad server redirects to someone else's, the innermost ad belongs to that other system, and `getCreativeId()` returns **their** creative ID. It will look like a perfectly valid ID. It will simply not correspond to anything in your ad server.

The wrapper arrays are ordered **innermost first, outermost last**. Since your own ad server initiates the chain, it is the outermost wrapper, so its entry is the last element.

> [!TIP]
>
> Always capture `getAdSystem()` alongside any creative ID. It tells you whose numbering scheme the ID belongs to, which is the difference between an ID you can use and one you cannot. Google Ad Manager reports as `GDFP`; Display & Video 360 reports as `DBM`.

## Which Identifier Should You Use

Find the row that matches how the creative is hosted.

| How the creative is served | In Google Ad Manager, creative Type is | Use | Granularity |
| - | - | - | - |
| Your ad server hosts the video file | **Video** | `getCreativeId()` | Per creative |
| Third-party creative, wrapper chain reaches the player | **Redirect** or **Programmatic** | [Wrapper chain lookup](#reading-the-id-from-the-wrapper-chain) | Per creative |
| Third-party creative, wrapper chain flattened server-side | **Redirect** or **Programmatic** | [Deal ID](#falling-back-to-deal-id) | Per line item |

In Google Ad Manager you can check this under **Delivery → Creatives**, in the **Type** column. The distinction is simply whether the ad server holds the video file itself or holds a pointer to it.

> [!NOTE]
>
> Direct-sold campaigns are usually trafficked as **Video**, because the publisher receives the asset and uploads it. Programmatic Guaranteed and other deal-based buys are usually **Redirect** or **Programmatic**, because the buyer serves the creative from their own ad server.
>
> This is why creative-level video retargeting often works perfectly for direct-sold inventory and returns nothing for programmatic inventory on the same site.

## Reading the ID From the Wrapper Chain

When the chain reaches the player, your ad server's creative ID is in `getWrapperCreativeIds()`. Rather than assuming a fixed position, find your ad server by name in `getWrapperAdSystems()` and read the same index.

```javascript theme={"dark"} theme={"dark"}
/**
 * Resolve the creative ID belonging to a specific ad server.
 * @param {google.ima.Ad} ad
 * @param {string} adServerName - e.g. 'GDFP' for Google Ad Manager
 * @returns {string|null}
 */
function resolveCreativeId(ad, adServerName) {
  // The ad server appears as a wrapper in the chain
  const systems = ad.getWrapperAdSystems();
  const index = systems.indexOf(adServerName);
  if (index !== -1) {
    return ad.getWrapperCreativeIds()[index];
  }

  // No wrapper: the inline ad is the ad server's own creative
  if (ad.getAdSystem() === adServerName) {
    return ad.getCreativeId();
  }

  // Third-party creative with no recoverable ad server creative ID
  return null;
}
```

This handles all three cases in the table above, and returns `null` precisely when no usable ID exists, which is the signal to fall back to deal ID.

> [!NOTE]
>
> Google documents the wrapper arrays as containing the full chain with the initiating ad server as the final element. Permutive has not independently verified this behaviour across ad servers, so treat the wrapper path as Google's stated behaviour and confirm it against your own inventory before relying on it in production. The [verification steps](#verifying-your-implementation) below show how.

## When the Wrapper Chain Is Not Available

Some ad servers can resolve the VAST chain on their own servers before responding to the player, returning a single flattened inline ad rather than a chain. In Google Ad Manager this is called **Server-Side Unwrapping (SSU)**. It reduces the number of round trips the player makes, which improves latency and reduces drop-off.

The trade-off is that the wrapper chain no longer exists by the time the SDK sees the response.

> [!WARNING]
>
> **When server-side unwrapping is enabled, all three wrapper arrays return empty arrays.**
>
> `getWrapperAdIds()`, `getWrapperCreativeIds()` and `getWrapperAdSystems()` return `[]`. They do not throw and they do not warn. The only ad metadata that reaches the player is the innermost creative, which for third-party creatives belongs to the buyer.
>
> In this configuration your ad server's creative ID cannot be recovered client-side by any means. No SDK change will surface it.

This is easy to misread, because the symptoms look like a coding error rather than a configuration outcome:

* The methods exist and return cleanly.
* The creative IDs look entirely normal in your ad server's UI.
* Events arrive in Permutive with populated, valid-looking creative IDs.
* Cohorts built against those IDs simply never match anyone.

> [!TIP]
>
> If `getWrapperAdSystems()` returns an empty array while `getAdSystem()` returns an ad server that is **not** yours, server-side unwrapping is the most likely explanation. Confirm with your ad server account team whether it is enabled for your network.

## Falling Back to Deal ID

Where the creative ID cannot be recovered, `getDealId()` is populated for programmatic deals and offers a workable alternative.

Deal ID identifies the **line item**, not the creative. Because a line item can carry several creatives, every creative on that line shares one deal ID, and they cannot be told apart from the deal ID alone.

> [!NOTE]
>
> **If you need per-creative granularity with deal ID, each creative must sit on its own line item.** This is an operational change in your ad server rather than a code change, and it is worth confirming your trafficking team is willing to work that way before committing to the approach.

## Implementation

Capture the identifiers on the ad's `STARTED` event and send them as a custom event.

> [tab] Capturing everything
>
> Recording all the identifiers, including the ones you do not immediately need, makes future diagnosis much easier and costs very little.
>
> ```javascript theme={"dark"} theme={"dark"}
> adsManager.addEventListener(
>   google.ima.AdEvent.Type.STARTED,
>   (event) => {
>     const ad = event.getAd();
>
>     permutive.track('VideoAdView', {
>       ad: {
>         // Canonical schema properties
>         title: ad.getTitle(),
>         duration: ad.getDuration(),
>         creative_id: ad.getCreativeId(),
>
>         // Additional properties for diagnosing identifier issues
>         ad_system: ad.getAdSystem(),
>         deal_id: ad.getDealId(),
>         wrapper_creative_ids: ad.getWrapperCreativeIds(),
>         wrapper_ad_systems: ad.getWrapperAdSystems()
>       }
>     });
>   }
> );
> ```
>
> > [!NOTE]
> >
> > `title`, `duration` and `creative_id` are part of the [canonical ad event schema](https://docs.permutive.com/sdks/ctv/video-tracking.md#ad-event-properties). The remaining properties are additions you would need to define in your own event schema first.

> [tab] Resolving to one ID
>
> If you would rather store a single resolved identifier, use the helper from [Reading the ID From the Wrapper Chain](#reading-the-id-from-the-wrapper-chain) and record the fallback alongside it.
>
> ```javascript theme={"dark"} theme={"dark"}
> adsManager.addEventListener(
>   google.ima.AdEvent.Type.STARTED,
>   (event) => {
>     const ad = event.getAd();
>
>     permutive.track('VideoAdView', {
>       ad: {
>         title: ad.getTitle(),
>         duration: ad.getDuration(),
>         creative_id: resolveCreativeId(ad, 'GDFP'), // null when unavailable
>         deal_id: ad.getDealId(),                    // fallback for programmatic
>         ad_system: ad.getAdSystem()                 // always worth keeping
>       }
>     });
>   }
> );
> ```
>
> > [!TIP]
> >
> > Keep `deal_id` even when `creative_id` resolves. It costs nothing, and it means you are not re-instrumenting your player if your creative hosting changes later.

> [!NOTE]
>
> `VideoAdView` and its `ad.*` properties follow Permutive's [canonical video ad event schema](https://docs.permutive.com/sdks/ctv/video-tracking.md#ad-event-properties). Use these names where you can, so cohorts and reporting behave consistently across platforms.
>
> If you add properties beyond the canonical set, define them in your workspace's event schema first, and avoid reusing an existing property for a new identifier — cohorts built before and after the change would silently target different things.

## Verifying Your Implementation

**1. Log the full ad metadata on a live impression**

Play an ad from the inventory you care about, and log every identifier rather than inspecting them in a debugger. Collapsed arrays in dev tools can be hard to distinguish from empty ones.

```javascript theme={"dark"} theme={"dark"}
const ad = event.getAd();
console.log('Ad system:', ad.getAdSystem());
console.log('Creative ID:', ad.getCreativeId());
console.log('Deal ID:', ad.getDealId());
console.log('Wrapper systems:', JSON.stringify(ad.getWrapperAdSystems()));
console.log('Wrapper creatives:', JSON.stringify(ad.getWrapperCreativeIds()));
```

**2. Check the ad system**

If `getAdSystem()` returns your ad server, the inline creative is yours and `getCreativeId()` is directly usable. If it returns something else, the creative is third-party and you need the wrapper chain or deal ID.

**3. Check whether the wrapper chain survived**

A populated `getWrapperAdSystems()` means the chain reached the player and the wrapper lookup will work. An empty array means it did not, and deal ID is your route.

**4. Cross-check the ID against your ad server**

Take the creative ID your event recorded and search for it in your ad server. If it does not exist there, you are capturing a third-party identifier. This single check settles the question faster than any other.

**5. Confirm the event lands in Permutive**

In the **Events** section of your dashboard, look for your event name and confirm the properties are populated as expected.

**6. Test both direct-sold and programmatic inventory**

These behave differently. An implementation verified only against direct-sold campaigns can still return unusable identifiers on programmatic inventory.

### Common Failure Modes

| Symptom | Likely cause |
| - | - |
| Cohort returns zero users despite the campaign delivering | Creative IDs being captured belong to a third-party ad server. Cross-check one against your ad server. |
| Retargeting works for direct-sold, returns nothing for programmatic | Expected where programmatic creatives are third-party hosted. Check the creative Type. |
| All wrapper arrays return `[]` | Server-side unwrapping is enabled, or the ad genuinely has no wrapper chain. Check `getAdSystem()` to tell the two apart. |
| `getCreativeId()` returns a valid-looking ID that matches nothing | It is the buyer's creative ID, not your ad server's. Confirm with `getAdSystem()`. |
| `getDealId()` returns `null` | The impression did not come from a deal. Direct-sold and open-auction inventory have no deal ID. |
| Cohort targets the wrong creatives after a change | An existing property was reused for a new identifier. Cohorts built before the change still reference the old meaning. |
| No events at all | Event schema not created; listener attached to the wrong event; ad errored before `STARTED`. |

## FAQ

> [accordion] Why does my ad server show one creative ID and Permutive record a different one?
>
> They are identifiers from two different systems that happen to describe the same video. Your ad server's creative ID is a record in your account. The buyer's creative ID is a record in theirs. When the creative is served through a redirect, `getCreativeId()` reads the buyer's.
>
> Capturing `getAdSystem()` alongside the ID tells you immediately which one you are holding.

> [accordion] Can I turn off server-side unwrapping to recover creative IDs?
>
> In principle yes, and it would restore the wrapper chain. In practice it is a meaningful trade-off: server-side unwrapping exists to reduce round trips and improve latency and fill.
>
> Weigh it against the alternative of splitting creatives onto separate line items and using deal ID, which achieves the same granularity without changing how ads are delivered. Discuss the options with your ad server account team.

> [accordion] Does this affect display advertising too?
>
> No. Display uses a different delivery mechanism and different tracking. This guide is specific to video ads rendered by the IMA SDK.

> [accordion] What about server-side ad insertion (SSAI/DAI)?
>
> SSAI stitches ads into the video stream before it reaches the player, so the metadata available differs from client-side playback. The identifiers described here may not be present. Contact [Support](mailto:support@permutive.com) to discuss an SSAI setup.

> [accordion] Should I store the wrapper arrays even if I do not use them?
>
> It is worth doing. They are small, and they make it possible to answer questions about historical impressions that you would otherwise need a new live test to investigate. They also make it obvious after the fact whether server-side unwrapping was in play.

## Next Steps

> [card] [Creating & Updating Event Schema](https://docs.permutive.com/guides/connectivity/events/create-update-event-schema.md)
>
> Define the custom event schema your video ad events will validate against.

> [card] [Tracking Off-Site Events with Pixels](https://docs.permutive.com/guides/connectivity/events/tracking-offsite-events.md)
>
> Track conversions and impressions that happen away from your own pages.

> [card] [IMA SDK Ad reference](https://developers.google.com/interactive-media-ads/docs/sdks/html5/client-side/reference/js/google.ima.Ad)
>
> Google's full reference for the ad metadata methods used in this guide.

> [card] [Contact Support](mailto:support@permutive.com)
>
> Troubleshoot an implementation or confirm the right approach for your setup.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.