First 200 users get the Growth plan for $19/mo.

Claim
Browse the docs

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.collectionMeaning
source_not_connectedNothing is connected for this source. Says nothing about your ads.
never_collectedA source exists and no reading has been taken yet.
collectedA reading exists; collectedAt is when.
collection_stalledAn 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.

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

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

QueryTypeMeaning
fromYYYY-MM-DDoptionalFirst day. Send both ends or neither; the default is the last 30 days.
toYYYY-MM-DDoptionalLast day, inclusive.
accountIduuidoptionalOne ad account. Omit for all.

Response: { data: { from, to, currencies, impressions, clicks, spendMinor, conversions, revenueMinor, sourceRows, coversThrough } | null, provenance }.

200json
{
  "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."
  }
}
curlbash
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.

QueryTypeMeaning
fromYYYY-MM-DDoptionalFirst day.
toYYYY-MM-DDoptionalLast day, inclusive.
accountIduuidoptionalOne ad account.
limitintegeroptionalDefault 500, max 2,000.

Response: { data: { date, accountId, platform, currency, impressions, clicks, spendMinor, conversions, revenueMinor }[], provenance }.

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

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

QueryTypeMeaning
siteIduuidoptionalOne site.
limitintegeroptionalDefault 200, max 1,000.

Response: { data: Page[], provenance } with id, siteId, url, path, source, state, stateDetail, lastHttpStatus, title, pricingConfidence, pricingSignals, lastFetchedAt, lastChangedAt.

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

QueryTypeMeaning
limitintegeroptionalDefault 200, max 500.

Response: { data: Snapshot[], provenance } with id, pageId, state, stateDetail, httpStatus, finalUrl, contentType, byteLength, contentHash, title, h1, capturedAt.

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

QueryTypeMeaning
siteIduuidoptionalOne site.
pageIduuidoptionalOne page.
limitintegeroptionalDefault 100, max 500.

Response: { data: Change[], provenance } with kinds, similarity, titleBefore, titleAfter, h1Before, h1After, ctasAdded, ctasRemoved, priceComparison, priceDetail, detectedAt.

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

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