Ads

Ads reporting

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"

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
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"
Response (trimmed)
{
  "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
LinkedIn 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
LinkedIn 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 fromDate and toDate, 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 202 with backfillPending: true and the part already covered, while Pozy imports the rest (up to 730 days back) in the background. Repeat the request until it returns 200. Other networks never return 202.
  • 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.
LinkedIn 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.
Reddit 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)
Google Ads: spend by campaign, last 7 days
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:

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