API tokens
API tokens are how everything that isn’t a browser authenticates. They belong to an organization, carry a set of scopes, and optionally expire.
The three kinds
Section titled “The three kinds”| Kind | Prefix | Acts as | Bound to | Use it for |
|---|---|---|---|---|
PAT |
cavs_pat_ |
you — capped by your org role | the organization | your laptop, the CLI, ad-hoc scripts |
REPO |
cavs_repo_ |
itself | one repository | a single deploy target or consumer |
CI |
cavs_ci_ |
itself | the organization | build pipelines |
The distinction that matters: a PAT is capped by your role. Its effective
permission is your role ∩ its scopes, so if you’re demoted to VIEWER, every
PAT you hold loses write access instantly. REPO and CI tokens have no user
behind them and are authorized by their scopes alone — which makes them the
right choice for automation that must keep working when people move around, and
the wrong choice to leave lying about.
Scopes
Section titled “Scopes”| Scope | Grants |
|---|---|
repo:read |
Read repositories and metadata; pull. Also satisfies org:read-level reads. |
repo:write |
Everything in repo:read, plus push and updating repositories. |
repo:admin |
Everything in repo:write, plus create/delete repositories and manage collaborators. |
org:read |
Read the organization and its member list. Also satisfies usage reads. |
usage:read |
Read usage and storage metrics. |
dedup:read |
List and download objects; read domains, namespaces and usage. |
dedup:write |
Everything in dedup:read, plus uploading and deleting objects. |
dedup:admin |
Everything in dedup:write, plus configuring domains and namespaces, rotating encryption keys, issuing gateway credentials, and running garbage collection. |
Scopes are a hard cap, never a grant: a repo:admin token held by a
VIEWER still can’t push.
The dedup scopes are separate from the repo scopes on purpose. A token handed
to a backup agent or an S3 application should be able to write objects through the
deduplication layer without also being able to push to Git repositories, so
dedup:admin grants nothing in Repos and repo:admin grants nothing in Dedup.
The live catalogue — every scope, what it grants, and which ones are destructive —
is served at GET /api/v1/scopes. The console renders that list rather than
keeping its own, so what you see in the token dialog is always what the server
enforces.
Choosing scopes
Section titled “Choosing scopes”| Job | Scopes |
|---|---|
| CI publishing build artifacts | repo:write |
| CI consuming a dataset | repo:read |
| A cost dashboard | usage:read |
| A provisioning script that creates repositories | repo:admin |
| Interactive use from your laptop | repo:write (+ usage:read if you use cav storage) |
| An application storing objects through the dedup SDK | dedup:write |
| A backup agent that only restores | dedup:read |
| Automation that runs garbage collection | dedup:admin |
Start narrow. Widening a token is one API call; recovering from a leaked
repo:admin token is not.
An unknown scope is rejected at creation with 400, naming the ones it did not
recognize. A token carrying a scope nobody recognizes grants nothing, and finding
that out at the first request rather than at creation is a bad way to spend an
afternoon.
Restricting a token to one dedup domain
Section titled “Restricting a token to one dedup domain”A token with dedup scopes reaches every domain in its organization unless you bind it. Binding narrows it to one domain, and optionally to one namespace inside that domain:
curl -sS -X POST "https://cavsnode.com/api/v1/organizations/acme-ai/tokens" \ -H "Authorization: Bearer $CAVS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "render-farm/asset-cache", "kind": "CI", "scopes": ["dedup:write"], "dedup_domain_id": "3f2a…", "dedup_namespace_id": "9c14…", "expires_in_days": 90 }'Both ids must belong to you: a domain from another organization, or a namespace
from a different domain, is refused with 404. dedup_namespace_id requires
dedup_domain_id — a namespace on its own does not say which deduplication
boundary it belongs to.
A bound token is answered 404, not 403, for anything outside its binding —
confirming that another domain exists is itself information. It also cannot create
a new domain or namespace, since that would be a way to grow out of its own
containment.
Creating one
Section titled “Creating one”-
Organization settings → Tokens → New token.
-
Give it a name that says where it lives —
github-actions/vision-build, nottoken1. This name is what you’ll see when deciding what to revoke. -
Pick the kind, the scopes, and an expiry in days.
-
Copy the secret. It is shown exactly once.
curl -sS -X POST "https://cavsnode.com/api/v1/organizations/acme-ai/tokens" \ -H "Authorization: Bearer $CAVS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "github-actions/vision-build", "kind": "CI", "scopes": ["repo:write"], "expires_in_days": 90 }'{ "token": { "id": "6f1a…", "name": "github-actions/vision-build", "kind": "CI", "prefix": "cavs_ci", "scopes": ["repo:write"], "expires_at": "2026-10-26T12:00:00Z", "created_at": "2026-07-28T12:00:00Z" }, "secret": "cavs_ci_9dK2…"}A REPO token additionally needs "repository_id": "<uuid>". Creating a token
requires the tokens.create permission (DEVELOPER and above) and counts
against your plan’s token limit.
Listing and revoking
Section titled “Listing and revoking”curl -sS "https://cavsnode.com/api/v1/organizations/acme-ai/tokens" \ -H "Authorization: Bearer $CAVS_TOKEN"
curl -sS -X DELETE "https://cavsnode.com/api/v1/organizations/acme-ai/tokens/$TOKEN_ID" \ -H "Authorization: Bearer $CAVS_TOKEN"The list shows name, kind, prefix, scopes, expiry, creation and last used
(timestamp and IP). Revocation is immediate — the next request with that token
gets 401. Both actions are recorded in the audit log.
Rotation
Section titled “Rotation”There is no in-place rotation; you create a replacement and retire the old one. This is deliberate — it means there’s always a window where both work, so rotation never causes an outage.
-
Create a new token with the same scopes and a clear name including the date:
github-actions/vision-build-2026-07. -
Update the secret in your secret store (GitHub Actions secret, Vault, etc.).
-
Run the pipeline and confirm it passes.
-
Revoke the old token.
-
Confirm the old token’s “last used” stops advancing.
Rotate on a schedule (quarterly is a reasonable default), and immediately when:
- Someone with access to it leaves.
- It may have been logged, pasted or committed.
- Its scope turns out to be wider than the job needs.
If a token leaks
Section titled “If a token leaks”- Revoke it first. Don’t investigate first — revoke, then investigate.
- Check the audit log for actions attributed to it.
- Check the token’s last-used IP for anything unexpected.
- Create a replacement with narrower scopes.
- If it was committed to Git, purge it from history — revocation is what actually protects you, but a live-looking secret in history invites confusion.
Storing tokens
Section titled “Storing tokens”Do
- Keep them in a secret manager or your CI platform’s secret store.
- Use
--token-stdinor an interactive prompt rather than a command-line flag. - Give each consumer its own token, so revocation is surgical.
- Set an expiry.
Don’t
- Commit them, including in
.envfiles that aren’t ignored. - Echo them in CI logs, or pass them as URL query parameters.
- Share one token across teams or environments.
- Use a
repo:admintoken whererepo:readwould do.
The CLI stores its token in ~/.config/cav/config.toml — a plain file. On a
shared machine, verify its permissions, or use CAVS_TOKEN per invocation
instead of logging in.
Limits per plan
Section titled “Limits per plan”| Plan | Max active tokens |
|---|---|
| Free | 5 |
| Developer | 50 |
| Team | 200 |
| Business | unlimited |
Exceeding it returns 402 quota_exceeded. Revoked tokens don’t count.
Not available: cavs_sk_ service-account keys
Section titled “Not available: cavs_sk_ service-account keys”The SDK contract documents cavs_sk_ service-account keys with per-key
rotation endpoints. That prefix has never existed — the three that do are
cavs_pat_, cavs_repo_ and cavs_ci_, and those endpoints return 404.
Fine-grained scoping is not what’s missing: use a CI token with the scopes the
job needs, bound to a domain or namespace where that applies. What is still manual
is rotation, which has no in-place endpoint by design (see Rotation).
Tokens cannot manage tokens
Section titled “Tokens cannot manage tokens”Creating and revoking tokens requires a browser session. No scope grants it,
including repo:admin, so a token cannot mint another token.
This is deliberate. Nothing in the model would cap what scopes a newly minted token could carry, so a token that can create tokens is a token that can widen its own reach — and the audit trail would show the escalation as ordinary automation. The cost is that rotation ends with a human in the loop; that is the intended trade.
- Roles & permissions — what a scope can and can’t reach.
- Authentication — how credentials are presented.
- Audit log — verifying token use.

