aggregation_level, the levels below it). Numbers combine Thrad and ChatGPT (OpenAI Ads) supply by default; you can isolate or split a single supply.
aggregation_level you may request.
Authorizations
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 thename[] convention (e.g. fields[]=impressions&fields[]=clicks).
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.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.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).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.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.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.Break each row into sub-rows along one dimension. At most one segment (
too_many_segments otherwise).source— split intothrad+openaisub-rows. Supported at every level; the sub-rows sum to the combined total.country/device— break bycountry_code/device_type. Ad-group or ad scope only (unsupported_segmenthigher up), Thrad supply only (segment_source_unsupportedifsource=openai), and onlynoneordailygranularity (segment_granularity_unsupported). This path aggregates a single window, so it rejects an entity-idfilters[](unsupported_segment) and more than onetime_ranges[](invalid_time_range).product— not supported (unsupported_segment). Any other value returnsinvalid_segment.
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.Maximum rows in the page,
1–2000 (default 20). Out of range returns invalid_value.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.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.Response
Every endpoint returns the same bare list object — not the platform{ success, data, error, meta } envelope.
Always
"list".The metric rows for this page. Each row is a flat object.
Number of rows in
data on this page.Id of the first row in the page. Pass it as
before to fetch the previous page. null for an empty page.Id of the last row in the page. Pass it as
after to fetch the next page. null for an empty page.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 passsourceand the two disagree, you getinvalid_filter.segments[]=source— keep the combined window but split each row into athradsub-row and anopenaisub-row. Each sub-row carries asourcefield and:source=…in itsid, 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.
| Status | Type | Code | When |
|---|---|---|---|
400 | invalid_request_error | invalid_value | A parameter has a bad value (time_granularity, aggregation_level, source, sort, limit, …). |
400 | invalid_request_error | aggregation_level_too_high | aggregation_level is above the endpoint’s scope. |
400 | invalid_request_error | invalid_time_range | A time_ranges[] entry is malformed, has end ≤ start, an unknown timezone, or (audience segments) more than one range. |
400 | invalid_request_error | range_too_wide | A window exceeds the 366-day maximum. |
400 | invalid_request_error | hourly_range_too_wide | An hourly window exceeds 25 hours. |
400 | invalid_request_error | hourly_unsupported_for_external_source | source=openai requested with time_granularity=hourly. |
400 | invalid_request_error | unknown_field | A fields[] value is not a known metric. |
400 | invalid_request_error | invalid_filter | A filters[] entry is malformed, references an unknown field, filters an id finer than aggregation_level, or its source disagrees with source. |
400 | invalid_request_error | invalid_filter_operator | A filters[] operator is not allowed for that field. |
400 | invalid_request_error | too_many_segments | More than one segments[] value. |
400 | invalid_request_error | invalid_segment | An unrecognized segments[] value. |
400 | invalid_request_error | unsupported_segment | product segment, or country/device outside ad-group/ad scope (or with an entity-id filter). |
400 | invalid_request_error | segment_source_unsupported | country/device segment with source=openai (Thrad supply only). |
400 | invalid_request_error | segment_granularity_unsupported | country/device segment with hourly or monthly granularity. |
400 | invalid_request_error | invalid_cursor | before and after both passed, or an unknown cursor. |
400 | invalid_request_error | unsupported_parameter | includes[] or override_segment_group_order[] was passed. |
401 | authentication_error | auth_required | No Authorization header was sent. |
401 | authentication_error | invalid_api_key | The Authorization: Bearer ak_... key is malformed, unknown, or revoked. |
404 | invalid_request_error | not_found | The path entity does not exist or belongs to another organization. |
429 | rate_limit_error | rate_limit_exceeded | Exceeded the per-key limit of 1000 requests/hour. |
504 | server_error | clickhouse_timeout | The analytics query timed out. Narrow the scope, window, or granularity and retry. |
