Scopes and access
A key’s authority is two independent choices, both made once when the key is created and never widened afterwards: what it may do (its scopes) and what it may touch (its resource selector). A status page needs to read one rotation; a deployment gate needs to advance one; neither needs to be able to rewrite a schedule. Splitting the two means a key can be exactly as small as its job.
Scopes
Section titled “Scopes”| Scope | Covers |
|---|---|
read:oncall |
The whole read surface: rotations, schedules, on-call state, and the directory lookups behind them. Always granted, whether or not you asked for it |
write:duty |
Advancing the rotation, and setting who is on duty |
write:rotation |
Creating and updating rotations and their configuration |
Reading is one scope rather than several, and every key holds it: asking for write:duty alone gives you a key holding read:oncall and write:duty. Writes are guarded by If-Match and the validator it takes comes from a read, so a write-only key could not perform a write in the first place.
write:duty covers POST /v1/rotations/{rotationId}/rotate and PUT /v1/rotations/{rotationId}/on-call: the two calls that change who holds the shift, and nothing about how the rotation is set up.
write:rotation covers PATCH /v1/rotations/{rotationId}, PUT /v1/rotations/{rotationId}/schedule and POST /v1/rotations. It is the wider of the two by some distance: a key holding it can rename a rotation, change who owns it, replace its schedule and create new rotations. A key that only needs to advance a shift should not hold it.
The roster is the exception in both directions: who is in a rotation cannot be changed over /v1 at all, whatever scopes a key holds.
A request that needs a scope the key does not hold is refused 403 with code: "scope_denied", not 404, so the refusal points you at the key rather than at a suspected typo in the path.
Which rotations a key may reach
Section titled “Which rotations a key may reach”Every key carries one of two selectors:
| Selector | The key reaches | Who can create one |
|---|---|---|
AllRotations |
Every rotation in the workspace, including ones created later | A workspace admin |
Rotations |
Only the rotations named at creation | Anybody who already administers each of those rotations |
The second is the one to reach for by default, and it needs no admin in the loop: a rotation’s owner can mint a key for their own rotation, and that key can never reach further than the person who created it could.
A request for a rotation outside the selector is refused 403 with code: "resource_denied", and so is POST /v1/rotations from a Rotations key: a new rotation is not one of the rotations it names. Collections respect the selector too: GET /v1/rotations returns only the rotations the key may see, and the page counts describe that same set, so paging never walks past the edge of what the key is allowed to know about.
Check what a key holds
Section titled “Check what a key holds”GET /v1/keys/self answers with the key’s own scopes and selector, which is why it is worth calling from your integration’s startup or health check: a 403 in production and a keys/self response side by side tell you immediately whether the key was minted wrong or the request was.
{ "scopes": ["read:oncall"], "selector": "Rotations", "rotationIds": ["664f…"]}rotationIds is empty when the selector is AllRotations: there is no list, because the answer is “all of them, including the ones that do not exist yet”.
One workspace, always
Section titled “One workspace, always”A key belongs to exactly one Slack workspace, and the workspace is read from the key rather than from the request. No endpoint takes a workspace id, so there is no parameter to substitute and no way for a key to reach another workspace’s data. On Enterprise Grid a key sees its own workspace, the same way the dashboard does.
Next steps
Section titled “Next steps”- Authenticating with an API key: presenting the key, and what each
401means - Errors: the full code vocabulary, including the two refusals above
- API reference: which scope each endpoint requires
