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
| Field | Type | Required | Description |
|---|---|---|---|
pixelId | string | Yes | Your OpenAI Pixel ID (provided during onboarding) |
id | string | Yes | Unique event ID for deduplication |
type | string | Yes | OpenAI event type (see below) |
timestampMs | number | Yes | Unix timestamp in milliseconds of when the event occurred |
customEventName | string | Conditional | Custom event name. Required when type is custom |
oppref | string | No | OpenAI attribution reference captured from the ad click |
sourceUrl | string | Conditional | URL where the event occurred. Required when actionSource is web |
actionSource | string | No | Where the event originated (see below). Defaults to offline when omitted |
userData | object | No | Customer identifiers for matching (see below) |
optOut | boolean | No | Forwarded to OpenAI as opt_out; does not suppress userData transmission or matching |
validateOnly | boolean | No | Ask OpenAI to validate the event without processing it |
data | object | Yes | Conversion details (see below) |
ingestionSource | string | No | Origin of the event (e.g. pixel, conversion-api) |
utmSource | string | No | UTM source parameter (e.g. openai) — stored for attribution reporting |
utmMedium | string | No | UTM medium parameter (e.g. paid-search) — stored for attribution reporting |
utmCampaign | string | No | UTM campaign name — stored for attribution reporting |
utmTerm | string | No | UTM search keyword — stored for attribution reporting |
utmContent | string | No | UTM content parameter (ad/creative variant) — stored for attribution reporting |
tntIdUnlike 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.
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.
sourceUrlis required whenactionSourceiswebapp_installedandapp_openedrequireactionSourceto bemobile_app
User Data Fields
Include as many identifiers as possible to improve match quality.
| Field | Type | Description |
|---|---|---|
email | string | Customer email address. Hashed before sending to OpenAI |
emailSha256 | string | SHA-256 hashed email address |
externalId | string | Your internal customer ID. Hashed before sending to OpenAI |
externalIdSha256 | string | SHA-256 hashed external customer ID |
country | string | Customer country |
city | string | Customer city |
zipCode | string | Customer postal/ZIP code |
ipAddress | string | IP address of the customer |
userAgent | string | Browser 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.
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
| Field | Type | Required | Description |
|---|---|---|---|
type | string | Yes | Data payload type (see below) |
planId | string | No | Plan or subscription identifier |
amount | number | No | Integer amount in minor currency units (for USD, 15000 represents $150.00) |
currency | string | Conditional | Currency code. Required when amount is present |
contents | array | No | Array of content objects (see below) |
data.type must be one of: contents, customer_action, plan_enrollment, custom.
Content Fields (data.contents[])
| Field | Type | Required | Description |
|---|---|---|---|
id | string | No | Content, product, or item ID |
name | string | No | Content, product, or item name |
contentType | string | No | Type/category of the content |
quantity | number | No | Quantity of the content |
amount | number | No | Integer amount in minor currency units (for USD, 15000 represents $150.00) |
currency | string | Conditional | Currency 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
| Status | Description |
|---|---|
pending | Conversion is queued and waiting to be processed |
pending_delivery | Submission is scheduled for future delivery. Currently only emitted by Google Ads ingest paths; OpenAI submissions never enter this state. |
complete | Conversion processed successfully |
complete_with_errors | Processing finished but some conversions failed — use GET /api/conversions/openai/failed?submissionId=<id> to fetch the actual error messages |
stalled | Processing has not progressed — contact support |
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
idfor deduplication — send the sameidif retrying the same conversion event - Include
opprefwhen available — this is OpenAI's attribution reference and provides the strongest click-level attribution signal - Send user identifiers like
email,externalId,ipAddress, anduserAgentto improve match quality - Use accurate millisecond timestamps —
timestampMsmust reflect when the event actually occurred and must be within OpenAI's accepted time window - Include
sourceUrlfor web events — OpenAI web events require it whenactionSourceisweb - Poll with a delay — wait 30-60 seconds after submission before checking status