> ## Documentation Index
> Fetch the complete documentation index at: https://docs.gavai.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Scopes and least privilege

> How gavAI tokens are scoped, what each action verb grants, and how to mint a key that does exactly one job.

export const Def = ({term, type, defaultValue, required, children}) => <>
    <dt className="gv-def-list__term">
      <span className="gv-def-list__name">{term}</span>
      {type && <span className="gv-def-list__type">{type}</span>}
      {required && <span className="gv-def-list__required">required</span>}
      {defaultValue !== undefined && <span className="gv-def-list__default">
          default <code>{defaultValue}</code>
        </span>}
    </dt>
    <dd className="gv-def-list__description">{children}</dd>
  </>;

export const DefList = ({children}) => <dl className="gv-def-list">{children}</dl>;

export const NumberedStep = ({n, title, children}) => <li className="gv-step">
    <span className="gv-step__index">{n}</span>
    <div className="gv-step__body">
      <h3 className="gv-step__title">{title}</h3>
      <div className="gv-step__content">{children}</div>
    </div>
  </li>;

export const NumberedSteps = ({children}) => <ol className="gv-steps">{children}</ol>;

export const ArrowItem = ({children}) => <li className="gv-arrow-item">
    <span className="gv-arrow-item__arrow" aria-hidden="true">{"↳︎"}</span>
    <span className="gv-arrow-item__content">{children}</span>
  </li>;

export const ArrowList = ({children}) => <ul className="gv-arrow-list">{children}</ul>;

export const FeatureTable = ({columns, rows, featuredRow, caption, variant}) => <div className={`gv-feature-table ${variant ? `gv-feature-table--${variant}` : ""}`}>
    <table>
      {caption && <caption className="gv-feature-table__caption">{caption}</caption>}
      <thead>
        <tr>
          {columns.map(col => <th key={col.key} scope="col">
              <span className="gv-feature-table__header">{col.label}</span>
            </th>)}
        </tr>
      </thead>
      <tbody>
        {rows.map((row, i) => {
  const rowClass = [i === featuredRow ? "gv-feature-table__row--featured" : "", row.chosen ? "gv-feature-table__row--chosen" : ""].filter(Boolean).join(" ");
  return <tr key={i} className={rowClass}>
              {columns.map(col => {
    const v = row[col.key];
    if (variant === "tradeoff" && Array.isArray(v)) {
      const pill = col.key === "pros" ? "gv-feature-table__pill--pro" : col.key === "cons" ? "gv-feature-table__pill--con" : "";
      return <td key={col.key}>
                      {v.map((item, j) => <span key={j} className={`gv-feature-table__pill ${pill}`}>{item}</span>)}
                    </td>;
    }
    return <td key={col.key}>{v}</td>;
  })}
            </tr>;
})}
      </tbody>
    </table>
  </div>;

export const Eyebrow = ({label, tone = "default"}) => <div className={`gv-eyebrow gv-eyebrow--${tone}`}>
    <span className="gv-eyebrow__bar" aria-hidden="true" />
    <span className="gv-eyebrow__label">{label}</span>
  </div>;

<Eyebrow label="SCOPES" />

Every gavAI API endpoint requires a scope, and every token carries the exact scope set it was minted with — nothing more. This page is the catalogue: it names each action verb, lists every scope and the endpoints it gates, and walks the four steps to mint a key that can do only what your integration actually needs. By the end you will be able to look at an endpoint and name the smallest scope set that calls it.

The shape to hold in your head: scopes follow `<resource>:<action>`. Resources are things like `pages`, `secrets`, `domains`. Actions are one of four verbs. Workspace API keys are minted with an explicit scope set; OAuth session tokens inherit the authenticated user's permissions for the active workspace.

## The four action verbs

<DefList>
  <Def term="read" type="verb">
    Read-only access to the resource.
  </Def>

  <Def term="write" type="verb">
    Create, update, and modify the resource.
  </Def>

  <Def term="set" type="verb">
    Write secrets. Separated from `write` because secret values are write-only — they cannot be read back after being stored.
  </Def>

  <Def term="destructive" type="verb">
    Delete, rotate, or cancel — any operation that is irreversible.
  </Def>
</DefList>

## The implication chain

`:destructive` implies `:write`, and `:write` implies `:read`. A token granted `tokens:destructive` can also read and write tokens — it holds the full chain.

The chain is a convenience for the broad cases. The recommendation is the opposite: grant the narrowest verb that does the job. If your integration only lists tokens, grant `tokens:read` and stop. Broad grants compound over time, and a key that holds more than it needs is the one that hurts when it leaks.

## Scope catalogue

Every endpoint in the public API is in this table. If you can't find an endpoint here, it's not callable with an API key — it's an internal route or requires an OAuth session.

<FeatureTable
  columns={[
{ key: "scope", label: "Scope" },
{ key: "endpoints", label: "Endpoints" },
]}
  rows={[
{
  scope: "`me:read`",
  endpoints: "`GET /v1/me`",
},
{
  scope: "`workspaces:read`",
  endpoints: "`GET /v1/workspaces`, `GET /v1/workspaces/current`, `GET /v1/workspaces/{slug}`",
},
{
  scope: "`workspaces:write`",
  endpoints: "`POST /v1/workspaces/{slug}/switch`",
},
{
  scope: "`tokens:read`",
  endpoints: "`GET /v1/workspaces/{slug}/tokens`, `GET /v1/workspaces/{slug}/tokens/{id}`",
},
{
  scope: "`tokens:write`",
  endpoints: "`POST /v1/workspaces/{slug}/tokens`",
},
{
  scope: "`tokens:destructive`",
  endpoints: "`POST /v1/workspaces/{slug}/tokens/{id}/rotate`, `DELETE /v1/workspaces/{slug}/tokens/{id}`",
},
{
  scope: "`pages:read`",
  endpoints: "`GET /v1/workspaces/{slug}/pages`, `GET /v1/workspaces/{slug}/pages/{id}`",
},
{
  scope: "`pages:write`",
  endpoints: "`PATCH /v1/workspaces/{slug}/pages/{id}`, `POST /v1/workspaces/{slug}/pages-publish`",
},
{
  scope: "`pages:destructive`",
  endpoints: "`POST /v1/workspaces/{slug}/pages/{id}` (roll back to prior version)",
},
{
  scope: "`theme:read`",
  endpoints: "`GET /v1/workspaces/{slug}/theme`",
},
{
  scope: "`theme:write`",
  endpoints: "`POST /v1/workspaces/{slug}/theme`, `PATCH /v1/workspaces/{slug}/theme`",
},
{
  scope: "`forms:read`",
  endpoints: "`GET /v1/workspaces/{slug}/forms`, `GET /v1/workspaces/{slug}/forms/{id}`, `GET /v1/workspaces/{slug}/forms/{id}/submissions`",
},
{
  scope: "`agents:read`",
  endpoints: "`GET /v1/workspaces/{slug}/agents`, `GET /v1/workspaces/{slug}/agents/{id}`, `GET /v1/workspaces/{slug}/agents/{id}/runs`",
},
{
  scope: "`agents:write`",
  endpoints: "`POST /v1/workspaces/{slug}/agents`, `POST /v1/workspaces/{slug}/agents/{id}/activate`, `POST /v1/workspaces/{slug}/agents/{id}/deactivate`",
},
{
  scope: "`secrets:read`",
  endpoints: "`GET /v1/workspaces/{slug}/secrets`, `POST /v1/workspaces/{slug}/secrets/proxy-url`",
},
{
  scope: "`secrets:set`",
  endpoints: "`PUT /v1/workspaces/{slug}/secrets` (write a secret value — write-only, cannot be read back)",
},
{
  scope: "`secrets:write`",
  endpoints: "`POST /v1/workspaces/{slug}/secrets/{name}/rotate`",
},
{
  scope: "`secrets:destructive`",
  endpoints: "`DELETE /v1/workspaces/{slug}/secrets/{name}`",
},
{
  scope: "`domains:read`",
  endpoints: "`GET /v1/workspaces/{slug}/domains`, `GET /v1/workspaces/{slug}/domains/{id}/status`",
},
{
  scope: "`domains:write`",
  endpoints: "`POST /v1/workspaces/{slug}/domains`, `POST /v1/workspaces/{slug}/domains/{id}/verify`",
},
{
  scope: "`domains:destructive`",
  endpoints: "`DELETE /v1/workspaces/{slug}/domains/{id}`",
},
{
  scope: "`logs:read`",
  endpoints: "`GET /v1/workspaces/{slug}/logs`, `GET /v1/workspaces/{slug}/logs/search`, `GET /v1/workspaces/{slug}/logs/tail`",
},
{
  scope: "`deployments:read`",
  endpoints: "`GET /v1/workspaces/{slug}/deployments`, `GET /v1/workspaces/{slug}/deployments/{id}`",
},
{
  scope: "`deployments:write`",
  endpoints: "`POST /v1/workspaces/{slug}/deployments/{id}/promote`",
},
{
  scope: "`deployments:destructive`",
  endpoints: "`POST /v1/workspaces/{slug}/deployments/{id}/cancel`",
},
{
  scope: "`billing:read`",
  endpoints: "`GET /v1/workspaces/{slug}/billing`, `GET /v1/workspaces/{slug}/billing/usage`, `GET /v1/workspaces/{slug}/billing/invoices`",
},
{
  scope: "`billing:write`",
  endpoints: "`POST /v1/workspaces/{slug}/billing/plan`, `POST /v1/workspaces/{slug}/billing/payment-method`",
},
{
  scope: "`monetization:read`",
  endpoints: "`GET /v1/workspaces/{slug}/monetization`, `GET /v1/workspaces/{slug}/monetization/accounts`",
},
{
  scope: "`monetization:write`",
  endpoints: "`POST /v1/workspaces/{slug}/monetization`",
},
{
  scope: "`approvals:read`",
  endpoints: "`GET /v1/approvals`, `GET /v1/approvals/{id}`",
},
{
  scope: "`approvals:write`",
  endpoints: "`POST /v1/approvals/{id}/approve`, `POST /v1/approvals/{id}/deny`",
},
]}
/>

## Why `secrets:set` is its own verb

Secrets break the implication chain on purpose. A secret value is write-only — once stored, it can never be read back through the API, not even by the key that wrote it. That's why `secrets:set` is separate from `secrets:write`: writing the value of a secret is a different operation from rotating one, and the platform won't let you confuse them.

The practical consequence: a key that injects secrets into a vault (CI, a deploy pipeline) gets `secrets:set`. A key that manages secret lifecycle (rotation, deletion) gets `secrets:write` or `secrets:destructive`. A key that only proxies through to a downstream service via the secret value gets `secrets:read` — which returns metadata and a proxy URL, never the value.

## Minting a least-privilege key

<NumberedSteps>
  <NumberedStep n="01" title="List every operation the integration will perform">
    Be specific. "Reads pages and publishes drafts" is the right level. "Manages pages" is not — it hides whether you also need to delete or roll back.
  </NumberedStep>

  <NumberedStep n="02" title="Map each operation to a scope">
    Look each endpoint up in the catalogue above. Write down the verbs you need. For read-plus-publish that's `pages:read` and `pages:write` — no `:destructive`, because rolling pages back is a different operation.
  </NumberedStep>

  <NumberedStep n="03" title="Mint the key in the console">
    In the [gavAI console](https://console.gavai.app), go to **Settings → API tokens → New token**. Name it after the integration (`ci-publish-acme`, not `key-1`). Enter the scope set. Save. Copy the secret immediately — it is shown exactly once.
  </NumberedStep>

  <NumberedStep n="04" title="Don't pad for the future">
    If requirements change, mint a new key or call `POST /v1/workspaces/{slug}/tokens` to issue one with the wider scope set. A key that holds scopes "just in case" is the key that hurts when it leaks.
  </NumberedStep>
</NumberedSteps>

## API keys vs. OAuth — which to issue

The two credential types exist for different consumers; they aren't interchangeable.

<ArrowList>
  <ArrowItem>**API keys** are for machines. CI pipelines, backend services, scripted automations. They carry an explicit scope set defined at mint time and remain valid until rotated or revoked. The format is `gak_test_...` for the test environment and `gak_live_...` for live.</ArrowItem>
  <ArrowItem>**OAuth session tokens** are for humans. The CLI, IDE tooling, the console. OAuth tokens inherit the authenticated user's permissions for the active workspace, so you don't manage scope sets explicitly — but they're short-lived and not suitable for unattended automation.</ArrowItem>
</ArrowList>

The decision rule: if a person clicks a button to start the flow, OAuth. If a cron job or a CI runner starts it, API key.

<Card title="Authentication reference" href="/api-reference/authentication">
  Token formats, OAuth 2.1 PKCE flow, token lifecycle operations, and the full error code table.
</Card>
