Skip to main content

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

PathScopeWhat it returns
GET /v1/health—Liveness. No token needed.
GET /v1/openapi.json—The OpenAPI document.
GET /v1/docs—The rendered reference.
GET /v1/meanyThis token's identity, plan, quotas and allow-list.
GET /v1/organizationsorganizations:readThe organization this client reads, as a list of one.
GET /v1/organizations/{id}organizations:readOne organization.
GET /v1/organizations/{id}/ad-accountsad-accounts:readThe ad accounts, cut by the allow-list.
GET /v1/organizations/{id}/ad-accounts/{adAccountId}ad-accounts:readOne ad account.
GET /v1/organizations/{id}/segmentssegments:readThe segments.
GET /v1/organizations/{id}/segments/{segmentId}segments:readOne segment.
GET /v1/organizations/{id}/campaignscampaigns:readThe campaigns, cut by the ad-account allow-list.
GET /v1/organizations/{id}/budgetsbudgets:readThe targets, cut by the ad-account allow-list.
GET /v1/organizations/{id}/budgets/{budgetId}budgets:readOne target.
GET /v1/organizations/{id}/metricsreports:readSystem metrics plus this organization's custom ones.
GET /v1/metricsreports:readThe system metric catalogue.
GET /v1/reportsreports:readThe reports this client may read.
GET /v1/reports/{id}reports:readOne report's definition.
GET /v1/reports/{id}/datareports:dataThe numbers.
GET /v1/reports/{id}/export.xlsxreports:exportThe same rows as an Excel workbook.
GET /v1/reports/{id}/export.csvreports:exportThe same rows as CSV.

Writing

OperationScopeWhat it does
POST /v1/organizations/{id}/segmentssegments:writeCreate a segment. Only name is required.
PATCH /v1/organizations/{id}/segments/{segmentId}segments:writeChange a segment. A field left out keeps its stored value.
DELETE /v1/organizations/{id}/segments/{segmentId}segments:writeDelete a segment. Its campaigns fall back to Unknown, exactly as in the UI.
PUT /v1/organizations/{id}/campaigns/{campaignId}/segmentssegments:assignReplace one campaign's whole segment set. An empty list moves it to Unknown.
POST /v1/organizations/{id}/campaign-segmentssegments:assignAdd, remove or replace segments across up to 500 campaigns in one write.
POST /v1/organizations/{id}/budgetsbudgets:writeCreate a target. metric defaults to Cost and cannot be changed afterwards.
PATCH /v1/organizations/{id}/budgets/{budgetId}budgets:writeChange a target. Sending metric at all is refused, even unchanged.
DELETE /v1/organizations/{id}/budgets/{budgetId}budgets:writeDelete a target.
PUT /v1/organizations/{id}/budgets/monthlybudgets:writeSave a whole year of one metric's monthly grid and get the affected set back.
POST /v1/reportsreports:writeCreate a report definition; its id joins this client's report allow-list.
PATCH /v1/reports/{reportId}reports:writeChange a report the API created. type is immutable.
DELETE /v1/reports/{reportId}reports:writeDelete a report the API created.
POST /v1/organizations/{id}/ad-accountsad-accounts:writeLink 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

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

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 sendWhat you get
The same key with the same requestThe original response, replayed, plus Idempotent-Replayed: true. The write does not run twice.
The same key with a different request422 idempotency-key-mismatch
The same key while the first is still running409 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}/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.