Try Measure.co — take control of your AI Search Visibility. Visit Measure.co

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.

Tarun Kapoor 13 min readPublished September 30, 2026
ChatGPT Ads Insights API: Report Non-Goal Conversions (attributed_events Guide, 2026)
Short answer. The ChatGPT Ads Insights API can now return attributed conversion events that are not your campaign's goal. Add "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.

Sources. Every endpoint, parameter and field name below is from OpenAI's Reporting & Insights developer documentation. Ads Manager behaviour is from Measure Results. Use cases and the Python example are ours. Field names are case-sensitive; copy them exactly.

Goal events vs non-goal events

Goal eventNon-goal event
DefinitionA conversion event configured and attached to the campaign as its conversion goalAny other attributed event recognised for your account, standard or custom
Counted in conversionsYesNo
Drives oCPC / oCPM optimisationYesNo
Visible via the APIAlwaysWith "include": ["attributed_events"]
Examplelead_created on a lead-gen campaignorder_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.

ParameterAccepted valuesDefault
aggregation_levelcampaign, ad_group, ad—
time_granularitydaily for daily rows, none for period totals—
time_rangesFull days in the account's time zone, exclusive end, up to 365 days—
entity_idsThe campaign, ad group or ad IDs to report—
attribution_window_days7, 14, 30 (days after a click)30
view_through_attribution_window_days0 or 1 (days after an impression); 0 excludes view-through1
attribution_time_basis"ad_event_time" or "conversion_time""ad_event_time"
include["attributed_events"] to expand non-goal eventsNot included
event_namesExact standard or custom event names to filter the expansionAll attributed events
breakdown"country" or "device"None
include_zero_rowstrue or falsetrue

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
}
FieldLevelWhat it means
conversionsSummary rowGoal conversions only — click-through plus view-through within your windows
click_through_conversions / view_through_conversionsSummary rowThe goal total split by attribution type. View-through is 0 when the view window is 0
order_created_attributed_sales (+ _currency)Summary rowAttributed purchase value from order_created events, whether or not purchase is the goal
attributed_events[]Summary rowOne entry per event name — present only with the include expansion
event_name / event_kindEvent entryThe event, and whether it is standard or custom
attributed_event_countEvent entryAll attributed activity for that event — goal and non-goal combined
click_through_conversions / view_through_conversionsEvent entryThat event's attributed count split by click and view
attributed_event_value_amount, _count, _currencyEvent entryTotal value and how many events carried a value
conversion_event_setting_breakdownsEvent entryWhich goal settings matched. An empty array means no goal settings matched — the event is purely non-goal
countResponseThe number of summary rows, not the number of events
Three reading rules that prevent most mistakes. (1) The outer 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 basisEach date meansUse it when
ad_event_time (default)The date of the ad click or impression that got creditJudging a campaign or creative by when it ran; required for reliable non-goal reporting
conversion_timeThe date the conversion actually happenedReconciling 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_ids per call.
  • Shorter, non-overlapping date ranges.
  • Fewer event_names in the expansion.
  • time_granularity: "none" instead of daily when 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.

EndpointBest for
GET /v1/ad_account/insightsAccount-wide reporting broken out by campaign, ad group or ad
GET /v1/campaigns/{campaign_id}/insightsOne campaign, including daily conversions beside clicks and spend
GET /v1/ad_groups/{ad_group_id}/insightsOne ad group
GET /v1/ads/{ad_id}/insightsOne ad
POST /v1/conversions/insightsGoal 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 > 0

Freshness and retention

DataWhen it updates or how long it lasts
Impressions, clicks, CTRCan appear within minutes; recent values can still change
Spend, CPC, CPMReflect finalised activity and can lag impressions and clicks
Conversions and purchase outcomesDaily processing — allow at least one day
Hourly delivery reportingAbout 30 days of history
Non-hourly delivery reporting (excluding product reports)The most recent 365 days
Product reportingAbout 30 days
Conversion endpoint, daily rowsUp 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

People Also Ask

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.

Tarun Kapoor
About the author

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

Want this run
for you?

Book a discovery call