# ChatGPT Ads Insights API: Report Non-Goal Conversions (attributed_events Guide, 2026)

URL: https://www.gptadsai.com/guides/chatgpt-ads-insights-api-conversions
Author: Tarun Kapoor — Founder, GPT Ads AI (12+ years paid media)
Published: 2026-09-30
Type: Guide

> 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.

## Contents

- [Goal events vs non-goal events](#goal-events-vs-non-goal-events)
- [The request](#the-request)
- [Reading the response](#reading-the-response)
- [Attribution windows and the time basis](#attribution-windows-and-the-time-basis)
- [Selecting specific events](#selecting-specific-events)
- [Empty rows, large reports and HTTP 413](#empty-rows-large-reports-and-http-413)
- [Revenue and ROAS from the general Insights endpoint](#revenue-and-roas-from-the-general-insights-endpoint)
- [Four ways to use non-goal reporting](#four-ways-to-use-non-goal-reporting)
- [A minimal Python example](#a-minimal-python-example)
- [Freshness and retention](#freshness-and-retention)
- [Sources and further reading](#sources-and-further-reading)

> **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](https://developers.openai.com/ads/reporting). Ads Manager behaviour is from [Measure Results](https://help.openai.com/en/articles/20001214-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 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.

```bash
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:

```json
{
  "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 |

> **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 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](/guides/chatgpt-ads-reporting.md).

## Selecting specific events

Omit `event_names` to return every attributed event. To narrow the expansion, pass exact names:

```json
{
  "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`:

```bash
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](/guides/chatgpt-ads-conversion-tracking.md).

| 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.

### Want non-goal revenue in your weekly report without building the pipeline?

We wire the Insights API into your warehouse or reporting stack, match its windows to Ads Manager so the numbers agree, and report cost per customer alongside cost per lead. That is part of our [measurement and attribution service](/services/chatgpt-ads-measurement.md).

[Book a discovery call](https://calendly.com/tarunkapoor/discovery)

[Measurement service](/services/chatgpt-ads-measurement.md)

## 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.

```python
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

| 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](https://developers.openai.com/ads/reporting), including *Report goal and non-goal events*
- OpenAI Help Center — [Measure Results](https://help.openai.com/en/articles/20001214-measure-results)
- OpenAI Help Center — [Conversion-optimized Campaigns](https://help.openai.com/en/articles/20001412-conversion-optimized-campaigns)
- GPT Ads AI — [reporting guide](/guides/chatgpt-ads-reporting.md), [conversion campaigns](/guides/chatgpt-ads-conversion-campaigns.md), [conversion tracking](/guides/chatgpt-ads-conversion-tracking.md), [event quality](/guides/chatgpt-ads-event-quality.md)

## FAQ

**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

- https://www.gptadsai.com/guides/chatgpt-ads-reporting.md
- https://www.gptadsai.com/guides/chatgpt-ads-conversion-campaigns.md
- https://www.gptadsai.com/guides/chatgpt-ads-conversion-tracking.md
- https://www.gptadsai.com/guides/chatgpt-ads-event-quality.md

## Work with GPT Ads AI

- Measurement & attribution — https://www.gptadsai.com/services/chatgpt-ads-measurement.md
- Campaign structure, bidding & targeting — https://www.gptadsai.com/services/chatgpt-ads-campaign-management.md
- Book a discovery call: https://calendly.com/tarunkapoor/discovery
