Scopes
The seven permissions an OAuth app can request, and the endpoints each one unlocks.
A scope is one line on the consent screen and one bundle of endpoints behind it. You send them space-delimited in the scope query parameter, and the token response echoes back the set you were actually granted.
scope=profile:read teams:read materials:readThe scopes
| Parameter | Type | Description |
|---|---|---|
| profile:read | read | The user's name, email address and profile picture. Unlocks GET /public/me. |
| teams:read | read | The names and ids of the teams the user granted. Unlocks GET /public/teams — the only reliable way to learn which x-team-id values your token is currently good for. |
| materials:read | read | List and read materials, including property values the team has already revealed. Unlocks GET /public/materials, GET /public/materials/:id and GET /public/materials/:id/properties/:propertyId, plus the legacy GET /public/library/search. |
| materials:pull | spends credits | Reveal material properties on the user's behalf. Unlocks POST /public/materials/:id/properties/pull and the legacy POST /public/library/pull-datapoint. Debits the team's credit balance. |
| processes:read | read | List and read processes, including impacts the team has already revealed. Unlocks GET /public/processes, GET /public/processes/:id and GET /public/processes/:id/impacts/:impactId. |
| processes:pull | spends credits | Reveal processes and their impacts on the user's behalf. Unlocks POST /public/processes/:id/pull and POST /public/processes/:id/impacts/pull. Debits the team's credit balance. |
| impact_methods:read | read | The impact methods available to the team. Unlocks GET /public/impact-methods. |
pull scopes are not read permissions — every call they authorize debits real credits from the balance of the team named in x-team-id. That is the user's money, spent by your code, without a second confirmation. Circa flags these scopes distinctly on the consent screen for exactly that reason.Only ask for them if pulling is a core part of what your app does, tell your users before you spend, and never pull in a retry loop — a failed request that actually succeeded server-side, retried five times, is five debits. A pull that runs out of credits returns
409 Conflict.Calling an endpoint you were not granted
The scope check runs before the endpoint does, and rejects the call with a 403 naming the first scope you are missing:
{
"error": "insufficient_scope",
"statusCode": 403,
"message": "This access token is missing the 'materials:read' scope."
}Branch on error, not on the message. This check applies to OAuth access tokens only — API-key requests carry no scopes and are never rejected by it.
Ask for the minimum
The consent screen is a conversion funnel. Every scope you add is another line the user reads and another reason to cancel, and the two pull scopes raise the bar considerably. Practical rules:
- Request only what your first release actually calls. You can ask for more later.
teams:readis nearly always worth including — without it you are guessing atx-team-idvalues.profile:readonly if you display or match on the user's identity.- Read the
scopefield in the token response and gate your UI on it. Never assume the grant matches the request. - Handle a
403 insufficient_scopefrom an endpoint whose scope you did not get, rather than crashing.
Re-authorization replaces, it does not merge
When you need a new scope — or a team the user did not originally tick — send the user through the authorization flow again with the full set you want. The new consent replaces the previous one; it is not unioned with it.
So if a user first granted materials:read processes:read and you later send them through asking only for materials:pull, you end up with materials:pull and nothing else — the reads are gone. The same applies to teams: the team selection on the new consent screen is the new list, not an addition to the old one. Always request the complete set you want to end up with.
invalid_scope before the user sees a consent screen — so add the scope to the app first, then change your authorize URL.