Ads
Read spend, delivery and conversion metrics across every ad network, from rolled-up dashboards to breakdowns, live network queries and sync health events.
Pozy syncs the performance of every ad on every connected ad account, whether it was created through Pozy or in the network's own ads manager. Most reporting reads those synced numbers and costs nothing on the network's side. When you need something the sync does not hold, such as an age split or a custom Google query, Pozy asks the network live.
#Pick the right call
| You want | Call | Source |
|---|---|---|
| A dashboard: spend, CPC, CPM, conversions and ROAS per campaign, ad set and ad | GET /v1/ads/tree |
Synced, rolled up per level |
| A flat list of ads, or one ad with its metrics | GET /v1/ads, GET /v1/ads/{adId} |
Synced |
| One row per day for an account | GET /v1/ads/timeline |
Synced |
| One ad or campaign over a range, with optional breakdowns | GET /v1/ads/{adId}/analytics, GET /v1/ads/campaigns/{campaignId}/analytics |
Synced, breakdowns live |
| Any field, breakdown or filter the network supports | GET /v1/ads/insights |
Live (Meta, Google Ads, TikTok) |
| The same over a long range or many accounts | POST /v1/ads/insights/reports |
A background job (Meta) |
Network-specific reports live on their pages: Google's search terms and keyword planner in Google Ads, LinkedIn firmographics in LinkedIn Ads.
#Build a dashboard
GET /v1/ads/tree returns campaigns with their ad sets and ads nested inside, each with a metrics object for the date range. Filter and sort it like a report.
curl "https://api.pozy.ai/v1/ads/tree?accountId=66b2e19d8c3f5a7e9d0b1c2e&fromDate=2027-01-01&toDate=2027-01-31&hasDelivery=true&sort=spend_desc&compare=previous" \
-H "Authorization: Bearer $POZY_API_KEY"const res = await fetch("https://api.pozy.ai/v1/ads/tree?accountId=66b2e19d8c3f5a7e9d0b1c2e&fromDate=2027-01-01&toDate=2027-01-31&hasDelivery=true&sort=spend_desc&compare=previous", {
headers: { Authorization: `Bearer ${process.env.POZY_API_KEY}` },
});
const data = await res.json();
if (!res.ok) throw new Error(data.error);
for (const c of data.campaigns) console.log(c.campaignName, c.metrics?.spend, c.metrics?.roas);import os, requests
resp = requests.get(
"https://api.pozy.ai/v1/ads/tree",
headers={"Authorization": f"Bearer {os.environ['POZY_API_KEY']}"},
params={
"accountId": "66b2e19d8c3f5a7e9d0b1c2e",
"fromDate": "2027-01-01",
"toDate": "2027-01-31",
"hasDelivery": "true",
"sort": "spend_desc",
"compare": "previous",
},
)
for c in resp.json()["campaigns"]:
print(c["campaignName"], c.get("metrics", {}).get("spend"))Useful filters on the tree: platform, status, adAccountId, profileId, campaignId, search, minSpend, hasDelivery, updatedSince and source (pozy for ads created through Pozy, all by default). compare=previous adds the same metrics for the period just before, for "vs last month" views. timeIncrement=1 with dailyLevel (campaign, adset or ad) adds a daily series at that level.
For an account-level chart, GET /v1/ads/timeline?accountId=... returns one row per day with spend, impressions, reach, clicks, engagement, conversions and the derived rates.
#One ad over time
GET /v1/ads/{adId}/analytics returns a summary for the range and a daily series.
curl "https://api.pozy.ai/v1/ads/66f0a1b2c3d4e5f6a7b8c9d0/analytics?fromDate=2027-01-04&toDate=2027-01-31&breakdowns=age,gender" \
-H "Authorization: Bearer $POZY_API_KEY"{
"backfillPending": false,
"ad": { "id": "66f0a1b2c3d4e5f6a7b8c9d0", "name": "Spring sale", "platform": "facebook", "status": "active", "currency": "USD" },
"analytics": {
"summary": { "spend": 412.6, "impressions": 98100, "reach": 61200, "clicks": 1530, "ctr": 1.56, "cpc": 0.27, "cpm": 4.21, "conversions": 38, "costPerConversion": 10.86, "purchaseValue": 2140.0, "roas": 5.19 },
"daily": [{ "date": "2027-01-04", "spend": 14.9, "impressions": 3400, "clicks": 51 }],
"breakdowns": { "age": [{ "value": "25-34", "spend": 168.2, "impressions": 40100 }] }
}
}#Breakdowns
breakdowns takes a comma-separated list. Each dimension is one live call to the network, so ask only for what you show.
| Network | Per ad | Per campaign | Dimensions |
|---|---|---|---|
| Meta | Yes | Yes | age, gender, country, region, publisher_platform, device_platform |
| TikTok | Yes | No | gender, age, country_code, platform, ac, language |
| Yes | Yes (the campaign group) | job_title, job_function, seniority, industry, company, company_size, country, region |
|
| Others | No | No |
A dimension no network defines returns 400 listing the valid ones. A dimension from another network (say age on a LinkedIn ad) is accepted and dropped, and the response has no breakdowns object. LinkedIn returns firmographics for the whole range only, with no daily split, lags 12 to 24 hours and hides segments with fewer than 3 events.
#Metrics
Every metrics and summary object uses the same names on every network. Money is in the ad account's currency, in whole units.
| Metric | Meaning |
|---|---|
spend, impressions, clicks |
The base counters |
reach |
People reached. On most networks it is de-duplicated within a day only, so never add it across days. |
ctr, cpc, cpm |
Derived from the counters. ctr is a percentage. |
engagement |
Interactions the network counts as engagement |
conversions, allConversions, costPerConversion |
The network's primary conversions, every conversion, and spend per primary conversion |
actions, actionValues, costPerAction |
Counts, values and costs per action type |
purchaseValue, roas |
Purchase revenue and purchaseValue / spend. Rolled up from sums, never averaged. |
outboundClicks, inlineLinkClicks, uniqueClicks and their CTRs |
Click detail, where the network reports it |
videoPlayActions, video2SecWatchedActions, video6SecWatchedActions, videoP25WatchedActions to videoP100WatchedActions, videoThruplayWatchedActions, videoAvgTimeWatchedActions, costPerThruplay |
Video delivery |
funnel |
Landing page views, content views, searches, cart adds, checkouts, payment info, purchases, leads, registrations, app installs and messaging conversations |
engagementBreakdown |
Post and page engagement, reactions, comments and more |
On Meta, the same purchase often shows under several action keys (a pixel key, an omni_ key and a plain one). Prefer the offsite_conversion.fb_pixel_* keys, or read conversions and purchaseValue, which already pick one.
#What each network reports
| Network | Synced metrics |
|---|---|
| Meta | Everything above, including reach and ROAS |
| Google Ads | Spend, delivery and conversions; raw GAQL for everything else |
| TikTok | Spend, delivery, conversions and video metrics; de-duplicated reach through live queries |
| Spend and delivery; conversions per conversion rule | |
| Pinterest, X | Spend, impressions, clicks and rates. reach reads 0. |
| Reddit, ChatGPT Ads | Impressions, clicks and spend only, with CTR, CPC and CPM. Everything else reads 0. |
#Date ranges and history
- Without
fromDateandtoDate, ranges default to the last 90 days. The longest range is 730 days. - When an ad account connects, Pozy imports its last 90 days. Synced summaries hold nothing before that.
- On Meta, asking for older days answers
202withbackfillPending: trueand the part already covered, while Pozy imports the rest (up to 730 days back) in the background. Repeat the request until it returns200. Other networks never return202. - Breakdowns and live queries read the network directly, so they are not limited by the import.
- Days follow the ad account's time zone.
#Currencies
Pozy never converts currencies. Each campaign node and timeline row carries currency. When one connection's ad accounts bill in different currencies, the timeline returns one row per currency per day. Do not add spend across them.
#Attribution
Each network counts a conversion against an ad within its own windows, so numbers differ between networks for the same sale.
| Network | Where the window is set |
|---|---|
| Meta | attributionSpec on create: eventType (CLICK_THROUGH, VIEW_THROUGH, ENGAGED_VIDEO_VIEW) with windowDays of 1, 7 or 28. Live queries take actionAttributionWindows and useUnifiedAttributionSetting. |
| Google Ads | Per conversion action, plus which actions count toward bidding through conversion goals. See Conversions. |
Per conversion rule: click and view windows of 1, 7, 30, 90 or 365 days. GET .../conversion-destinations/{id}/metrics reads conversions per rule. |
|
| X | Per conversion event, with click and view windows up to 90 days. |
| Fixed 7-day click, 1-day view for conversion goals. | |
| ChatGPT Ads | Fixed 30 days. |
#Live queries
GET /v1/ads/insights passes your query to the network and returns its rows as they come. The connection's platform decides which parameters apply, and validation is the network's: an unknown field returns 400 with the network's message.
| Network | Send | Paging |
|---|---|---|
| Meta | objectId (any insights node: ad account, campaign, ad set, ad), level, fields, breakdowns, actionBreakdowns, filtering, datePreset or fromDate/toDate, timeIncrement |
limit and after |
| Google Ads | query: any read-only GAQL SELECT. adAccountId when the connection has several customers. |
10,000 rows per page; pass paging.nextPageToken as pageToken |
| TikTok | adAccountId, dataLevel, dimensions (1 to 4), metrics, fromDate/toDate (at most 365 days), optional filtering and reportType |
page and pageSize (up to 1,000) |
curl -G https://api.pozy.ai/v1/ads/insights \
-H "Authorization: Bearer $POZY_API_KEY" \
--data-urlencode "accountId=66b2e19d8c3f5a7e9d0b1c2f" \
--data-urlencode "query=SELECT campaign.name, metrics.cost_micros, metrics.clicks FROM campaign WHERE segments.date DURING LAST_7_DAYS"Google returns its own units, so cost_micros divides by 1,000,000, and counters arrive as strings. Selecting segments.date needs a finite date filter.
#Reach across several campaigns on TikTok
Adding the reach of two campaigns counts a person who saw both twice. To count each person once, filter the set and group by country instead of by campaign:
dataLevel=AUCTION_CAMPAIGN&dimensions=country_code&metrics=reach,impressions,frequency
&fromDate=2027-01-01&toDate=2027-01-31
&filtering=[{"field":"campaign_ids","operator":"IN","value":["1876574798182050","1876575504573650"]}]Reach is then per country; adding countries together double counts people reached in both. Filter at the same level as dataLevel or a coarser one.
#Long reports on Meta
For long ranges or many accounts, submit a background job with the same fields as a live Meta query:
curl -X POST https://api.pozy.ai/v1/ads/insights/reports \
-H "Authorization: Bearer $POZY_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "accountId": "66b2e19d8c3f5a7e9d0b1c2e", "objectId": "act_1234567890", "level": "ad", "fields": "ad_name,spend,impressions,actions", "fromDate": "2026-01-01", "toDate": "2026-12-31", "timeIncrement": "monthly" }'It answers 202 with a reportRunId. Poll GET /v1/ads/insights/reports/{reportRunId}: it reports status and percentCompletion while it runs, and once status is Job Completed it returns the rows, paged with limit and after.
#Know when numbers are stale
Subscribe to these webhook events instead of polling sync state:
| Event | When it fires |
|---|---|
account.ads.initial_sync_completed |
Once per connection, after ad account discovery and the 90-day import. sync.status says whether it fully or partly succeeded; on failure, sync.errorCategory is token_invalid, permission_denied, no_ad_accounts, rate_limited, discovery_failed or unknown. |
account.ads.sync_failed |
Once when an ad account has had no successful sync for 24 hours, or every live ad in it hit the retry limit. Flag its numbers as stale. sync.errorCategory is ad_account_not_listed, sync_error or stale. |
account.ads.sync_recovered |
Once when that ad account syncs again. |
ad_account.status_changed |
An ad account became active, restricted, disabled or closed (Meta, Google, TikTok, X). |
ad.review.approved, ad.review.rejected |
A network's review decision on an ad you already had, within one sync cycle. |
ad.delivery_issue |
An ad stopped delivering for a network reason (Meta, TikTok, LinkedIn). |
campaign.budget.depleted |
A TikTok campaign ran out of budget. |
A failed initial sync is not final: running the ads connect flow again queues the import again. Reconnecting fixes token_invalid, permission_denied and rate_limited; no_ad_accounts needs the person to have an ad account first.
#Endpoints
| Endpoint | What it does |
|---|---|
GET /v1/ads/tree |
Get campaign tree |
GET /v1/ads/timeline |
Get daily account metrics |
GET /v1/ads |
List ads |
GET /v1/ads/{adId} |
Get ad details |
| Endpoint | What it does |
|---|---|
GET /v1/ads/search-terms |
Google Ads search terms report |
GET /v1/ads/local-services/leads |
Google Local Services Ads leads |
GET /v1/ads/local-services/leads/{leadId}/conversations |
List lead conversations |
GET /v1/ads/campaigns/{campaignId}/analytics |
Get campaign analytics |
POST /v1/ads/keywords/ideas |
Generate keyword ideas |
POST /v1/ads/keywords/historical-metrics |
Get historical keyword metrics |
GET /v1/ads/insights |
Flexible live insights query |
POST /v1/ads/insights/reports |
Submit async insights report |
GET /v1/ads/insights/reports/{reportRunId} |
Poll an async insights report run |
GET /v1/ads/{adId}/analytics |
Get ad analytics |
GET /v1/ads/tiktok-smart-plus-materials |
Per-creative performance inside TikTok Smart+ ads |
#Webhook events
| Event | When it fires |
|---|---|
account.ads.initial_sync_completed |
Ads initial sync completed. |
account.ads.sync_failed |
Ads sync failed. |
account.ads.sync_recovered |
Ads sync recovered. |
ad.review.approved |
Ad review approved. |
ad.review.rejected |
Ad review rejected. |
ad.delivery_issue |
Ad delivery issue. |
campaign.budget.depleted |
Campaign budget depleted. |
ad_account.status_changed |
Ad account status changed. |