> ## 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 Off-Site Events with Pixels

> Capture conversions, email opens, and ad impressions that happen outside your site, and attribute them to your Permutive users.

## Overview

Off-site tracking lets you record events that happen away from your own pages — a conversion on a partner site, an email open, an ad impression in a video player — and attribute them to a user in Permutive. Typical use cases:

* **Campaign reporting** — measure conversions driven by a campaign on partner inventory (e.g. a `PartnerConversion` event).
* **Campaign optimization** — adjust targeting mid-flight to hit conversion KPIs.
* **Suppression** — drop users from a targeting cohort once they convert.
* **Premium targeting** — target users who visited a partner site but have not yet converted.
* **Engagement audiences** — track email opens (e.g. an `EmailOpen` event) or ad views (e.g. an `AdImpression` event) to build audiences from off-site engagement.

Every method below ultimately calls one endpoint — Permutive's pixel-tracking endpoint — with a small set of parameters. The differences between methods come down to two independent choices: **how you identify the user**, and **how the pixel is fired**. The rest of this guide is organized around those two choices.

<Info>
  **Before you start**

  * A custom event schema for your event must exist in your workspace. See [Creating & Updating Event Schema](/guides/connectivity/events/create-update-event-schema).
  * Decide which identifier you'll use (see [Identifying the user](#identifying-the-user)).
  * If the event involves EU/UK/Swiss users, read [Consent and privacy](#consent-and-privacy) first.
</Info>

## Choose Your Setup

Find the row closest to where your event happens. This tells you the identifier and firing mechanism to use; the linked sections cover each in detail.

| Where the event happens | Recommended identifier | Firing mechanism |
| - | - | - |
| A web page you control (JS available) | Permutive user ID (`u`) or first-party ID (`i` + `it`) | [JavaScript tag](#firing-the-pixel) |
| A partner web page (you supply a tag) | First-party ID, or third-party cookie sync | [JavaScript tag or tracking URL](#firing-the-pixel) |
| A marketing email | First-party ID or Permutive user ID | [HTML image pixel](#firing-the-pixel) |
| An ad server, display/web | Third-party cookie sync (legacy) or first-party ID | [Tracking URL](#firing-the-pixel) in the impression/view field |
| An ad server, video / CTV / in-app | Device ID (MAID) via an ad-server macro | [Tracking URL](#firing-the-pixel) with ad-server macros — contact [Support](mailto:support@permutive.com) |

<Note>
  On a page where the Permutive SDK is already loaded, prefer `permutive.track()` — use `px/track` where the SDK isn't available on that specific page (for example, a post-checkout page reached by redirect).
</Note>

## Parameter Reference

All methods build a request to:

```
https://api.permutive.app/v2.0/px/track
```

| Parameter | Required | Description |
| - | - | - |
| `k` | Yes | Your workspace **public API key**. |
| `e` | Yes | **Event name**. Must match a custom event schema in your workspace. |
| `p` | No | **Event properties**, as a JSON object, URL-encoded (the snippets below handle this — see the [FAQ](#faq) for details). |
| `i` | Conditional | **Identifier value** — the user identifier you're tracking against. Must be paired with `it`. At least one of `i` (+ `it`) or `u` must be set. |
| `it` | Conditional | **Identifier tag** — the type/namespace of the value in `i` (e.g. `appnexus`, `idfa`, `aaid`, or your own first-party identifier tag). |
| `u` | Conditional | **Permutive user ID**, used when you already know it. Use this *or* the `i` + `it` pair, not both. |
| `rand` | Recommended | **Cache-buster.** Not read by Permutive — added so intermediaries (CDNs, ad servers, email clients) don't serve a cached response. Use a random number or a platform macro. |

The endpoint also accepts `s` (session ID) and `vid` (view ID), which Permutive's own SDKs set to stitch events into a browsing session. Off-site tracking has no such session, so leave them out.

<Note>
  Consent parameters such as `gdpr`, `gdpr_consent` and `consent` are **not** read by `px/track`. They belong to the upstream ID-sync provider (e.g. AppNexus, Google), which uses them to decide whether to serve the request at all. See [Consent and privacy](#consent-and-privacy).
</Note>

## Identifying the User

You attach an event to a user in one of the following ways.

<AccordionGroup>
  <Accordion title="Permutive user ID — most direct">
    If you already hold the Permutive-assigned user ID (for example, you read it on your own site before redirecting), pass it as `u`. No `it` is needed.
  </Accordion>

  <Accordion title="First-party identifier — most durable">
    Use your own identifier (such as a hashed email) through Permutive's identity framework. Pass the value as `i` and its tag as `it`. This is the most reliable method across browsers and environments because it doesn't depend on third-party cookies. Contact [Support](mailto:support@permutive.com) to confirm the identifier tag to use.
  </Accordion>

  <Accordion title="Third-party cookie sync — legacy, web only">
    Use a partner's cookie (AppNexus, Google, The Trade Desk) by routing the request through that partner's sync endpoint, which substitutes its own ID into `i` before redirecting to `px/track`. This only works in browsers that allow third-party cookies, so its reach is shrinking; prefer a first-party identifier where you can. Patterns are in [Third-party cookie sync providers](#third-party-cookie-sync-providers).
  </Accordion>

  <Accordion title="Device ID (MAID) — app and CTV">
    In mobile-app and connected-TV environments there is no cookie, but the ad server can supply the device's advertising ID through a macro. Pass the ID as `i` with the `it` tag `idfa` (iOS IDFA) or `aaid` (Android AAID). This is the right path for video/CTV — contact [Support](mailto:support@permutive.com) to set it up: share the event you want to track and the ad server you'll fire from, and Support will confirm the identifier setup for your workspace and the macro tokens to use in your tracking URL.
  </Accordion>
</AccordionGroup>

## Firing the Pixel

<Tabs>
  <Tab title="Tracking URL">
    Use this when you paste a URL into a third-party platform (such as an ad server's impression/view-tracking field) and the platform fires the request for you.

    The snippets below build a URL you can copy. Run them in your browser console.

    <Steps>
      <Step title="With a first-party identifier (recommended)">
        ```javascript theme={"dark"}
        const publicKey = 'your-public-api-key';
        const eventName = 'PartnerConversion';
        const identifierValue = 'your-user-id';
        const identifierTag = 'your-identifier-tag';
        const eventProperties = { partner: 'Example Partner', value: 50.0 };

        const baseUrl = 'https://api.permutive.app/v2.0/px/track';
        const params = new URLSearchParams({
          k: publicKey,
          i: identifierValue,
          it: identifierTag,
          e: eventName,
          p: JSON.stringify(eventProperties),
          rand: Math.round(Math.random() * 1000000)
        });

        console.log(`${baseUrl}?${params}`);
        ```

        **Example output:**

        ```
        https://api.permutive.app/v2.0/px/track?k=your-public-api-key&i=your-user-id&it=your-identifier-tag&e=PartnerConversion&p=%7B%22partner%22%3A%22Example+Partner%22%2C%22value%22%3A50%7D&rand=123456
        ```
      </Step>

      <Step title="With the Permutive user ID">
        ```javascript theme={"dark"}
        const publicKey = 'your-public-api-key';
        const eventName = 'PartnerConversion';
        const userId = 'permutive-user-id';
        const eventProperties = { partner: 'Example Partner', value: 50.0 };

        const baseUrl = 'https://api.permutive.app/v2.0/px/track';
        const params = new URLSearchParams({
          k: publicKey,
          u: userId,
          e: eventName,
          p: JSON.stringify(eventProperties),
          rand: Math.round(Math.random() * 1000000)
        });

        console.log(`${baseUrl}?${params}`);
        ```

        **Example output:**

        ```
        https://api.permutive.app/v2.0/px/track?k=your-public-api-key&u=permutive-user-id&e=PartnerConversion&p=%7B%22partner%22%3A%22Example+Partner%22%2C%22value%22%3A50%7D&rand=123456
        ```
      </Step>

      <Step title="With a third-party cookie sync">
        Route the request through the partner's sync endpoint so it can inject its cookie ID. See [Third-party cookie sync providers](#third-party-cookie-sync-providers) for AppNexus, Google and The Trade Desk patterns — and note the [encoding](#faq) implications of the extra redirect hop.
      </Step>
    </Steps>

    <Tip>
      Many platforms expose a macro for the `rand` cache-buster (and for IDs). Prefer the platform's macro over a static value — check your platform's macro documentation, and see the [FAQ](#faq) on using macros.
    </Tip>
  </Tab>

  <Tab title="JavaScript tag">
    Use this when you can run JavaScript on the page where the event happens — a partner conversion page, an iframe, or a web video player. It lets you populate properties dynamically from the page.

    ```html theme={"dark"}
    <script>
    (function () {
      const publicKey = 'your-public-api-key';
      const eventName = 'PartnerConversion';
      const eventProperties = {
        partner: 'Example Partner',
        url: window.location.href
      };

      const baseUrl = 'https://api.permutive.app/v2.0/px/track';
      const params = new URLSearchParams({
        k: publicKey,
        // identify with u, or i + it — see "Identifying the user"
        u: 'permutive-user-id',
        e: eventName,
        p: JSON.stringify(eventProperties),
        rand: Math.round(Math.random() * 1000000)
      });

      new Image().src = `${baseUrl}?${params}`;
    })();
    </script>
    ```

    <Tip>
      The same tag can serve several partner sites — infer the partner from `window.location.href` rather than hard-coding it.
    </Tip>
  </Tab>

  <Tab title="HTML image pixel">
    Use this where JavaScript isn't allowed, most commonly marketing emails.

    Build the URL with the snippets in the **Tracking URL** tab, then drop it into a 1×1 image:

    ```html theme={"dark"}
    <img src="YOUR_TRACKING_URL" height="1" width="1" border="0" alt="" />
    ```

    **Example (Permutive user ID):**

    ```html theme={"dark"}
    <img src="https://api.permutive.app/v2.0/px/track?k=your-public-api-key&u=permutive-user-id&e=EmailOpen&p=%7B%22campaign%22%3A%22Summer+Sale%22%7D&rand=123456" height="1" width="1" border="0" alt="" />
    ```

    <Tip>
      Many email providers offer a cache-buster macro for `rand` — use it instead of a static number so opens aren't under-counted.
    </Tip>
  </Tab>
</Tabs>

## Third-Party Cookie Sync Providers

These route the `px/track` request through a partner's cookie-sync endpoint, which substitutes its own user ID before redirecting. They are **browser-only** and depend on third-party cookies. Prefer a first-party identifier where the environment allows it.

<Warning>
  Each of these adds a redirect hop, which adds an encoding layer to the nested `px/track` URL. The snippets handle this for you, but read the [encoding FAQ](#faq) before adapting them — the `p` value ends up double-encoded in the AppNexus and Trade Desk patterns.
</Warning>

<Expandable title="AppNexus (getuid)">
  AppNexus substitutes its cookie ID where the `$UID` macro appears, then redirects to the nested URL. The entire redirect target is percent-encoded so its query string isn't confused with `getuid`'s own parameters; `getuid` decodes it exactly once before redirecting, so `p` arrives at `px/track` single-encoded. The `$UID` macro is substituted after that decode, so it's fine for it to be encoded (`%24UID`) along with the rest of the redirect target.

  ```javascript theme={"dark"}
  const publicKey = 'your-public-api-key';
  const eventName = 'PartnerConversion';
  const eventProperties = { partner: 'Example Partner', value: 50.0 };

  const baseUrl = 'https://api.permutive.app/v2.0/px/track';
  const params = new URLSearchParams({
    k: publicKey,
    it: 'appnexus',
    e: eventName,
    p: JSON.stringify(eventProperties),
    rand: Math.round(Math.random() * 1000000)
  });

  // encodeURIComponent (not encodeURI) so the whole inner URL is encoded as one value
  const redirect = encodeURIComponent(`${baseUrl}?${params}&i=$UID`);
  console.log(`https://ib.adnxs.com/getuid?${redirect}`);
  ```

  **Example output:**

  ```
  https://ib.adnxs.com/getuid?https%3A%2F%2Fapi.permutive.app%2Fv2.0%2Fpx%2Ftrack%3Fk%3Dyour-public-api-key%26it%3Dappnexus%26e%3DPartnerConversion%26p%3D%257B%2522partner%2522%253A%2522Example%2BPartner%2522%252C%2522value%2522%253A50%257D%26rand%3D123456%26i%3D%24UID
  ```

  <Note>
    **Consent:** for EU/UK users you must append the provider's consent parameters (`gdpr` + `gdpr_consent`, or the `consent=1`/`consent=0` fallback) to the **AppNexus** URL — outside the encoded redirect target, e.g. `...%24UID&gdpr=1&gdpr_consent=YOUR_CONSENT_STRING`. Without them, `getuid` rejects the request outright with an HTTP 400 ("Request failed due to privacy signals"), so no event reaches Permutive. See [Consent and privacy](#consent-and-privacy).
  </Note>
</Expandable>

<Expandable title="Google ID sync">
  Google's cookie-match endpoint forwards your custom parameters to Permutive and appends the Google ID.

  ```javascript theme={"dark"}
  const publicKey = 'your-public-api-key';
  const permutiveUserId = 'permutive-user-id'; // optional, if known
  const eventName = 'AdImpression';
  const eventProperties = { campaign_id: 123 };

  const params = new URLSearchParams({
    google_nid: 'permutive_dmp',
    google_cm: '',
    type: 'ddp',
    k: publicKey,
    e: eventName,
    p: JSON.stringify(eventProperties),
    gdpr: 0, // set to 1 if GDPR applies, and add gdpr_consent
    rand: Math.round(Math.random() * 1000000)
  });
  if (permutiveUserId) params.set('u', permutiveUserId);

  console.log(`https://cm.g.doubleclick.net/pixel?${params}`);
  ```

  **Example output:**

  ```
  https://cm.g.doubleclick.net/pixel?google_nid=permutive_dmp&google_cm=&type=ddp&k=your-public-api-key&e=AdImpression&p=%7B%22campaign_id%22%3A123%7D&gdpr=0&rand=123456&u=permutive-user-id
  ```
</Expandable>

<Expandable title="The Trade Desk ID sync">
  The Trade Desk carries custom parameters inside `ttd_passthrough`, which is itself URL-encoded — so the JSON in `p` ends up **double-encoded** (one decode by The Trade Desk, one by Permutive — see the [encoding FAQ](#faq)).

  ```javascript theme={"dark"}
  const publicKey = 'your-public-api-key';
  const permutiveUserId = 'permutive-user-id';
  const eventName = 'AdImpression';
  const eventProperties = { campaign_id: 123 };

  // These are forwarded to Permutive; URLSearchParams encodes them once...
  const passthrough = new URLSearchParams({
    e: eventName,
    p: JSON.stringify(eventProperties)
  });

  // ...and they're encoded a second time when nested as a parameter value.
  const params = new URLSearchParams({
    ttd_pid: 'dbegppc',
    ttd_tpi: '1',
    ttd_puid: `${publicKey},${permutiveUserId}`, // <api_key>,<permutive_user_id>
    ttd_passthrough: passthrough.toString(),
    gdpr: 1,
    gdpr_consent: 'YOUR_CONSENT_STRING',
    rand: Math.round(Math.random() * 1000000)
  });

  console.log(`https://match.adsrvr.org/track/cmf/generic?${params}`);
  ```

  **Example output:**

  ```
  https://match.adsrvr.org/track/cmf/generic?ttd_pid=dbegppc&ttd_tpi=1&ttd_puid=your-public-api-key%2Cpermutive-user-id&ttd_passthrough=e%3DAdImpression%26p%3D%257B%2522campaign_id%2522%253A123%257D&gdpr=1&gdpr_consent=YOUR_CONSENT_STRING&rand=123456
  ```
</Expandable>

## Consent and Privacy

<Warning>
  `px/track` records whatever it receives — it performs **no consent enforcement of its own**. It is your responsibility to ensure the pixel only sends events and identifiers where you are permitted to do so.
</Warning>

Two mechanical points to be aware of:

* The `gdpr`, `gdpr_consent` and `consent` parameters that appear in the [third-party cookie sync patterns](#third-party-cookie-sync-providers) are read by the **sync provider** (AppNexus, Google, The Trade Desk), not by Permutive. The provider uses them to decide whether to serve the request and return its ID — for example, AppNexus rejects requests without them outright for EU/UK traffic.
* That provider-side gate only exists when a sync provider sits in front of `px/track`. When you fire the pixel directly (a first-party identifier, a Permutive user ID, or an ad-server tracking field pointing straight at `px/track`), there is no gate in the request path — only fire the pixel where your own consent controls permit it.

<Note>
  This guide describes mechanisms, not legal advice. Confirm the requirements for your audiences with your privacy/legal team.
</Note>

## Verifying Your Implementation

<Steps>
  <Step title="Fire a test event">
    Trigger the pixel (load the page, open the test email, or preview the creative in your ad server).
  </Step>

  <Step title="Inspect the request that actually fires">
    In browser dev tools (Network tab) or your ad server's creative preview, confirm the final URL: macros have expanded, `p` is encoded the expected number of times, and the identifier is populated. Most issues are visible here.
  </Step>

  <Step title="Check the event lands in Permutive">
    In the **Events** section of your dashboard, look for your event name (e.g. `PartnerConversion`). Allow a little time for events to surface.
  </Step>
</Steps>

### Common failure modes

| Symptom | Likely cause |
| - | - |
| No events at all | Event schema not created for `e`; wrong `k`; pixel not firing (check Network tab). |
| Events arrive but properties are garbled | Wrong number of encoding layers on `p` — see the [FAQ](#faq). |
| No events for EU users via cookie sync (AppNexus / Google / TTD) | Consent gate blocking the request. If you have consent: check `gdpr` + `gdpr_consent` (or `consent`) are on the sync-provider URL, outside the encoded redirect target. If the user hasn't consented: this is expected — no ID sync means no pixel fires. |
| No events for EU users on a direct pixel (first-party ID or `u`) | Not the pixel itself — `px/track` has no consent gate. Check your CMP / tag-manager conditions and whether your consent controls are suppressing the pixel before it fires. |
| Cookie-sync pixel returns a 200 image but no event | The user has no cookie with that provider — e.g. `getuid` serves a GIF instead of redirecting, so nothing reaches `px/track`. |
| Macro appears literally in the fired URL | Macro token was URL-encoded, or the platform doesn't recognize it — see the [FAQ](#faq). |
| Identifier empty in app/CTV | Limit Ad Tracking / child-directed flag, or non-HTTPS creative. |

## FAQ

<AccordionGroup>
  <Accordion title="How many times should I URL-encode the event properties?">
    The snippets in this guide handle encoding for you — `URLSearchParams` applies the one layer that a direct `px/track` request needs. If you're hand-assembling or adapting a URL, use this rule:

    **Count the number of times the value will be URL-*decoded* between where you write it and where Permutive reads it, and encode it that many times.**

    * `px/track` decodes the `p` parameter **once** to recover your JSON — so on a direct request, `p` must be **single-encoded**.
    * A property value sitting *inside* `p` is part of that JSON — it inherits `p`'s encoding; don't encode it separately.
    * If the **entire** `px/track` URL is carried as a parameter or redirect target inside another URL — an AppNexus `getuid` redirect, or The Trade Desk's `ttd_passthrough` — that outer hop adds **one more** decode, so everything inside (including `p`) needs **one extra** layer. The provider strips one layer when it redirects; `px/track` strips the last.

    <Note>
      **Spaces:** `URLSearchParams` encodes a space as `+`, not `%20`. Both decode to a space in a query string, but be aware of the difference if you hand-assemble a URL.
    </Note>
  </Accordion>

  <Accordion title="Can I use platform macros in the tracking URL?">
    Yes — most ad servers and email providers offer macros for cache-busters and IDs, and using them is recommended (see the `rand` tips above). One rule: **never URL-encode the macro token itself.** The platform matches it literally to substitute the value; an encoded token won't be recognized and will appear verbatim in the fired URL.
  </Accordion>
</AccordionGroup>

## Next Steps

<CardGroup cols={2}>
  <Card title="Creating & Updating Event Schema" icon="table" href="/guides/connectivity/events/create-update-event-schema">
    Set up the custom event schema your pixel events will validate against.
  </Card>

  <Card title="Contact Support" icon="envelope" href="mailto:support@permutive.com">
    Troubleshoot an implementation or confirm identifier tags.
  </Card>
</CardGroup>


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