API reference
Analytics
Ad accounts, spend and results by day, and the landing-page crawl, every response with provenance.
Base URL https://api.overads.io/public/v1. Every route needs a key with the scope shown, sent as Authorization: Bearer sk_live_YOUR_KEY. The machine-readable contract is at https://api.overads.io/public/v1/openapi.json, no key needed. Scope for this family: analytics:read.
Every response carries provenance
Analytics routes answer { data, provenance }. Read provenance before data: it says which source produced the reading, when it was taken, and, when data is empty, which of three different things that means.
provenance.collection | Meaning |
|---|---|
source_not_connected | Nothing is connected for this source. Says nothing about your ads. |
never_collected | A source exists and no reading has been taken yet. |
collected | A reading exists; collectedAt is when. |
collection_stalled | An older reading exists and collection has been failing since; stalledDetail says why. |
provenance.emptyMeans is no_source_connected, collection_has_not_run or we_looked_and_there_is_nothing. Only the last licenses a sentence like "there were none". rowCount is null when no reading was taken. statement is the whole thing as one sentence, safe to show verbatim.
Missing is null, never zero. sum() over no rows is null on the wire, and a null counter beside sourceRows: 0 is how you tell "no rows in the window" from "rows that sum to zero".
GET/sources
Scope analytics:read. What this deployment can answer: every source, whether it is enabled, and why when it is not.
Response: { version: "v1", sources: { id, label, ownership, origin, enabled, disabledBecause }[] }. Platform-derived sources ship off pending a legal review and say so in disabledBecause.
curl https://api.overads.io/public/v1/sources -H "Authorization: Bearer sk_live_YOUR_KEY"GET/ad-accounts
Scope analytics:read. The ad accounts this workspace connected, each with its own sync state.
Response: { data: AdAccount[], provenance }. Each row has id, platform, externalId, name, currency, timezone, status, metricsCollection, syncStatus, syncError.
curl https://api.overads.io/public/v1/ad-accounts -H "Authorization: Bearer sk_live_YOUR_KEY"GET/metrics/summary
Scope analytics:read. Totals over a window. Every counter is null when there is no reading, never 0.
Money is in minor units (cents) as a string. Check currencies before adding totals: more than one means they cannot be summed.
| Query | Type | Meaning | |
|---|---|---|---|
from | YYYY-MM-DD | optional | First day. Send both ends or neither; the default is the last 30 days. |
to | YYYY-MM-DD | optional | Last day, inclusive. |
accountId | uuid | optional | One ad account. Omit for all. |
Response: { data: { from, to, currencies, impressions, clicks, spendMinor, conversions, revenueMinor, sourceRows, coversThrough } | null, provenance }.
{
"data": {
"from": "2026-08-12", "to": "2026-09-10",
"currencies": ["USD"],
"impressions": "1842211", "clicks": "40318", "spendMinor": "1284400", "conversions": "912", "revenueMinor": null,
"sourceRows": 58, "coversThrough": "2026-09-10"
},
"provenance": {
"source": "own_ad_metrics", "sourceLabel": "your connected ad accounts", "ownership": "first_party",
"collection": "collected", "collectedAt": "2026-09-11T02:04:11.000Z", "stalledDetail": null,
"rowCount": 58, "emptyMeans": null,
"statement": "Collected from your connected ad accounts at 2026-09-11T02:04:11.000Z. Every figure below describes that moment and no later one."
}
}curl "https://api.overads.io/public/v1/metrics/summary?from=2026-08-12&to=2026-09-10" -H "Authorization: Bearer sk_live_YOUR_KEY"GET/metrics/daily
Scope analytics:read. Per-day totals over a window, newest first, split by account, platform and currency.
| Query | Type | Meaning | |
|---|---|---|---|
from | YYYY-MM-DD | optional | First day. |
to | YYYY-MM-DD | optional | Last day, inclusive. |
accountId | uuid | optional | One ad account. |
limit | integer | optional | Default 500, max 2,000. |
Response: { data: { date, accountId, platform, currency, impressions, clicks, spendMinor, conversions, revenueMinor }[], provenance }.
curl "https://api.overads.io/public/v1/metrics/daily?from=2026-09-01&to=2026-09-10&limit=31" -H "Authorization: Bearer sk_live_YOUR_KEY"GET/crawl/sites
Scope analytics:read. Sites overads crawls for this workspace, each with its own crawl state.
Response: { data: Site[], provenance } with id, origin, host, label, robotsState, robotsFetchedAt, robotsCrawlDelaySeconds, trackedPages, crawl.
curl https://api.overads.io/public/v1/crawl/sites -H "Authorization: Bearer sk_live_YOUR_KEY"GET/crawl/pages
Scope analytics:read. Tracked URLs and the outcome of the last visit to each, verbatim from the crawler.
state is one of twelve values. Only fetched_empty, fetched_baseline, fetched_unchanged, fetched_drifted and fetched_changed say anything about the page; the rest say we did not read it and why.
| Query | Type | Meaning | |
|---|---|---|---|
siteId | uuid | optional | One site. |
limit | integer | optional | Default 200, max 1,000. |
Response: { data: Page[], provenance } with id, siteId, url, path, source, state, stateDetail, lastHttpStatus, title, pricingConfidence, pricingSignals, lastFetchedAt, lastChangedAt.
curl "https://api.overads.io/public/v1/crawl/pages?limit=50" -H "Authorization: Bearer sk_live_YOUR_KEY"GET/crawl/pages/:pageId/timeline
Scope analytics:read. Every visit to one page, newest first, including the visits that returned nothing.
| Query | Type | Meaning | |
|---|---|---|---|
limit | integer | optional | Default 200, max 500. |
Response: { data: Snapshot[], provenance } with id, pageId, state, stateDetail, httpStatus, finalUrl, contentType, byteLength, contentHash, title, h1, capturedAt.
curl https://api.overads.io/public/v1/crawl/pages/0b1c2d3e-4f5a-4b6c-8d7e-9f0a1b2c3d4e/timeline -H "Authorization: Bearer sk_live_YOUR_KEY"GET/crawl/changes
Scope analytics:read. Detected page changes: the before string, the after string, and the detector that fired. No direction is ever inferred.
An empty list is qualified. Read provenance.emptyMeans before concluding that nothing changed.
| Query | Type | Meaning | |
|---|---|---|---|
siteId | uuid | optional | One site. |
pageId | uuid | optional | One page. |
limit | integer | optional | Default 100, max 500. |
Response: { data: Change[], provenance } with kinds, similarity, titleBefore, titleAfter, h1Before, h1After, ctasAdded, ctasRemoved, priceComparison, priceDetail, detectedAt.
curl "https://api.overads.io/public/v1/crawl/changes?limit=20" -H "Authorization: Bearer sk_live_YOUR_KEY"GET/instagram/profiles
Scope analytics:read. Tracked Instagram handles. Gated: answers 404 unless the deployment enables platform-derived sources.
Response: { data: Profile[], provenance } when enabled; 404 otherwise. GET /sources says which applies.
curl https://api.overads.io/public/v1/instagram/profiles -H "Authorization: Bearer sk_live_YOUR_KEY"Engagement on published posts
Per-post engagement and per-account insights are read through the MCP tools post_stats and profile_stats, not through a REST route yet. Both return null readings with an unavailableReason on deployments where platform-derived reads are off, which is the default today.