Skip to main content

Endpoints

All sixteen paths of the Level API — reports and their data, campaigns, ad accounts, segments, organizations and metric catalogues, with paging, filters and caching.

Sixteen paths, every one a GET, all under https://api.marketing-bar.com/v1. The canonical machine-readable version is GET /v1/openapi.json, rendered at GET /v1/docs; this page is the map.

The whole surface

PathScopeWhat it returns
/v1/healthLiveness. No token needed.
/v1/openapi.jsonThe OpenAPI document.
/v1/docsThe rendered reference.
/v1/meanyThis token's identity, plan, quota and allow-list.
/v1/organizationsorganizations:readThe organization this client reads, as a list of one.
/v1/organizations/{id}organizations:readOne organization.
/v1/organizations/{id}/ad-accountsad-accounts:readThe ad accounts, cut by the allow-list.
/v1/organizations/{id}/segmentssegments:readThe segments.
/v1/organizations/{id}/campaignscampaigns:readThe campaigns, cut by the ad-account allow-list.
/v1/organizations/{id}/metricsreports:readSystem metrics plus this organization's custom ones.
/v1/metricsreports:readThe system metric catalogue.
/v1/reportsreports:readThe reports this client may read.
/v1/reports/{id}reports:readOne report's definition.
/v1/reports/{id}/datareports:dataThe numbers.
/v1/reports/{id}/export.xlsxreports:exportThe same rows as an Excel workbook.
/v1/reports/{id}/export.csvreports:exportThe same rows as CSV.

Paging

Every list endpoint takes page (from 1) and pageSize (1–200, default 50) and answers in the same envelope:

{
  "data": [ ],
  "page": 1,
  "pageSize": 50,
  "totalCount": 137,
  "hasMore": true
}

A pageSize over 200 is refused with 400, not silently clamped — a job that asks for 1 000 rows and quietly gets 200 is a job that loses data without noticing.

Reading a report

GET /v1/reports/{reportId}/data is the endpoint most integrations live on.

curl -H "Authorization: Bearer $TOKEN" \
  "https://api.marketing-bar.com/v1/reports/$ID/data\
?dateFrom=2026-08-01&dateTo=2026-08-31&groupBy=campaign&metrics=cost,clicks,roas"
{
  "reportId": "",
  "groupBy": "campaign",
  "currency": "EUR",
  "dateFrom": "2026-08-01",
  "dateTo": "2026-08-31",
  "metrics": ["cost", "clicks", "roas"],
  "dataFreshness": "2026-09-12T09:00:00Z",
  "rows": [
    {
      "dimensions": { "campaignId": "", "campaignName": "Brand · Search" },
      "metrics": { "cost": 1240.55, "clicks": 8123, "roas": 4.12 }
    }
  ]
}

Every row has the same shape whatever the grouping — dimensions identify it, metrics carry the numbers — so one parser handles all seven groupings.

Parameters

ParameterNotes
dateFrom, dateToInclusive. Required, except on a months report, which spans everything it holds. At most 400 days apart.
groupBytotal, campaign, segment, month, ad-account, video or creative. Default total. A grouping the report can't answer is refused with a 400 naming the ones it can.
metricsWhich metrics to return. Omit for every metric the report saves. Repeat the parameter or pass one comma-separated value. An unknown name is refused, not dropped.
adAccountIdsNarrow to some of the report's ad accounts. Also one of the two inputs to what the call costs — see Quotas.
segmentIdsSegment reports only.
sort, sortDescSort rows by a metric. Campaign, video and creative groupings only.
nameFilterSubstring filter on the row's own name.

The same parameters apply to export.xlsx and export.csv, which return the same rows as a file.

Currency and freshness

  • currency is the report's own. Money metrics arrive already converted into it — you never have to apply a rate yourself.
  • metrics in the response lists what was actually returned. It can be shorter than what you asked for: conversion-derived metrics are absent on plans that don't include them.
  • dataFreshness is the newest successful platform fetch across this report's ad accounts, and it is also the Last-Modified header. null means nothing has ever loaded. It is the honest answer to "how old are these numbers?" — see Data sync.

Caching: ask again for free

Data responses carry a weak ETag. Send it back as If-None-Match and an unchanged report answers 304 Not Modifiedand the quota charge is refunded, so polling a report that hasn't moved costs nothing.

curl -H "Authorization: Bearer $TOKEN" \
     -H 'If-None-Match: W/"a1b2c3…"' \
     "https://api.marketing-bar.com/v1/reports/$ID/data?dateFrom=…&dateTo=…"

This is the recommended shape for a job that runs every 15 minutes against a report that refreshes hourly.

Two rules that shape every response

  • The allow-list is invisible, not forbidden. A report or ad account outside what this client may read answers 404, exactly like one that doesn't exist. A 403 would confirm that somebody else's row exists.
  • A filter naming something out of reach returns an empty page, not an error — ?organizationId= for another organization, ?adAccountId= for an unlisted cabinet.

Other endpoints worth knowing

  • /v1/organizations/{id}/campaigns takes adAccountId and name (a case-insensitive substring). The externalId field on each campaign is the platform's own id — the join key for anything outside Level.
  • /v1/organizations/{id}/ad-accounts takes platform to narrow to one network.
  • /v1/organizations/{id}/metrics describes what the metric names in report data mean: display name, kind (money, count, percent, ratio, decimal), unit, and whether it's a system metric. Fetch it once and cache it — it changes only when someone edits custom metrics.