Skip to main content

Custom ChatGPT Integration (End‑to‑End Example)

This guide shows how to wire the Teads AI Chatbot SDK into a Custom ChatGPT using Actions (OpenAPI), a lightweight Node/Express proxy, and prompt/tooling conventions that let your GPT fetch organic/paid content and fire measurement events safely.

What you’ll get

  • A ready-to-import OpenAPI (Actions) spec for Custom ChatGPT
  • A Node/Express proxy that calls Teads/Partner endpoints
  • Example system & tool use instructions for your GPT
  • Conversation flows for measurement, content fetch, and click tracking
  • Privacy/Security tips tailored for Actions

0) Architecture at a Glance

User ↔ Custom ChatGPT ──(Actions/OpenAPI)──► Your Proxy ──► Teads/Partner
◄────────────── JSON (vjnc)
  • Custom ChatGPT invokes Actions you define via an OpenAPI spec.
  • Your proxy signs/forwards Teads requests, keeping keys secret.
  • Teads replies in JSON (vjnc) format; your GPT summarizes/uses it in chat.
  • Use testMode=true in non-production.

1) Environment & Prereqs

  • Teads/Partner provisioning:
    • {YOUR_MEASUREMENT_API_KEY}, {YOUR_CONTENT_API_KEY}
    • {YOUR_MEASUREMENT_WIDGET_ID}, {YOUR_ORGANIC_WIDGET_ID}, {YOUR_PAID_WIDGET_ID}
  • Publicly reachable HTTPS proxy (e.g., Vercel, Fly.io, Cloud Run).
  • Do not expose API keys to ChatGPT; the proxy injects them.

Example .env for your proxy:

TEADS_BASE_URL=https://mv.outbrain.com
TEADS_BASE_PATH=/Multivac/api/platforms
TEADS_FORMAT=vjnc
TEADS_CORS=true
TEADS_IDX=0

TEADS_MEASUREMENT_API_KEY=REPLACE_ME
TEADS_MEASUREMENT_WIDGET_ID=REPLACE_ME

TEADS_CONTENT_API_KEY=REPLACE_ME
TEADS_ORGANIC_WIDGET_ID=REPLACE_ME
TEADS_PAID_WIDGET_ID=REPLACE_ME

TEADS_TEST_MODE=true # set false in production
PORT=8080

2) Node/Express Proxy (Secure Key Injection)

Save as server.js and deploy. Replace env vars as needed.

import express from "express";
import fetch from "node-fetch";

const app = express();

const BASE = process.env.TEADS_BASE_URL || "https://mv.outbrain.com";
const PATH = process.env.TEADS_BASE_PATH || "/Multivac/api/platforms";
const FORMAT = process.env.TEADS_FORMAT || "vjnc";
const CORS = process.env.TEADS_CORS || "true";
const IDX = process.env.TEADS_IDX || "0";
const TEST_MODE = (process.env.TEADS_TEST_MODE || "true") === "true";

function buildUrl(params) {
const url = new URL(`${BASE}${PATH}`);
for (const [k, v] of Object.entries(params)) url.searchParams.append(k, v);
return url.toString();
}

function canonicalEncode(rawUrl) {
// strip query/fragments before encoding
const u = new URL(rawUrl);
u.search = "";
u.hash = "";
return encodeURIComponent(u.toString());
}

// Measurement endpoint
app.get("/teads/measure", async (req, res) => {
try {
const { contentUrl, extid = "chatbot_load" } = req.query;
if (!contentUrl) return res.status(400).json({ error: "contentUrl required" });

const params = {
contentUrl: canonicalEncode(contentUrl),
key: process.env.TEADS_MEASUREMENT_API_KEY,
widgetJSId: process.env.TEADS_MEASUREMENT_WIDGET_ID,
idx: IDX,
format: FORMAT,
cors: CORS,
extid,
...(TEST_MODE ? { testMode: "true" } : {})
};

const url = buildUrl(params);
const r = await fetch(url, { method: "GET" });
// Measurement flow can ignore body, but we pass-through for debugging
const text = await r.text();
res.status(r.status).send(text);
} catch (e) {
res.status(500).json({ error: "measure failed", details: e.message });
}
});

// Organic content endpoint
app.get("/teads/organic", async (req, res) => {
try {
const { contentUrl, news = "latest", newsFrom = "US" } = req.query;
if (!contentUrl) return res.status(400).json({ error: "contentUrl required" });

const params = {
contentUrl: canonicalEncode(contentUrl),
key: process.env.TEADS_CONTENT_API_KEY,
widgetJSId: process.env.TEADS_ORGANIC_WIDGET_ID,
idx: IDX,
format: FORMAT,
cors: CORS,
news,
newsFrom,
...(TEST_MODE ? { testMode: "true" } : {})
};

const url = buildUrl(params);
const r = await fetch(url, { method: "GET" });
const text = await r.text();
res.status(r.status).type("application/json").send(text);
} catch (e) {
res.status(500).json({ error: "organic failed", details: e.message });
}
});

// Paid content endpoint
app.get("/teads/paid", async (req, res) => {
try {
const { contentUrl } = req.query;
if (!contentUrl) return res.status(400).json({ error: "contentUrl required" });

const params = {
contentUrl: canonicalEncode(contentUrl),
key: process.env.TEADS_CONTENT_API_KEY,
widgetJSId: process.env.TEADS_PAID_WIDGET_ID,
idx: IDX,
format: FORMAT,
cors: CORS,
...(TEST_MODE ? { testMode: "true" } : {})
};

const url = buildUrl(params);
const r = await fetch(url, { method: "GET" });
const text = await r.text();
res.status(r.status).type("application/json").send(text);
} catch (e) {
res.status(500).json({ error: "paid failed", details: e.message });
}
});

app.listen(process.env.PORT || 8080, () => {
console.log("Teads proxy running on port", process.env.PORT || 8080);
});

Deploy (examples): Vercel (serverless functions), Cloud Run, Fly.io, Render. Ensure HTTPS.


3) OpenAPI (Actions) Spec for Custom ChatGPT

Create a Custom GPT → ActionsAdd Action → Paste this OpenAPI 3.1 spec (update your server URL).

openapi: 3.1.0
info:
title: Teads Proxy API for Custom ChatGPT
version: 1.0.0
description: Secure proxy to Teads/Partner endpoints used by ChatGPT Actions.
servers:
- url: https://YOUR_PUBLIC_PROXY_HOST
paths:
/teads/measure:
get:
operationId: measureEvent
summary: Fire a Teads measurement event (e.g., chatbot_load, unit_view, unit_click)
parameters:
- in: query
name: contentUrl
required: true
schema: { type: string, format: uri }
description: Canonical page/chat URL
- in: query
name: extid
required: false
schema: { type: string, enum: [chatbot_load, unit_load, unit_view, unit_click] }
description: Measurement event ID
responses:
'200':
description: Measurement response (body may be ignored)
content:
application/json:
schema:
type: object
additionalProperties: true
/teads/organic:
get:
operationId: getOrganic
summary: Fetch organic (publisher) recommendations
parameters:
- in: query
name: contentUrl
required: true
schema: { type: string, format: uri }
- in: query
name: news
required: false
schema: { type: string }
example: latest
- in: query
name: newsFrom
required: false
schema: { type: string }
example: US
responses:
'200':
description: vjnc JSON response
content:
application/json:
schema:
type: object
additionalProperties: true
/teads/paid:
get:
operationId: getPaid
summary: Fetch marketer (paid) recommendations
parameters:
- in: query
name: contentUrl
required: true
schema: { type: string, format: uri }
responses:
'200':
description: vjnc JSON response
content:
application/json:
schema:
type: object
additionalProperties: true

Auth: None needed in Actions; keys live on your proxy only.


4) Custom GPT Instructions (Prompting Template)

Add to your Instructions (System message) in the GPT builder:

  • You can call Actions to:
    1. Fire measurement events (measureEvent) when sessions begin (chatbot_load), when units render (unit_load), when ≥50% is visible for 1s (unit_view), and when a user initiates a click (unit_click).
    2. Fetch organic content (getOrganic) using the user’s page/topic URL.
    3. Fetch paid content (getPaid) for sponsored recommendations.
  • Always pass the canonical contentUrl (no query/fragment).
  • Prefer news=latest & newsFrom aligned with user region for organic.
  • Do not rewrite or modify click URLs in responses.
  • Include disclosures when displaying sponsored/paid items.
  • Use test mode environment during QA (proxy handles it).

Optional tool-use policy (developer instructions):

  • Call measureEvent with extid=chatbot_load once per new session.
  • After showing any recommendation list, call measureEvent with extid=unit_load.
  • When user scrolls/asks to preview → call measureEvent with extid=unit_view.
  • When user selects an item → present doc.url as-is (and state it will open externally).

5) Example Tool Calls from ChatGPT

Start of session:

{
"tool_name": "measureEvent",
"parameters": {
"contentUrl": "https://example.com/page-about-topic",
"extid": "chatbot_load"
}
}

Fetch organic:

{
"tool_name": "getOrganic",
"parameters": {
"contentUrl": "https://example.com/page-about-topic",
"news": "latest",
"newsFrom": "US"
}
}

Fetch paid:

{
"tool_name": "getPaid",
"parameters": {
"contentUrl": "https://example.com/page-about-topic"
}
}

Render guidance (what your GPT should display):

  • Title, short description, publisher/source.
  • Paid: label as Sponsored.
  • Provide a “Open link” button using doc.url unchanged.
  • Optional: doc.orig_url for organic if you choose split-click handling (navigate to orig_url, ping doc.url).

6) Click Handling Inside ChatGPT

  • For paid: Always navigate to doc.url as provided (e.g., http://paid.outbrain.com/network/redir...).
  • For organic: you may (a) open doc.url directly or (b) open doc.orig_url and ping doc.url in parallel (server-side recommended).

Because ChatGPT UIs cannot execute parallel browser pings, implement click registration in your proxy if needed (e.g., a /click endpoint that performs a server-side GET to doc.url before redirecting the user).

Example (optional) proxy route:

app.get("/click", async (req, res) => {
const { logUrl, dest } = req.query; // logUrl = doc.url, dest = doc.orig_url
try {
if (logUrl) await fetch(logUrl, { method: "GET" });
} catch (_) {}
res.redirect(dest || logUrl || "/");
});

7) Response Shaping (Optional)

You can have the proxy return a normalized payload to make the GPT’s rendering simpler, for example:

{
"items": [
{
"title": "Headline",
"source": "Publisher",
"type": "paid|organic",
"summary": "Short snippet...",
"click": { "url": "doc.url", "orig_url": "doc.orig_url" },
"trackers": {
"pixels": ["..."],
"jsTrackers": ["..."],
"clickTrackers": ["..."]
}
}
]
}

This is optional; by default you can return raw vjnc JSON from Teads/Partner.


8) QA & Validation

  • Keep TEADS_TEST_MODE=true in staging.
  • Confirm:
    • Measurement events fire within 3 minutes of render.
    • contentUrl is canonical and URI-encoded server-side.
    • format=vjnc, idx=0, cors=true present.
    • Paid clicks use doc.url verbatim.
  • Add a Sponsored badge for paid items.

9) Privacy & Security for Actions

  • Never send API keys through Actions; keep them on the proxy.
  • Use HTTPS end-to-end; disable HTTP.
  • Sanitize/validate contentUrl and user-provided inputs.
  • Respect regional consent via your CMP; block personalized flows until consent is true.
  • Log without keys; rotate keys periodically.

10) Production Checklist

  • TEADS_TEST_MODE=false
  • Public proxy behind WAF/rate limiting
  • Health check & timeouts (3s default)
  • Error mapping to helpful messages in GPT
  • Observability (request ID, minimal metrics)
  • Click handling proven for paid vs organic

11) Quick Smoke Tests (cURL)

# 1) Session load event
curl -G "https://YOUR_PUBLIC_PROXY_HOST/teads/measure" --data-urlencode "contentUrl=https://example.com/topic" --data-urlencode "extid=chatbot_load"

# 2) Organic
curl -G "https://YOUR_PUBLIC_PROXY_HOST/teads/organic" --data-urlencode "contentUrl=https://example.com/topic" --data-urlencode "news=latest" --data-urlencode "newsFrom=US"

# 3) Paid
curl -G "https://YOUR_PUBLIC_PROXY_HOST/teads/paid" --data-urlencode "contentUrl=https://example.com/topic"

12) Troubleshooting

SymptomLikely CauseFix
403 on any callWrong key or widget IDVerify env vars; check which flow uses which key
Empty organicMissing news/newsFromProvide news=latest&newsFrom=US
Clicks not countedURL modifiedUse doc.url exactly; use /click proxy if needed
Slow responsesHigh latency upstreamAdd 3s timeout + retry/backoff on proxy

That’s it! Import the Action, deploy the proxy, and your Custom ChatGPT can query Teads content and report events safely.

— Teads AI Platform