Skip to content

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.

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.

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.

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.

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:

Terminal window
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.

  1. Organization settings → Tokens → New token.

  2. Give it a name that says where it lives — github-actions/vision-build, not token1. This name is what you’ll see when deciding what to revoke.

  3. Pick the kind, the scopes, and an expiry in days.

  4. Copy the secret. It is shown exactly once.

Over the API
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
}'
Response — the only time you see the secret
{
"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.

Terminal window
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.

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.

  1. Create a new token with the same scopes and a clear name including the date: github-actions/vision-build-2026-07.

  2. Update the secret in your secret store (GitHub Actions secret, Vault, etc.).

  3. Run the pipeline and confirm it passes.

  4. Revoke the old token.

  5. 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.
  1. Revoke it first. Don’t investigate first — revoke, then investigate.
  2. Check the audit log for actions attributed to it.
  3. Check the token’s last-used IP for anything unexpected.
  4. Create a replacement with narrower scopes.
  5. 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.

Do

  • Keep them in a secret manager or your CI platform’s secret store.
  • Use --token-stdin or 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 .env files 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:admin token where repo:read would 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.

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).

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.