ChatGPT Ads Insights API: Report Non-Goal Conversions (attributed_events Guide, 2026)
Optimise a ChatGPT Ads campaign for leads and still report the purchases it drove. How the Insights API's attributed_events expansion works: the request, every response field, attribution windows, time basis, event selection, HTTP 413 limits, ROAS fields, and a Python example.

"include": ["attributed_events"] to a POST /v1/conversions/insights request and each result row gains an attributed_events array — one entry per event name, with click-through and view-through counts and value. The practical upshot: you can optimise a campaign for lead_created and still report the order_created purchases it drove, without touching the campaign's conversion goal or optimisation settings.Until this change, a conversion-optimised campaign could only tell you about the one event it was optimising toward. That forced a bad trade. Optimise for the high-volume event the auction can learn from — a lead, a sign-up, an add-to-cart — and you lost visibility of the revenue event your finance team actually cares about. Optimise for the revenue event and you starved the model of volume. Non-goal reporting removes the trade: optimise for what the auction needs, report on what the business needs.
Goal events vs non-goal events
| Goal event | Non-goal event | |
|---|---|---|
| Definition | A conversion event configured and attached to the campaign as its conversion goal | Any other attributed event recognised for your account, standard or custom |
Counted in conversions | Yes | No |
| Drives oCPC / oCPM optimisation | Yes | No |
| Visible via the API | Always | With "include": ["attributed_events"] |
| Example | lead_created on a lead-gen campaign | order_created purchases the same campaign's clicks later produced |
Two rules frame everything else. First, goal reporting applies going forward from attachment — attaching a goal today does not backfill last week. Second, adding the non-goal expansion never changes the goal numbers. OpenAI states that adding or removing include "leaves the reporting clock, windows, and metric values for matching summary rows unchanged." It only adds detail.
The request
Non-goal reporting lives on the dedicated conversion endpoint, not on the general Insights endpoints.
curl -X POST "https://api.ads.openai.com/v1/conversions/insights" \
-H "Authorization: Bearer ${OPENAI_ADS_API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"aggregation_level": "campaign",
"time_granularity": "none",
"time_ranges": [
"{\"type\":\"unix_range\",\"start\":\"1788235200\",\"end\":\"1788840000\"}"
],
"entity_ids": ["cmpn_123"],
"attribution_time_basis": "ad_event_time",
"attribution_window_days": 30,
"view_through_attribution_window_days": 1,
"include": ["attributed_events"]
}'That example is OpenAI's own: one campaign, a single period total, Unix timestamps running from midnight on 1 September to midnight on 8 September in the account's time zone. Note that each time_ranges entry is a JSON-encoded string, not a nested object.
| Parameter | Accepted values | Default |
|---|---|---|
aggregation_level | campaign, ad_group, ad | — |
time_granularity | daily for daily rows, none for period totals | — |
time_ranges | Full days in the account's time zone, exclusive end, up to 365 days | — |
entity_ids | The campaign, ad group or ad IDs to report | — |
attribution_window_days | 7, 14, 30 (days after a click) | 30 |
view_through_attribution_window_days | 0 or 1 (days after an impression); 0 excludes view-through | 1 |
attribution_time_basis | "ad_event_time" or "conversion_time" | "ad_event_time" |
include | ["attributed_events"] to expand non-goal events | Not included |
event_names | Exact standard or custom event names to filter the expansion | All attributed events |
breakdown | "country" or "device" | None |
include_zero_rows | true or false | true |
Reading the response
OpenAI's illustrative response shows a campaign with zero goal conversions that nonetheless drove two attributed purchases:
{
"object": "list",
"account_currency": "USD",
"data": [
{
"entity_id": "cmpn_123",
"conversions": 0,
"click_through_conversions": 0,
"view_through_conversions": 0,
"order_created_attributed_sales": "100",
"order_created_attributed_sales_currency": "USD",
"attributed_events": [
{
"entity_id": "cmpn_123",
"event_name": "order_created",
"event_kind": "standard",
"attributed_event_count": 2,
"click_through_conversions": 2,
"view_through_conversions": 0,
"attributed_event_value_amount": 100,
"attributed_event_value_count": 2,
"attributed_event_value_currency": "USD",
"conversion_event_setting_breakdowns": []
}
]
}
],
"count": 1
}| Field | Level | What it means |
|---|---|---|
conversions | Summary row | Goal conversions only — click-through plus view-through within your windows |
click_through_conversions / view_through_conversions | Summary row | The goal total split by attribution type. View-through is 0 when the view window is 0 |
order_created_attributed_sales (+ _currency) | Summary row | Attributed purchase value from order_created events, whether or not purchase is the goal |
attributed_events[] | Summary row | One entry per event name — present only with the include expansion |
event_name / event_kind | Event entry | The event, and whether it is standard or custom |
attributed_event_count | Event entry | All attributed activity for that event — goal and non-goal combined |
click_through_conversions / view_through_conversions | Event entry | That event's attributed count split by click and view |
attributed_event_value_amount, _count, _currency | Event entry | Total value and how many events carried a value |
conversion_event_setting_breakdowns | Event entry | Which goal settings matched. An empty array means no goal settings matched — the event is purely non-goal |
count | Response | The number of summary rows, not the number of events |
conversions counts goal conversions; the nested attributed_event_count includes goal and non-goal activity for that event, so never add them together. (2) "Unavailable monetary values are null, not zero" — treat null as unknown, not as no revenue. (3) If an outcome qualifies for both a click and a view, the click takes precedence, so click-through and view-through counts never double-count one conversion.Attribution windows and the time basis
The dedicated endpoint applies the windows you pass — independently of which events you select — and defaults to a 30-day click window, a 1-day view window, and ad-event time. Those defaults differ from how many older integrations assumed the endpoint worked. OpenAI's warning is direct: "Existing integrations that relied on click-only totals or conversion-date defaults should review their reporting assumptions. The dedicated endpoint now uses the selected click/view windows and defaults to ad-event time; it does not provide a legacy reporting mode."
| Time basis | Each date means | Use it when |
|---|---|---|
ad_event_time (default) | The date of the ad click or impression that got credit | Judging a campaign or creative by when it ran; required for reliable non-goal reporting |
conversion_time | The date the conversion actually happened | Reconciling against an order table or CRM by order date — goal events only |
The line that matters for this feature: "Conversion-time reporting has limited coverage of non-goal events; use ad-event time when reporting on those events." If your non-goal purchases look suspiciously low, check the time basis first.
To make API numbers match Ads Manager, OpenAI says to use the same date range, time basis, and click and view windows in both. Ads Manager exposes the same controls under Edit columns → Conversion Events: 7, 14 or 30-day click windows, a 0 or 1-day view window, and Conversions (by conv. time). More on reading those in the reporting guide.
Selecting specific events
Omit event_names to return every attributed event. To narrow the expansion, pass exact names:
{
"include": ["attributed_events"],
"event_names": ["order_created", "newsletter_signup"]
}- Names must be recognised for your account. That means configured in conversion event settings — including archived settings — or present in published received-event history. They do not need to be a campaign goal or have activity in the period you request.
- Brand-new custom events are not selectable immediately. They become selectable after their received-event history is published, and publication delays and retention limits apply.
- Invalid selectors return HTTP 400.
- Selecting names filters the nested details only. It does not recalculate goal counts or summary sales. A retained row with no matching events shows
attributed_events: []— which OpenAI notes does not confirm that historical event processing is complete.
Empty rows, large reports and HTTP 413
include_zero_rows defaults to true. Set it to false with the expansion and a row survives only if its goal count, summary sales amount, or a selected event count is non-zero. OpenAI's example: a row with no goal conversions but USD 100 in sales stays in the report, with attributed_events: [], when you select an inactive event.
There is no pagination. The endpoint returns HTTP 413 when a report exceeds 2,000 summary rows, 2,000 event rows, or when an event's goal-setting breakdown exceeds 2,000 entries — with no partial report and no cursor. The fix is to split the request:
- Fewer
entity_idsper call. - Shorter, non-overlapping date ranges.
- Fewer
event_namesin the expansion. time_granularity: "none"instead ofdailywhen you only need totals.
Revenue and ROAS from the general Insights endpoint
For spend and purchase value side by side, use the general Insights endpoints, which return order_created_attributed_sales, its currency, and order_created_roas:
curl -G "https://api.ads.openai.com/v1/ad_account/insights" \
-H "Authorization: Bearer ${OPENAI_ADS_API_KEY}" \
--data-urlencode 'aggregation_level=campaign' \
--data-urlencode 'time_granularity=none' \
--data-urlencode 'time_ranges[]={"type":"date_range","since":"2026-09-01","until":"2026-09-07","timezone":"America/New_York"}' \
--data-urlencode 'fields[]=campaign_id' \
--data-urlencode 'fields[]=campaign_name' \
--data-urlencode 'fields[]=spend' \
--data-urlencode 'fields[]=conversions' \
--data-urlencode 'fields[]=order_created_attributed_sales' \
--data-urlencode 'fields[]=order_created_attributed_sales_currency' \
--data-urlencode 'fields[]=order_created_roas'USD 1,000 in attributed sales against USD 250 in spend returns a ROAS of 4. These metrics only exist if your order_created events carry a value — the most common reason a ROAS column reads empty is a purchase event sent without amount and currency. See the conversion tracking guide.
| Endpoint | Best for |
|---|---|
GET /v1/ad_account/insights | Account-wide reporting broken out by campaign, ad group or ad |
GET /v1/campaigns/{campaign_id}/insights | One campaign, including daily conversions beside clicks and spend |
GET /v1/ad_groups/{ad_group_id}/insights | One ad group |
GET /v1/ads/{ad_id}/insights | One ad |
POST /v1/conversions/insights | Goal totals, click vs view split, sales, and non-goal event detail |
Four ways to use non-goal reporting
These are our playbooks, built on the documented behaviour above.
1. Optimise for leads, prove revenue
A B2B or high-ticket campaign optimises toward lead_created because purchases are too rare for the model to learn from. Pull order_created as a non-goal event on the same campaigns, at the same windows, and you can finally report cost per customer rather than cost per lead — without moving the goal and resetting learning.
2. Choose the next goal with evidence
Before switching a campaign's optimisation event, pull every attributed event for the last 30 days. The event with enough volume and a stable relationship to revenue is your next goal. Because a campaign's conversion event cannot be changed after creation, this analysis decides what the new campaign should optimise for.
3. Measure the whole funnel from one campaign
Omit event_names and you get contents_viewed, items_added, checkout_started and order_created attributed to the same entity, date and breakdown. That is a per-campaign funnel without stitching three reports together.
4. Keep reporting honest when the goal is not revenue
If a finance team only trusts purchase numbers, report order_created_attributed_sales and order_created_roas for every campaign regardless of goal. The goal event becomes an internal optimisation detail rather than the headline.
A minimal Python example
A sketch that pulls non-goal purchases for a list of campaigns and computes cost per purchase. It assumes you have already fetched spend per campaign from the general Insights endpoint.
import json, os, requests
API = "https://api.ads.openai.com/v1/conversions/insights"
HEADERS = {"Authorization": f"Bearer {os.environ['OPENAI_ADS_API_KEY']}"}
def non_goal_purchases(campaign_ids, start_unix, end_unix):
body = {
"aggregation_level": "campaign",
"time_granularity": "none",
"time_ranges": [json.dumps({"type": "unix_range",
"start": str(start_unix), "end": str(end_unix)})],
"entity_ids": campaign_ids,
"attribution_time_basis": "ad_event_time", # required for non-goal coverage
"attribution_window_days": 30,
"view_through_attribution_window_days": 1,
"include": ["attributed_events"],
"event_names": ["order_created"],
}
r = requests.post(API, headers=HEADERS, json=body, timeout=60)
if r.status_code == 413:
raise RuntimeError("Report too large: split entity_ids or the date range")
r.raise_for_status()
out = {}
for row in r.json()["data"]:
purchases = sum(e["attributed_event_count"] for e in row.get("attributed_events", []))
out[row["entity_id"]] = {
"goal_conversions": row["conversions"],
"purchases": purchases,
"sales": row.get("order_created_attributed_sales"), # may be None, never assume 0
}
return out
# cost_per_purchase = spend / purchases, where purchases > 0Freshness and retention
| Data | When it updates or how long it lasts |
|---|---|
| Impressions, clicks, CTR | Can appear within minutes; recent values can still change |
| Spend, CPC, CPM | Reflect finalised activity and can lag impressions and clicks |
| Conversions and purchase outcomes | Daily processing — allow at least one day |
| Hourly delivery reporting | About 30 days of history |
| Non-hourly delivery reporting (excluding product reports) | The most recent 365 days |
| Product reporting | About 30 days |
| Conversion endpoint, daily rows | Up to 365 days per request |
Two consequences. Pull conversion data no earlier than the day after the period closes, and re-pull the last few days on each run because late events are still being attributed. And if you want more than 30 days of product or hourly history, export it on a schedule — the API will not hold it for you.
Sources and further reading
- OpenAI developer docs — Reporting & Insights, including Report goal and non-goal events
- OpenAI Help Center — Measure Results
- OpenAI Help Center — Conversion-optimized Campaigns
- GPT Ads AI — reporting guide, conversion campaigns, conversion tracking, event quality
Quick answers.
Can I report conversions that are not my ChatGPT Ads campaign goal?
Yes. Add "include": ["attributed_events"] to a POST /v1/conversions/insights request and each row returns an attributed_events array with every attributed event, including events that are not configured as the campaign's conversion goal. This does not change the campaign's goal or optimisation settings.
What is a non-goal event in ChatGPT Ads?
An attributed conversion event that is recognised for your ad account but is not configured as the campaign's conversion goal. For example, order_created purchases on a campaign that optimises for lead_created. Non-goal events are not counted in the conversions field and do not drive optimisation.
What is the difference between conversions and attributed_event_count?
The outer conversions field counts only campaign goal conversions. The nested attributed_event_count counts all attributed activity for that event name, goal and non-goal combined. Do not add them together.
Why are my non-goal conversions missing when I use conversion_time?
OpenAI states that conversion-time reporting has limited coverage of non-goal events. Use attribution_time_basis set to ad_event_time, which is the default, when reporting on non-goal events.
What attribution windows does the ChatGPT Ads conversions insights endpoint use?
attribution_window_days accepts 7, 14 or 30 and defaults to 30 days after a click. view_through_attribution_window_days accepts 0 or 1 and defaults to 1 day after an impression; 0 excludes view-through attribution.
Why does my ChatGPT Ads API report return HTTP 413?
The conversions insights endpoint returns 413 when a report exceeds 2,000 summary rows, 2,000 event rows, or 2,000 goal-setting breakdown entries for an event. There is no pagination, so split the request into fewer entity IDs, shorter non-overlapping date ranges, or fewer event_names.
Which event names can I request as non-goal events?
Exact standard or custom event names recognised for your account — configured in conversion event settings, including archived settings, or present in published received-event history. They do not need to be campaign goals or have activity in the requested period. Invalid names return HTTP 400.
How do I get ROAS from the ChatGPT Ads API?
Request order_created_attributed_sales, order_created_attributed_sales_currency and order_created_roas from a general Insights endpoint such as GET /v1/ad_account/insights. They are calculated only from attributed order_created events that carry a value.
Why don't my API conversions match Ads Manager?
The dedicated endpoint defaults to a 30-day click window, a 1-day view window and ad-event time. Use the same date range, time basis and click and view windows as your Ads Manager columns, and allow at least a day for conversion processing.
How far back can I pull ChatGPT Ads reporting data?
Non-hourly delivery reporting covers the most recent 365 days, and daily conversion requests can cover up to 365 days. Hourly delivery and product reporting keep roughly 30 days.
Related guides.
ChatGPT Ads Reporting
Ads Manager reports one combined Conversions number by default — and most advertisers never find the menu that breaks it apart. Metrics, columns, exports, macros and the troubleshooting tree.
Read guideConversion Campaigns (oCPC)
The Conversions objective, oCPC bidding mechanics, how to pick the event you optimise toward, Bid Cap strategy, the July 2026 average daily budget change, the API fields — and the mistakes that quietly kill conversion campaigns.
Read guideConversion Tracking
The oaiq pixel snippet, every supported standard event, the image tag fallback, the full Conversions API payload, deduplication, hashing rules, consent and CSP, validation — and an honest account of what this pixel cannot see.
Read guideEvent Quality Warnings
Nine named warnings, no published score, and a seven-day assessment window. Every warning explained, the exact field that clears it, the pixel-vs-API key map that silently loses match rate, and a weekly routine that catches regressions before they reach your CPA.
Read guideWant this run for you?
GPT Ads AI is a done-for-you ChatGPT Ads agency. The services below cover exactly what this guide describes.
Measurement & attribution
Pixel and Conversions API, event quality warnings cleared, attribution windows read correctly, dynamic URL parameters as the analytics join key, and cross-channel integrations that send signal back to OpenAI.
See the serviceCampaign structure, bidding & targeting
Objective selection, Maximize results versus manual bidding, platform targeting across iOS, Android and web, and budget strategy — including the decisions that cannot be reversed after launch.
See the service
Tarun Kapoor
Founder · GPT Ads AI
Performance marketer with 12+ years in paid acquisition. Former senior media buyer at Neil Patel Digital. Alumni of GroupM, WPP, Ogilvy & Mather, and Toptal's Growth Collective. Fractional CMO to Fortune 500 brands and venture-backed startups.
Book a call with Tarun