Roles and permissions
Every organization-scoped and environment-scoped endpoint runs the same check: does the caller have at least this role, on this organization or environment? Set up your workspace introduces the three roles in passing; this page is the full picture, including how to read a 403 that does not tell you why.
The three roles#
Lowercase on the wire — owner, admin, user — and each one implies
the ones below it:
| Role | Reaches |
|---|---|
owner | Every environment in the organization, implicitly. Billing, member management, organization deletion. Exactly one per organization, assigned at creation, no transfer operation. |
admin | Every environment in the organization, implicitly. Everything but the owner-only organization actions. |
user | Only the environments explicitly granted to them. |
"Reaches every environment implicitly" is not a convenience default —
owner and admin do not hold per-environment grants at all, because
they do not need them. Only a user-role membership has anything to
grant.
Environment-level access grants#
A user-role member starts with access to nothing. Give them an
environment:
curl -X POST "$API/organizations/$ORG/members/$MEMBER/environment-access" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"environment_id": "..."}'
Granting access to an owner or admin membership is rejected outright
— they already have it, implicitly, and a grant that does nothing would
just be confusing to read later. This is how you hand a contractor
staging and nothing else: add them to the organization as user, then
grant only that one environment.
A member can always read their own membership and their own environment
grants (GET /organizations/{organization_id}/members/me, and the same
member id's /environment-access), regardless of role — that exemption
covers reading only. Granting and revoking access stays an
Owner/Admin-only action, and the exemption is keyed to the caller's own
membership id, resolved server-side from the verified token — never to a
member_id the client asserts, so it cannot be used to read anyone else's
grants.
API keys do not inherit a role's implicit reach#
Everything above describes a human membership. An API key carries one of
the same three role names, but it participates in this model differently
in one specific way that is easy to assume works like a membership and
does not: a key never gets an owner or admin's implicit "every
environment in the organization" reach, regardless of its role. An
owner-scoped key is checked against the one environment it was minted
for, exactly like a user-scoped one — role only ever affects what it
may do inside that environment, never how many environments it can
reach. See Authentication for how a key is
minted and why this is enforced as a hard equality check independent of
role.
A key also answers no organization-wide question at all — nothing that
is not scoped to an environment. Listing an organization's members,
listing its environments, anything behind an organization-level role
check: a key gets 404 there, the same response a non-member gets,
regardless of the role it carries.
Reading a 403 — and a 404 that means the same thing#
Two different failures collapse to two different status codes, and the choice is deliberate:
- You are not a member of the organization at all → 404. Not 403. A non-member cannot even confirm the organization exists, so the response looks identical to "no such organization."
- You have a
user-role membership with no grant for this specific environment → 404 as well, for the identical reason: auser-role member should not be able to tell "this environment does not exist" from "it exists but I can't see it." - You are a member, with a role, but it falls short of what the endpoint requires → 403. You already know the resource exists — you got this far — so there is nothing left to hide.
So a 403 always means: you can see this, you're just not allowed to do it. A 404 on a resource you swear you just created usually means your token's role does not extend to it — check whether it needs an environment grant before assuming it was deleted.
Every error carries the shared body ({ "error": "forbidden", "message": "..." } for a 403, { "error": "not_found", "message": "..." } for the
404 case) — see
Errors, pagination and versioning.
The check fails closed#
If the identity service that resolves membership cannot be reached, the request is refused — never treated as authorized. An outage in the authorization path shows up as errors, not as everyone suddenly having access.
Next: Sending commands to a charger, which uses
this same role model at the user floor for every command.