System design · Worked example

How to collect ChatGPT Ads reports

11 min readUpdated Guide preview

Build a read-only report of daily spend by campaign. Return the rows you received, their totals, and enough detail to tell whether the report is complete and current. Try the collector locally with fictional data. Live setup is in development and requires verified advertiser API access.

On this page

Use this guide to

  • Turn a question about daily campaign spend into an Insights request.
  • Collect the result in batches and show which rows, dates, or metrics are missing.
  • Decide whether the result is ready for a decision or safe to pass to an agent.

Before you start

The examples use HTTP, the rules software follows to send a request over the web and receive a reply.

No account or API key is needed for this guide.

Decide what the report should cover

Start with results by campaign and day for one ad account you have permission to use. Choose the dates and metrics, then keep the account’s time zone, currency and the time you collected the data with the report. A live request needs separate Advertiser API access. Reporting definitions and data updates

Harness example of how to collect and save a report
  1. 01Request data from your server
  2. 02Collect results in batches
  3. 03Save data and progress
  4. 04Show results and gaps

Reports show totals for the campaigns or ads you ask about. Individual conversations, the ads considered for them and their ranking scores stay inside the platform. Keep the Ads API key, a secret your software uses to sign in, on your server. Authentication and account scope

Define the result before choosing the interface. Return the requested scope, the scope actually covered, and a status with the reason collection stopped. A subtotal should identify the rows it covers so the caller can tell whether it answers the original question.

Explore System design

Ask Insights for daily campaign results

Insights is the part of the Ads API that returns performance reports. This request asks for one row per campaign per day, with the campaign ID, date, impressions, clicks and spend. Insights request reference

Example API request · sample dateshttp
GET https://api.ads.openai.com/v1/ad_account/insights

aggregation_level=campaign
time_granularity=daily
fields[]=metadata.readable_time
fields[]=campaign.id
fields[]=campaign.impressions
fields[]=campaign.clicks
fields[]=campaign.spend
time_ranges[]={"type":"unix_range","start":1793491200,"end":1793577600}

# Query values must be URL-encoded.
# Authorisation is supplied by your server, never this page.

This request covers 1 November 2026 in UTC, the time standard used for the example. The end time is midnight at the start of 2 November, so the request excludes that day. Check the account’s reporting time zone if you want a local calendar day. The download labels OpenAI’s request fields and our own report fields separately.

Check that you have every batch

InterfaceHow to get the full result
General InsightsInsights sends results in batches called pages. Save first_id, last_id and has_more. If has_more is true, send the same request with after set to the last_id from that page. Stop when has_more is false.
Dedicated conversion reportA request to POST /conversions/insights can return error 413 if the report is too large. In that case you get no rows or marker for a next page. Split the request by campaign or other ID, or use shorter date ranges that do not overlap.
Local sample collectorTrack the page markers already seen and the campaign and date pairs already accepted. Stop on a repeated marker or duplicate row and retain the earlier valid batches.

Pagination means asking for the next page of the same result. Splitting an oversized conversion report means making several smaller requests and combining their results. Use the rules for the report you asked for. Reporting limits

Explore Algorithms & data

Show what your report is missing

Sample report from Harness · made-up datajson
{
  "reference_kind": "illustrative-reporting-envelope",
  "account_ref": "sample-account",
  "status": "partial",
  "coverage": {"completed_partitions": 2, "expected_partitions": 3},
  "missing_partitions": ["campaign-group-c"],
  "stop_reason": "retry_budget_exhausted"
}

In this example, two of three batches are ready and the remaining batch is named. Give temporary failures a bounded retry budget. If access is denied or the results cannot be validated, stop and return the batches you already accepted with the reason the report is incomplete.

To resume across runs in production, save the original request, accepted batches, and the next cursor. Use the campaign and date to avoid counting a row twice within this report. When figures arrive late, reconcile the newer snapshot for the same scope instead of adding both snapshots together.

Explore Software & production

Can you use this report?

Prepared examples · Fictional data

The request in each example

Get daily impressions, clicks, spend and credited conversions for campaigns in a fictional UK ad account on 1 and 2 November 2026.

Time zone and currency
London time, GBP
Conversion credit
30 days after a click or one day after a view. Results use the date of the ad interaction.

Open an example to see what came back and what you can do with it.

Complete reportThe final batch arrived

What came back

Both batches arrived, giving you three rows. Each row covers one campaign and day.

Impressions
2,300
Clicks
64
Spend
£36.00
Credited conversions
Total unavailable

One row shows zero conversions and another has no conversion count. The spend total covers all the returned rows, but there isn’t enough information to add up conversions.

What to do next

Use the spend total for the requested period. Record when you collected it and check for later updates before using recent figures to make a decision.

Empty reportThe request finished with zero rows

What came back

The first batch was empty and said there were no more batches. There are no totals to show.

No rows matched the request. That gives you no spend or conversion figures to interpret.

What to do next

Check the account, dates and filters. If those are right, report “no rows returned” and keep the totals blank.

Partial reportAnother batch is missing

What came back

The first batch returned two rows and a marker for the next batch. The next batch never arrived.

Rows received
2
Spend in rows received
£30.50

£30.50 is the subtotal for the rows you have. The spend for the full period is still unknown.

What to do next

Keep the original dates and filters and use the saved marker to retry the missing batch. If you can’t recover it, label the report partial and carry the gap into any summary.

Stale reportAll batches arrived, but the figures are old

What came back

At a review on 5 November, the team still had a complete report collected on 3 November. They refreshed the same dates and received revised figures.

Same reporting dates, two collection times
Metric3 Nov, 09:005 Nov, 09:00
Spend£36.00£36.30
Credited conversionsUnavailable3

Receiving every batch tells you the collection finished. It doesn’t tell you whether the platform has since updated the figures.

What to do next

Refresh the same request before using the old copy. Use the newer figures for the same dates, and retain both collection times so readers can see what changed.

Rejected requestThe requester lacked access

What came back

In this example, the reporting tool refused the request because the signed-in person didn’t have permission for the selected account. It returned no report.

The tool stopped at its access check. There are no figures to interpret for that account.

What to do next

Check that you selected the right account and ask an account admin to confirm your access. Retry once the account or permission has been corrected.

Interpret credited conversions

Before comparing reports, check that the account, dates, time zone, currency, and attribution settings match. Check coverage and freshness too. An unavailable conversion count represents missing information. A newer snapshot may have a different count for the same dates.

An attributed conversion is an action the platform credits to an ad under its reporting rules. To estimate how many additional sales the campaign caused, define the outcome and compare it with a credible estimate of what would have happened without the campaign. A suitable holdout can provide that comparison.

Explore Measurement & ML

Give an agent a bounded reporting tool

Give the tool one job: read a campaign report within an explicit scope. For a live design, the service should enforce account access and request limits. Return the scope, status, coverage, and freshness alongside the values so an agent can explain an incomplete result before recommending a decision.

The local MCP tool reads the bundled fictional reports. Its inputs select a scenario and a page limit. Try the partial scenario and check whether the agent can explain which totals are available and why collection stopped. The collector and adapter run instructions are below.

Explore AI engineering

Reference downloads

Use OpenAI’s documentation for the exact requests your software can send. The Harness downloads use made-up examples to explain how you might model ad selection or collect reports.

  • OpenAI · official · externalOpenAI Ads OpenAPI specification

    OpenAI’s file listing the requests its advertiser API accepts and the data it returns. OpenAPI is the format used to describe those rules.

  • Harness · worked example · v1.0.0Example reporting request and result · JSON

    See which fields go into a request to OpenAI and how your own software could record results and missing data. The sample values are made up.

  • Harness · reference · v1.0.0Agent reference notes

    See where the files came from, how they fit together and how to use them with a coding agent.

Platform sources

Official sources used in this guide. Check your own ad account for the features you can access.

ChatGPT Ads · reviewed

Build on this

Try the report collector

Local report collector Run locally · Sample data

Run the collector from a checkout with Node.js 20 or later. Compare a complete report with a partial run using fictional data. No account is needed.

Open run instructions

Local reporting CLI and MCP tool Run locally · Sample data

Use Node.js 20.3 or later to run the CLI or connect a local MCP host. Both return the same report from fictional campaign data.

Open run instructions

Connect to a live account Planned build

A deployment walkthrough and verified account setup are in development.

A video walkthrough is planned. The written reference is available independently.

Continue exploring system design