---
title: Tealium + ChatGPT Apps
description: Implement Tealium tracking in a ChatGPT app with consistent visitor identity across client-side, server-side, and hybrid approaches.
url: https://docs.tealium.com/guides/chatgpt-apps/
---
## Introduction

Tealium offers a data foundation for app-like experiences in browsers, widgets, and custom ChatGPT interfaces. It supports consistent data collection and visitor identity management across platforms.

This solution for ChatGPT uses an MCP-friendly design to achieve the following:

* Track key interactions (product views, button clicks, purchases).
* Unify identity across client, server, and ChatGPT contexts.
* Stream events in real time using Tealium Collect.
* Personalize experiences using Tealium Moments API and Tealium AudienceStream.

## Option 1: Client-side tracking

This approach uses the standard [Universal Tag (`utag.js`)](https://docs.tealium.com/platforms/javascript/) installation from a dedicated profile in Tealium iQ. The only difference is that it does not rely on the anonymous ID generated by `utag.js`, so it generates and persists its own ID using utility functions.

**Benefits:**

* Full access to the Tealium iQ Tag Management tag vendor marketplace.
* Built-in support for consent.
* Supports A/B testing and personalization triggers.

The following sample code demonstrates the following:

* Load `utag.js`, just like you do on a web site.
* Generate and persist an anonymous visitor ID.
* Track the initial loading of the ChatGPT app.
* Track related user actions with custom methods.

```html
<script>
  // Load Tealium iQ
  (function(a,b,c,d){
    a='https://tags.tiqcdn.com/utag/[ACCOUNT]/[PROFILE]/[ENVIRONMENT]/utag.js';
    b=document;c='script';d=b.createElement(c);d.src=a;
    d.type='text/java'+c;d.async=true;
    a=b.getElementsByTagName(c)[0];a.parentNode.insertBefore(d,a);
  })();

  // Generate a 32-char lowercase alphanumeric visitor ID
  function generateTealiumVisitorId() {
    const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
    return Array.from({ length: 32 }, () => chars[Math.floor(Math.random() * chars.length)]).join('');
  }

  // Retrieve or create & persist the visitor ID
  let tealium_visitor_id = localStorage.getItem('tealium_visitor_id');
  if (!tealium_visitor_id) {
    tealium_visitor_id = generateTealiumVisitorId();
    localStorage.setItem('tealium_visitor_id', tealium_visitor_id);
  }

  // App initialized
  utag.track('event', {
    'tealium_event': 'interface_loaded',
    'app_version': '2.1.0',
    'user_tier': 'enterprise',
    'tealium_visitor_id': tealium_visitor_id
  });

  // PDP view
  function trackViewPdp(product) {
    utag.track('view', {
      'tealium_event': 'view_pdp',
      'product_id': product.id,
      'product_name': product.name,
      'category': product.category,
      'price': product.price,
      'currency': product.currency || 'USD',
      'tealium_visitor_id': tealium_visitor_id
    });
  }

  // Button click
  function trackButtonClick(button) {
    utag.track('link', {
      'tealium_event': 'button_click',
      'button_id': button.id,
      'button_text': button.text,
      'location': button.location || 'pdp',
      'app_version': '2.1.0',
      'tealium_visitor_id': tealium_visitor_id
    });
  }
</script>
```

## Option 2: Server-side tracking

Server-side tracking sends events directly to Tealium using the [HTTP API](https://docs.tealium.com/platforms/http-api/endpoint/). Similar to the client-side solution, it generates and persists its own anonymous visitor ID using added utility functions.

We recommend this approach for the following scenarios:

* When CSP restrictions prevent external JavaScript from loading.
* To guarantee data tracking for essential events such as purchases.

The following server-side tracking solutions are available:

### Node.js apps (Recommended)

The Node.js solution has client code that generates and persists the anonymous visitor ID and calls wrapper functions for tracked events.

For example, the following sample code generates the visitor ID and sends order tracking requests to the server. The `order` object comes from your app when a customer completes a purchase.

```js
// Generate a 32-char lowercase alphanumeric visitor ID
function generateTealiumVisitorId() {
  const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
  return Array.from({ length: 32 }, () => chars[Math.floor(Math.random() * chars.length)]).join('');
}

// Retrieve or create & persist the visitor ID
let tealium_visitor_id = localStorage.getItem('tealium_visitor_id');
if (!tealium_visitor_id) {
  tealium_visitor_id = generateTealiumVisitorId();
  localStorage.setItem('tealium_visitor_id', tealium_visitor_id);
}

async function trackPurchaseServer(order) {
  await fetch('/api/tealium/track', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      'tealium_event': 'purchase',
      'tealium_visitor_id': tealium_visitor_id,
      'order_id': order.id,
      'order_total': order.total,
      'currency': order.currency || 'USD',
      'items': order.items
    })
  });
}
```

The following sample server-side code sets up an endpoint to accept tracking calls and uses [Tealium for Node.js](https://docs.tealium.com/platforms/node/install/) to forward those events on to Tealium Collect.

```js
// npm i tealium-collect express
const express = require('express');
const TealiumCollect = require('tealium-collect');
const app = express();
app.use(express.json());

const tealium = new TealiumCollect({
  'account': 'your-account',
  'profile': 'chatgpt-app',
  'environment': 'prod'
});

app.post('/api/tealium/track', async (req, res) => {
  try {
    const tealium_visitor_id = req.body.tealium_visitor_id;

    if (!/^[a-z0-9]{32}$/.test(tealium_visitor_id || '')) {
      return res.status(400).json({ error: 'Invalid or missing tealium_visitor_id' });
    }

    const allowed = new Set(['interface_loaded','view_pdp','button_click','purchase']);
    if (!allowed.has(req.body.tealium_event)) {
      return res.status(400).json({ error: 'Unsupported event type' });
    }

    await tealium.track({
      ...req.body,
      tealium_visitor_id,
      'timestamp': req.body.timestamp || new Date().toISOString()
    });

    res.status(204).end();
  } catch (e) {
    console.error('Tealium track error:', e);
    res.status(500).json({ error: 'Tracking failed' });
  }
});

app.listen(3000, () => console.log('Tealium server listening on 3000'));
```

### Browser apps

Use this approach for browser apps where you have access to embed `<script>` tags. This code uses `fetch()` and the [HTTP API](https://docs.tealium.com/platforms/http-api/endpoint/) to send events directly to Tealium and uses the same utility methods to generate and persist an anonymous visitor ID.

```html
<script>
  // Generate a 32-char lowercase alphanumeric visitor ID
  function generateTealiumVisitorId() {
    const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
    return Array.from({ length: 32 }, () => chars[Math.floor(Math.random() * chars.length)]).join('');
  }

  // Retrieve or create & persist the visitor ID
  let tealium_visitor_id = localStorage.getItem('tealium_visitor_id');
  if (!tealium_visitor_id) {
    tealium_visitor_id = generateTealiumVisitorId();
    localStorage.setItem('tealium_visitor_id', tealium_visitor_id);
  }

  async function sendCollectEvent(payload) {
    const base = {
      'tealium_account': 'your-account',
      'tealium_profile': 'chatgpt-app',
      'tealium_visitor_id': tealium_visitor_id,
      'timestamp': new Date().toISOString()
    };

    await fetch('https://collect.tealiumiq.com/event', {
      'method': 'POST',
      'headers': { 'Content-Type': 'application/json' },
      'body': JSON.stringify({ ...base, ...payload })
    });
  }

  function trackViewPdpServer(product) {
    return sendCollectEvent({
      'tealium_event': 'view_pdp',
      'product_id': product.id,
      'product_name': product.name,
      'product_category': product.category,
      'product_price': product.price,
      'product_currency': product.currency || 'USD'
    });
  }

  function trackButtonClickServer(button) {
    return sendCollectEvent({
      'tealium_event': 'button_click',
      'button_id': button.id,
      'button_text': button.text,
      'location': button.location || 'pdp',
      'app_version': '2.1.0'
    });
  }
</script>
```

## Option 3: Hybrid (Recommended)

A hybrid approach uses the client-side solution to track interaction events, such as  `interface_loaded`, `view_pdp`, and `button_click`, and the server-side solution to track authoritative events such as `purchase`. We recommend this approach to achieve the most complete tracking of your app.


<blockquote>
When running the hybrid solution, do not load the Tealium Collect tag in your iQ profile. This prevents duplicate event tracking.
</blockquote>


## Visitor identity

To ensure accurate tracking and a unified visitor profile, always use the same anonymous visitor ID for both client-side and server-side events. This approach maintains a single visitor identity across ChatGPT, web, and mobile, enabling consistent tracking and real-time personalization.


<blockquote>
We recommend including known user identifiers as separate parameters when available, for example `customer_id` or `email_address_hash`, but do not use these values to replace `tealium_visitor_id`.
</blockquote>


## MCP integration (Optional)

Expose a simple MCP tool from your server for ChatGPT to call. This approach lets ChatGPT handle conversational logic, while your server enforces format, identity, and policy.

**Tool:** `tealium-track-event`

**Example schema:**

```json
{
  "account": "your-account",
  "profile": "chatgpt-app",
  "environment": "prod",
  "event": "view_pdp",
  "visitorId": "abcdefghijklmnopqrstuvwxyz012345",
  "data": {
    "product_id": "widget-123",
    "price": 29.99,
    "currency": "USD"
  }
}
```

**Validation:**

* `visitorId` must match the regular expression `/^[a-z0-9]{32}$/`.
* `event` must be in list of allowed events (`interface_loaded`, `view_pdp`, `button_click`, `purchase`).

## Moments API MCP

Add the [Moments MCP server](https://docs.tealium.com/moments-api-mcp-server/) to your app to enable personalization.

**Example flow:**

1. Track events `view_pdp`, `button_click`, and `purchase`.
2. **AudienceStream** builds the visitor profile.
3. Chat UI (or MCP tool) queries **Moments API** with `tealium_visitor_id`.
4. Responses adapt (recommendations, offers, tone).

## Data privacy and compliance

This app supports data privacy and compliance through the following features:

* Consent-driven activation through the Tealium iQ Consent Manager.
* PII governance through omitting or hashing before transmitting data.
* Regional compliance (GDPR, CCPA, and data residency).

## Sample app

### Prerequisites

* Node.js 18+ (LTS)
* `npm` or `pnpm`
* Tealium account, profile, and environment
* (Optional) ngrok for MCP exposure
 
### Install  
    
1. Install dependencies for the main project:   
    ```bash
    pnpm install
    ```
2. Install dependencies for the server:   
    ```bash
    cd server
    pnpm install
    cd ..
    ```

### Run the application  
  
1. Build the frontend bundle:  
    ```sh
    pnpm run build  
    ```
2. Start the MCP server:  
    ```sh
    cd server  
    pnpm start
    ```  
2. The server runs at [http://localhost:8000/mcp](http://localhost:8000/mcp)
3. Generate an ngrok tunnel (in a new terminal):  
    ```sh
    ngrok http 8000  
    ```
3. ngrok returns the following output:  
    ```none
    Session Status    online  
    Account           Your Account (Plan: Free)  
    Update            update available (version 3.x.x, Ctrl-C to update)  
    Version           3.x.x  
    Region            United States (us)  
    Web Interface     http://127.0.0.1:4040  
    Forwarding        https://abc123def456.ngrok-free.app -> http://localhost:8000  
    ```
4. Update the CSP domains in server code:
    * Copy the ngrok URL from the terminal output (for example: `https://abc123def456.ngrok-free.app`).  
    * In `server/index.ts` on lines 30 and 34, replace `https://resolvedly-pouched-nena.ngrok-free.dev` with your new ngrok URL. 
    * Make the same update in `src/tealium/TealiumTracker.tsx` on line 134.
5. Restart the server:  
    ```sh
    # Kill the current server (Ctrl+C)
    cd server
    pnpm start
    ``` 
6. Rebuild the frontend with the new URL:  
    ```sh
    cd ..  
    pnpm run build
    ```  

Make note of the following access points:  

* Local MCP Server: [http://localhost:8000/mcp](http://localhost:8000/mcp)  
* Health Check: [http://localhost:8000/health](http://localhost:8000/health)
* API Endpoint: [http://localhost:8000/api/tealium/track](http://localhost:8000/api/tealium/track)  
* Public ngrok URL: ngrok URL + `/mcp`
* Public API: ngrok URL + `/api/tealium/track`

### Add the app in ChatGPT

1. Go to **ChatGPT > Settings > Apps and Connectors**.
2. Enable **Developer Mode** under **Advanced Settings**.
3. Paste your **ngrok URL** in the **MCP server field**.

### Launch the Tealium app

1. When prompted, use the `tealium-tracker` tool.  
1. Enter your Tealium account, profile, and environment.  
1. The Tealium Universal tag (`utag.js`) loads and tracks events as if in a web app.


<blockquote>
If third-party libraries are blocked, allow their resources in your CSP.
</blockquote>


### **Server-side event tracking**

Turning on server-side tracking sends client-side calls to `/api/tealium/track`, which uses the Tealium Collect NPM module to make tracking requests.



Validate results in [Tealium Trace](https://docs.tealium.com/about-trace/).


