Endpoints
Every operation of the Level API — reports and their data, campaigns, ad accounts, segments, targets, organizations and metric catalogues, plus the writes and how they behave.
Thirty-three operations under https://api.marketing-bar.com/v1 — twenty reads and
thirteen writes. The canonical machine-readable version is GET /v1/openapi.json,
rendered at GET /v1/docs; this page is the map.
A GET only ever needs a read scope, and a POST / PATCH / PUT / DELETE only
ever needs a write scope. A client that never asked for a write scope cannot change
anything, whatever it sends.
Reading
| Path | Scope | What it returns |
|---|---|---|
GET /v1/health | — | Liveness. No token needed. |
GET /v1/openapi.json | — | The OpenAPI document. |
GET /v1/docs | — | The rendered reference. |
GET /v1/me | any | This token's identity, plan, quotas and allow-list. |
GET /v1/organizations | organizations:read | The organization this client reads, as a list of one. |
GET /v1/organizations/{id} | organizations:read | One organization. |
GET /v1/organizations/{id}/ad-accounts | ad-accounts:read | The ad accounts, cut by the allow-list. |
GET /v1/organizations/{id}/ad-accounts/{adAccountId} | ad-accounts:read | One ad account. |
GET /v1/organizations/{id}/segments | segments:read | The segments. |
GET /v1/organizations/{id}/segments/{segmentId} | segments:read | One segment. |
GET /v1/organizations/{id}/campaigns | campaigns:read | The campaigns, cut by the ad-account allow-list. |
GET /v1/organizations/{id}/budgets | budgets:read | The targets, cut by the ad-account allow-list. |
GET /v1/organizations/{id}/budgets/{budgetId} | budgets:read | One target. |
GET /v1/organizations/{id}/metrics | reports:read | System metrics plus this organization's custom ones. |
GET /v1/metrics | reports:read | The system metric catalogue. |
GET /v1/reports | reports:read | The reports this client may read. |
GET /v1/reports/{id} | reports:read | One report's definition. |
GET /v1/reports/{id}/data | reports:data | The numbers. |
GET /v1/reports/{id}/export.xlsx | reports:export | The same rows as an Excel workbook. |
GET /v1/reports/{id}/export.csv | reports:export | The same rows as CSV. |
Writing
| Operation | Scope | What it does |
|---|---|---|
POST /v1/organizations/{id}/segments | segments:write | Create a segment. Only name is required. |
PATCH /v1/organizations/{id}/segments/{segmentId} | segments:write | Change a segment. A field left out keeps its stored value. |
DELETE /v1/organizations/{id}/segments/{segmentId} | segments:write | Delete a segment. Its campaigns fall back to Unknown, exactly as in the UI. |
PUT /v1/organizations/{id}/campaigns/{campaignId}/segments | segments:assign | Replace one campaign's whole segment set. An empty list moves it to Unknown. |
POST /v1/organizations/{id}/campaign-segments | segments:assign | Add, remove or replace segments across up to 500 campaigns in one write. |
POST /v1/organizations/{id}/budgets | budgets:write | Create a target. metric defaults to Cost and cannot be changed afterwards. |
PATCH /v1/organizations/{id}/budgets/{budgetId} | budgets:write | Change a target. Sending metric at all is refused, even unchanged. |
DELETE /v1/organizations/{id}/budgets/{budgetId} | budgets:write | Delete a target. |
PUT /v1/organizations/{id}/budgets/monthly | budgets:write | Save a whole year of one metric's monthly grid and get the affected set back. |
POST /v1/reports | reports:write | Create a report definition; its id joins this client's report allow-list. |
PATCH /v1/reports/{reportId} | reports:write | Change a report the API created. type is immutable. |
DELETE /v1/reports/{reportId} | reports:write | Delete a report the API created. |
POST /v1/organizations/{id}/ad-accounts | ad-accounts:write | Link a platform account the client's creator already connected. |
budgets is what the API calls the Targets feature.
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
| Parameter | Notes |
|---|---|
dateFrom, dateTo | Inclusive. Required, except on a months report, which spans everything it holds. At most 400 days apart. |
groupBy | total, 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. |
metrics | Which 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. |
adAccountIds | Narrow to some of the report's ad accounts. Also one of the two inputs to what the call costs — see Quotas. |
segmentIds | Segment reports only. |
sort, sortDesc | Sort rows by a metric. Campaign, video and creative groupings only. |
nameFilter | Substring 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
currencyis the report's own. Money metrics arrive already converted into it — you never have to apply a rate yourself.metricsin 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.dataFreshnessis the newest successful platform fetch across this report's ad accounts, and it is also theLast-Modifiedheader.nullmeans 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 Modified — and 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. A403would 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.
What a write is allowed to do
Who acts. A client credential has nobody behind it, so every write acts as the
person who created the API client — not necessarily the organization's owner.
Level re-checks that person's current permission in the organization before the
write reaches the database. A creator who left the organization, or lost the
permission, gets 403 insufficient-permission on every write from then on; reads
keep working.
That is also why a write scope is only issued to a client whose creator already holds
the matching permission: segments:write needs Manage and Delete segments,
segments:assign needs Assign campaign segments, budgets:write needs Manage
budgets, reports:write needs Create reports, ad-accounts:write needs Create
ad accounts. The organization's owner is exempt from that check.
Changing a report asks for more than the scope. PATCH and DELETE reach a
report only when it was created through the API — by any client of this
organization, so a rotated credential keeps its predecessor's reports — and when the
client's creator currently holds Edit (for PATCH) or Full (for DELETE) access to
it. A report a person built inside Level answers 404, identically to one that does
not exist. Reading is unchanged.
Retries are safe. Any POST may carry an Idempotency-Key header (1–128
printable ASCII characters). The key is remembered for 24 hours, scoped to the client
and fingerprinted on the method, path, query and body:
| What you send | What you get |
|---|---|
| The same key with the same request | The original response, replayed, plus Idempotent-Replayed: true. The write does not run twice. |
| The same key with a different request | 422 idempotency-key-mismatch |
| The same key while the first is still running | 409 conflict |
PUT, PATCH, DELETE and GET ignore the header — only a POST creates
something new, so only a POST needs protecting.
When the body is wrong, the answer is 400 invalid-parameter with an errors
object mapping each field name to what it failed on (a payload that isn't valid JSON
reports under the key body). A clash with what already exists — a duplicate name,
an overlapping target period, an ad account already linked elsewhere — is
409 conflict. See Errors.
Writes have their own hourly budget, separate from reads — see Quotas.
Other endpoints worth knowing
/v1/organizations/{id}/campaignstakesadAccountIdandname(a case-insensitive substring). TheexternalIdfield on each campaign is the platform's own id — the join key for anything outside Level./v1/organizations/{id}/ad-accountstakesplatformto narrow to one network./v1/organizations/{id}/metricsdescribes 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.