Authentication
Exchange the client secret for a one-hour token, read what the token may do with /v1/me, and know how long a revocation takes to bite.
The API speaks OAuth2 client credentials. You exchange the client id and secret for a bearer token, and send that token on every call. There is no user, no browser redirect and no consent screen — this is a machine talking to a machine.
Getting a token
curl -X POST https://api.marketing-bar.com/api/connect/token \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=client_credentials' \
--data-urlencode "client_id=$LEVEL_CLIENT_ID" \
--data-urlencode "client_secret=$LEVEL_CLIENT_SECRET" \
--data-urlencode 'scope=reports:read reports:data'
{
"access_token": "eyJhbGciOiJSUzI1NiIs…",
"token_type": "Bearer",
"expires_in": 3600
}
Then send it on every request:
curl -H "Authorization: Bearer $ACCESS_TOKEN" \
https://api.marketing-bar.com/v1/me
About the scope parameter
Ask for the scopes you need, separated by spaces. You may ask for fewer than the client was granted — never for more; a scope the client doesn't hold is refused.
level-api is the audience a token gets automatically once any public-API scope is
granted. Asking for it by name is refused with invalid_request. Ask for real
scopes — reports:read, campaigns:read and so on — and the audience comes with
them.
Tokens live one hour, and there is no refresh token
Ask again with the same secret when the current token nears its end. That is the whole lifecycle — deliberately, because a refresh token would be a second long-lived credential to protect.
In practice: cache the token in memory for a little under an hour, and re-exchange
on expiry or on the first 401.
/v1 sends no CORS headers, on purpose — a browser cannot call it, and a secret that
reaches a browser is a secret that has leaked. Exchange the token on your server, in
your scheduler, or inside the BI tool's own connector runtime.
What the token carries
The token is a JWT, and its claims are the boundary the API enforces:
| Claim | Meaning |
|---|---|
sub | The client id — a machine token's subject is the client itself, never a person. |
aud | level-api. A token without it is refused on every /v1 path. |
scope | The scopes actually granted for this token. |
organization_id | The one organization this client reads. |
report_ids / ad_account_ids | The allow-lists, as issued. |
reports_all / ad_accounts_all | Whether the matching allow-list was lifted. |
You never have to parse it yourself — GET /v1/me answers the same
questions in plain JSON.
GET /v1/me {#v1-me}
The one endpoint to reach for when something is wrong. It costs no quota, keeps
answering while a subscription is read-locked, and returns 200 even when every
other endpoint is refusing this same token.
{
"clientId": "lvl-org-3f9a2c71",
"organizationId": "9c7e2f10-0000-0000-0000-000000000000",
"scopes": ["reports:read", "reports:data"],
"plan": { "code": "business", "state": "healthy" },
"quota": {
"limitPerHour": 1000,
"remaining": 874,
"resetAt": "2026-09-12T15:00:00Z",
"state": "ok"
},
"allowList": { "reports": 12, "adAccounts": "all" },
"nearestSecretExpiry": null
}
plan.state—healthymeans the plan was confirmed within the last five minutes.stalemeans Level's billing store is briefly unreachable and the last known plan is still being honoured.unknownmeans nothing usable is left, and the data endpoints are answering503.quota.state—okwith liveremaining/resetAt, orunavailable, in which case the metered endpoints are answering503. See Quotas & rate limits.allowList— a count, or the string"all"when the wildcard is on.nearestSecretExpiry— the soonest deadline among the client's dated secrets, ornullwhen none of them expires. Worth alerting on.
How long a revocation takes
Two numbers, and they differ on purpose:
| What you revoke | New tokens | A token already issued |
|---|---|---|
| A secret | Refused immediately | Keeps working until it expires — up to an hour |
| The whole client | Refused immediately | Stops within a second |
If a secret has leaked, revoke the client. Rotating or revoking the secret alone leaves whoever holds a live token up to an hour of access. See API clients.
When a token stops being accepted
Every one of these answers 401 with the problem type invalid-token, and
deliberately without saying which:
- expired, malformed, or missing
- issued to a client that has since been revoked
- a token of a person signed into Level rather than of an API client
- a token for something other than
level-api
The errors page has the recovery steps.