Skip to main content

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.

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.

What the token carries

The token is a JWT, and its claims are the boundary the API enforces:

ClaimMeaning
subThe client id — a machine token's subject is the client itself, never a person.
audlevel-api. A token without it is refused on every /v1 path.
scopeThe scopes actually granted for this token.
organization_idThe one organization this client reads.
report_ids / ad_account_idsThe allow-lists, as issued.
reports_all / ad_accounts_allWhether 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.statehealthy means the plan was confirmed within the last five minutes. stale means Level's billing store is briefly unreachable and the last known plan is still being honoured. unknown means nothing usable is left, and the data endpoints are answering 503.
  • quota.stateok with live remaining / resetAt, or unavailable, in which case the metered endpoints are answering 503. 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, or null when none of them expires. Worth alerting on.

How long a revocation takes

Two numbers, and they differ on purpose:

What you revokeNew tokensA token already issued
A secretRefused immediatelyKeeps working until it expires — up to an hour
The whole clientRefused immediatelyStops 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.