Skip to main content

OpenAI Conversions

This guide covers how to submit conversion events to OpenAI through the TNT Growth API.

Authentication​

All requests require an x-api-key header with your client-specific API key provided by TNT Growth.

-H "x-api-key: YOUR_API_KEY"

Submitting a Conversion​

Endpoint​

POST /api/openai/events/add

Request Body Schema​

FieldTypeRequiredDescription
pixelIdstringYesYour OpenAI Pixel ID (provided during onboarding)
idstringYesUnique event ID for deduplication
typestringYesOpenAI event type (see below)
timestampMsnumberYesUnix timestamp in milliseconds of when the event occurred
customEventNamestringConditionalCustom event name. Required when type is custom
opprefstringNoOpenAI attribution reference captured from the ad click
sourceUrlstringConditionalURL where the event occurred. Required when actionSource is web
actionSourcestringNoWhere the event originated (see below). Defaults to offline when omitted
userDataobjectNoCustomer identifiers for matching (see below)
optOutbooleanNoForwarded to OpenAI as opt_out; does not suppress userData transmission or matching
validateOnlybooleanNoAsk OpenAI to validate the event without processing it
dataobjectYesConversion details (see below)
ingestionSourcestringNoOrigin of the event (e.g. pixel, conversion-api)
utmSourcestringNoUTM source parameter (e.g. openai) — stored for attribution reporting, and also decides delivery when it names a different paid platform. See When we withhold a conversion
utmMediumstringNoUTM medium parameter (e.g. paid-search) — stored for attribution reporting
utmCampaignstringNoUTM campaign name — stored for attribution reporting
utmTermstringNoUTM search keyword — stored for attribution reporting
utmContentstringNoUTM content parameter (ad/creative variant) — stored for attribution reporting
This endpoint does not accept tntId

Unlike the other conversion endpoints, the OpenAI path does not read the attribution handle. Nothing on this path resolves an attribution session, so the field is deliberately not declared rather than accepted and ignored. Send oppref and userData — those are what OpenAI matches on.

Timestamp window

timestampMs must be within the last 7 days and cannot be more than 10 minutes in the future.

Event Types​

type must be one of:

app_installed, app_opened, appointment_scheduled, checkout_started, contents_viewed, custom, items_added, lead_created, order_created, page_viewed, registration_completed, subscription_created, trial_started

When type is custom, include customEventName.

Action Sources​

actionSource must be one of:

web, mobile_app, offline, physical_store, phone_call, email, other

If actionSource is omitted, TNT Growth sends the event to OpenAI with offline.

Action source requirements
  • sourceUrl is required when actionSource is web
  • app_installed and app_opened require actionSource to be mobile_app

User Data Fields​

Include as many identifiers as possible to improve match quality.

FieldTypeDescription
emailstringCustomer email address. Hashed before sending to OpenAI
emailSha256stringSHA-256 hashed email address
externalIdstringYour internal customer ID. Hashed before sending to OpenAI
externalIdSha256stringSHA-256 hashed external customer ID
countrystringCustomer country
citystringCustomer city
zipCodestringCustomer postal/ZIP code
ipAddressstringIP address of the customer
userAgentstringBrowser user agent string

Raw email and externalId values are hashed on TNT Growth servers before they leave. If you already hash identifiers yourself, send emailSha256 or externalIdSha256.

Consent and optional identifiers

TNT Growth forwards any optional identifiers you include. If consent is absent for a user identifier, omit that identifier from userData; setting optOut does not remove it from the outbound OpenAI event.

Data Fields​

FieldTypeRequiredDescription
typestringYesData payload type (see below)
planIdstringNoPlan or subscription identifier
amountnumberNoInteger amount in minor currency units (for USD, 15000 represents $150.00)
currencystringConditionalCurrency code. Required when amount is present
contentsarrayNoArray of content objects (see below)

data.type must be one of: contents, customer_action, plan_enrollment, custom.

Event type and data type must match

data.type is not independent of type. OpenAI rejects a mismatched pair as event_type_data_mismatch. Flex overwrites data.type to the legal shape for the event type before sending:

  • custom → custom
  • lead_created, appointment_scheduled, registration_completed, app_installed, app_opened → customer_action
  • checkout_started, contents_viewed, items_added, order_created, page_viewed → contents
  • subscription_created, trial_started → plan_enrollment

Content Fields (data.contents[])​

FieldTypeRequiredDescription
idstringNoContent, product, or item ID
namestringNoContent, product, or item name
contentTypestringNoType/category of the content
quantitynumberNoQuantity of the content
amountnumberNoInteger amount in minor currency units (for USD, 15000 represents $150.00)
currencystringConditionalCurrency code. Required when content amount is present

The example below uses 15000 with USD, which represents $150.00.

Example Request​

TIMESTAMP_MS=$(date +%s000)

curl -X POST https://flex.tntgrowth.io/api/openai/events/add \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"pixelId": "px_openai_123456789",
"id": "order-12345",
"type": "order_created",
"timestampMs": '"${TIMESTAMP_MS}"',
"oppref": "oppref_abc123",
"sourceUrl": "https://example.com/thank-you",
"actionSource": "web",
"userData": {
"email": "customer@example.com",
"externalId": "cust-12345",
"country": "US",
"city": "Austin",
"zipCode": "78701",
"ipAddress": "203.0.113.10",
"userAgent": "Mozilla/5.0"
},
"data": {
"type": "contents",
"amount": 15000,
"currency": "USD",
"contents": [
{
"id": "SKU-001",
"name": "Treatment Plan",
"contentType": "service",
"quantity": 1,
"amount": 15000,
"currency": "USD"
}
]
},
"utmSource": "openai",
"utmMedium": "paid-search",
"utmCampaign": "brand-search-q2",
"utmTerm": "best treatment center",
"utmContent": "search-ad-variant-b"
}'

Example Response​

{
"message": "OpenAI event successfully queued for client Example Client",
"data": {
"submissionId": "8f3c2d10-7e6b-4a51-9d8c-3b2e1f0a9b8c",
"duplicate": false
}
}

When a conversion is dedup-skipped (the same event was submitted within the last 24 hours), submissionId is null and duplicate is true.

Checking Submission Status​

After submitting a conversion, use the submission ID to check processing status.

Endpoint​

GET /api/conversions/submissions/:submissionId

Example Request​

curl https://flex.tntgrowth.io/api/conversions/submissions/8f3c2d10-7e6b-4a51-9d8c-3b2e1f0a9b8c \
-H "x-api-key: YOUR_API_KEY"

Example Response​

{
"id": "8f3c2d10-7e6b-4a51-9d8c-3b2e1f0a9b8c",
"source": "OPENAI",
"externalAccountId": "px_openai_123456789",
"expectedCount": 1,
"createdAt": "2026-05-11T12:00:00.000Z",
"scheduledFor": null,
"counts": {
"success": 1,
"failed": 0
},
"status": "complete"
}

Status Values​

StatusDescription
pendingConversion is queued and waiting to be processed
pending_deliverySubmission is scheduled for future delivery. Currently only emitted by Google Ads ingest paths; OpenAI submissions never enter this state.
completeEvery conversion reached a final outcome and none was rejected. Usually means all were delivered — but a withheld conversion also finishes here, so compare counts.success with expectedCount if you need delivery confirmed
complete_with_errorsProcessing finished but some conversions failed — use GET /api/conversions/openai/failed?submissionId=<id> to fetch the actual error messages
stalledProcessing has not progressed — contact support

Withheld Conversions​

A conversion is withheld when its attribution says another paid channel served the click — a utmSource naming a different platform, with no OpenAI click ID beside it. Uploading it would train OpenAI's bidding on traffic OpenAI never sent, so we hold it back instead. The full rules, and the utm_source values that count as a claim, are in When we withhold a conversion from a platform.

This is not an error. A withheld pair appears in counts.withheld, is excluded from counts.success, and never counts as failed — failed means the platform rejected it. A submission whose only non-delivered pairs were withheld finishes as complete, so compare counts.success with expectedCount if you need delivery confirmed.

{
"expectedCount": 2,
"counts": {
"success": 1,
"failed": 0,
"withheld": 1
},
"withheld": [
{
"reason": "cross_channel",
"claimedPlatform": "google",
"count": 1
}
],
"status": "complete"
}

Both counts.withheld and the withheld array are present only when something was withheld, so a submission with nothing withheld returns exactly the shape it always has. Each entry carries reason (always cross_channel today), claimedPlatform (google, meta, bing, tiktok, linkedin, reddit or openai) and count; the entries sum to counts.withheld. Nothing is discarded — the payload is kept, so a withheld conversion can still be delivered later if your tagging or the rules change. Re-sending the same payload will be withheld again; send the OpenAI click ID alongside utmSource and the click ID wins.

The scheduledFor field is always null for OpenAI submissions and is included only for schema parity with Google Ads, which uses it for deferred delivery.

Inspecting Failed Conversions In Your Batch​

When a submission ends with complete_with_errors, the submission status endpoint surfaces only the count of failed rows. Fetch the actual error messages with:

GET /api/conversions/openai/failed?submissionId=<submissionId>

The response payload mirrors the Google Ads failed-conversion shape — see Google Ads → Inspecting Failed Conversions In Your Batch.

Best Practices​

  • Use a stable id for deduplication — send the same id if retrying the same conversion event
  • Include oppref when available — this is OpenAI's attribution reference and provides the strongest click-level attribution signal
  • Send user identifiers like email, externalId, ipAddress, and userAgent to improve match quality
  • Use accurate millisecond timestamps — timestampMs must reflect when the event actually occurred and must be within OpenAI's accepted time window
  • Include sourceUrl for web events — OpenAI web events require it when actionSource is web
  • Poll with a delay — wait 30-60 seconds after submission before checking status