Skip to main content
The insights surface is four read-only endpoints, one per scope in the hierarchy. Each returns a bare list of metric rows for the entity you ask about (and, via aggregation_level, the levels below it). Numbers combine Thrad and ChatGPT (OpenAI Ads) supply by default; you can isolate or split a single supply.
GET /v1/ad_account/insights
GET /v1/campaigns/{campaign_id}/insights
GET /v1/ad_groups/{ad_group_id}/insights
GET /v1/ads/{ad_id}/insights
All four take the same query parameters and return the same list shape. The only difference is scope: the entity you target and the deepest aggregation_level you may request.
Insights money is dollar floats, not micros. spend, cpc, and cpm are already in dollars (e.g. 0.70), and ctr is a fraction (e.g. 0.10 = 10%). This is deliberately different from the resource endpoints (campaigns, ad groups, ads), where budgets and bids are integer micros ($1 = 1,000,000).

Authorizations

Authorization
string
required
Bearer token using your organization’s API key: Authorization: Bearer ak_.... There is one active key per organization — create or rotate it in the Thrad Platform dashboard under Settings → API keys. Store it as the THRAD_ADS_API_KEY environment variable.

Query parameters

These apply to every insights endpoint. Repeated parameters use the name[] convention (e.g. fields[]=impressions&fields[]=clicks).
time_granularity
string
default:"daily"
How rows are bucketed over time: hourly, daily (default), monthly, or none. With none a single row covers the whole requested range and has no readable_time. monthly rolls daily data up by calendar month. Any other value returns invalid_value.
aggregation_level
string
The row grain: ad_account, campaign, ad_group, or ad. Defaults to the endpoint’s own scope. It must be at or below the endpoint scope — e.g. GET /v1/campaigns/{id}/insights?aggregation_level=ad returns one row per ad in that campaign, but aggregation_level=ad_account on a campaign endpoint is rejected with aggregation_level_too_high. Use a deeper level to discover the entity ids beneath a scope; there are no separate list endpoints.
time_ranges[]
array
One or more JSON window objects. Each must be one of three types, and each window may span at most 366 days (range_too_wide). When omitted, a trailing 30-day window is used.
end must be after start (invalid_time_range). With time_granularity=hourly, each window must also be 25 hours or less (hourly_range_too_wide).
fields[]
array
Which metrics to project onto each row: impressions, clicks, spend, ctr, cpc, cpm. Defaults to impressions, clicks, spend. A dotted form like ad.impressions is accepted (the prefix is ignored). An unknown metric returns unknown_field.
filters[]
array
One or more JSON { "field", "operator", "value" } objects. Three kinds of field are accepted:An unknown field returns invalid_filter; a disallowed operator returns invalid_filter_operator.
sort[]
array
One or more JSON { "field", "direction" } objects, applied in order. field is a metric (ad.clicks accepted); direction is asc or desc. A non-metric field or a bad direction returns invalid_value. Row id is always the final deterministic tiebreak.
segments[]
array
Break each row into sub-rows along one dimension. At most one segment (too_many_segments otherwise).
  • source — split into thrad + openai sub-rows. Supported at every level; the sub-rows sum to the combined total.
  • country / device — break by country_code / device_type. Ad-group or ad scope only (unsupported_segment higher up), Thrad supply only (segment_source_unsupported if source=openai), and only none or daily granularity (segment_granularity_unsupported). This path aggregates a single window, so it rejects an entity-id filters[] (unsupported_segment) and more than one time_ranges[] (invalid_time_range).
  • product — not supported (unsupported_segment). Any other value returns invalid_segment.
source
string
default:"all"
Which supply to report: thrad, openai, or all (default — combined Thrad + ChatGPT). A convenience equivalent to the source filter above.
ChatGPT (OpenAI Ads) insights are daily-only. source=openai with time_granularity=hourly is a hard error (hourly_unsupported_for_external_source); a combined (all) hourly request silently reports Thrad supply only.
limit
integer
default:"20"
Maximum rows in the page, 12000 (default 20). Out of range returns invalid_value.
before
string
Cursor for the previous page — pass a prior response’s first_id. Mutually exclusive with after (invalid_cursor if both). An unknown cursor returns invalid_cursor.
after
string
Cursor for the next page — pass a prior response’s last_id. Mutually exclusive with before.
includes[] and override_segment_group_order[] exist in OpenAI’s Ads API but are not supported here. Passing either returns 400 unsupported_parameter — they are never silently ignored.
curl -G https://api.thrad.ai/v1/campaigns/$CAMPAIGN_ID/insights \
  -H "Authorization: Bearer $THRAD_ADS_API_KEY" \
  --data-urlencode 'time_granularity=daily' \
  --data-urlencode 'fields[]=impressions' \
  --data-urlencode 'fields[]=clicks' \
  --data-urlencode 'fields[]=spend' \
  --data-urlencode 'fields[]=ctr' \
  --data-urlencode 'time_ranges[]={"type":"date_range","since":"2026-04-01","until":"2026-04-03"}'
{
  "object": "list",
  "data": [
    {
      "id": "start=1775347200:end=1775433600:entity_id=8c1f...",
      "start_time": 1775347200,
      "end_time": 1775433600,
      "readable_time": "2026-04-02",
      "timezone": "UTC",
      "campaign_id": "8c1f2a3b-4d5e-6f70-8192-a3b4c5d6e7f8",
      "campaign_name": "Spring Sale",
      "impressions": 500,
      "clicks": 50,
      "spend": 0.70,
      "ctr": 0.1
    }
  ],
  "count": 1,
  "first_id": "start=1775347200:end=1775433600:entity_id=8c1f...",
  "last_id": "start=1775347200:end=1775433600:entity_id=8c1f...",
  "has_more": false
}

Response

Every endpoint returns the same bare list object — not the platform { success, data, error, meta } envelope.
object
string
Always "list".
data
array
The metric rows for this page. Each row is a flat object.
count
integer
Number of rows in data on this page.
first_id
string
Id of the first row in the page. Pass it as before to fetch the previous page. null for an empty page.
last_id
string
Id of the last row in the page. Pass it as after to fetch the next page. null for an empty page.
has_more
boolean
true if more rows exist beyond this page.

Choosing supply (Thrad vs ChatGPT)

By default insights combine Thrad and ChatGPT (OpenAI Ads) supply into one number. There are three ways to look at supply individually:
  • source=thrad|openai — report a single supply. source=all (the default) is combined.
  • filters[]={"field":"source","operator":"IN","value":["thrad"]} — the OpenAI-style equivalent. If you also pass source and the two disagree, you get invalid_filter.
  • segments[]=source — keep the combined window but split each row into a thrad sub-row and an openai sub-row. Each sub-row carries a source field and :source=… in its id, and the two sum to the combined total.

Errors

Errors use the bare OpenAI shape — { "error": { "message", "type", "param", "code" } } — not the platform envelope. type is one of invalid_request_error, authentication_error, rate_limit_error, or server_error.
StatusTypeCodeWhen
400invalid_request_errorinvalid_valueA parameter has a bad value (time_granularity, aggregation_level, source, sort, limit, …).
400invalid_request_erroraggregation_level_too_highaggregation_level is above the endpoint’s scope.
400invalid_request_errorinvalid_time_rangeA time_ranges[] entry is malformed, has endstart, an unknown timezone, or (audience segments) more than one range.
400invalid_request_errorrange_too_wideA window exceeds the 366-day maximum.
400invalid_request_errorhourly_range_too_wideAn hourly window exceeds 25 hours.
400invalid_request_errorhourly_unsupported_for_external_sourcesource=openai requested with time_granularity=hourly.
400invalid_request_errorunknown_fieldA fields[] value is not a known metric.
400invalid_request_errorinvalid_filterA filters[] entry is malformed, references an unknown field, filters an id finer than aggregation_level, or its source disagrees with source.
400invalid_request_errorinvalid_filter_operatorA filters[] operator is not allowed for that field.
400invalid_request_errortoo_many_segmentsMore than one segments[] value.
400invalid_request_errorinvalid_segmentAn unrecognized segments[] value.
400invalid_request_errorunsupported_segmentproduct segment, or country/device outside ad-group/ad scope (or with an entity-id filter).
400invalid_request_errorsegment_source_unsupportedcountry/device segment with source=openai (Thrad supply only).
400invalid_request_errorsegment_granularity_unsupportedcountry/device segment with hourly or monthly granularity.
400invalid_request_errorinvalid_cursorbefore and after both passed, or an unknown cursor.
400invalid_request_errorunsupported_parameterincludes[] or override_segment_group_order[] was passed.
401authentication_errorauth_requiredNo Authorization header was sent.
401authentication_errorinvalid_api_keyThe Authorization: Bearer ak_... key is malformed, unknown, or revoked.
404invalid_request_errornot_foundThe path entity does not exist or belongs to another organization.
429rate_limit_errorrate_limit_exceededExceeded the per-key limit of 1000 requests/hour.
504server_errorclickhouse_timeoutThe analytics query timed out. Narrow the scope, window, or granularity and retry.
A single response is capped at a fixed maximum number of rows (the decoded set is sorted, filtered, and paginated in memory). If you need more, narrow the scope or window, or page with before/after.