Authentication & Scoped API Keys

Scoped API keys (§1.5) - how to create keys, the 27-scope catalog, scope hierarchy, rotation, and the /whoami endpoint for introspection.

8 min read
authenticationapi-keysscopes

Every request to the CodeCourier REST API authenticates with a scoped project API key. Each key carries an explicit allow-list of 1 or more scopes (see the catalog below). The server rejects requests whose scope is not present on the key with a 403 SCOPE_DENIED response in the v2 error envelope (see errors).

Creating a Key

In the dashboard: Project Settings → API Keys → Generate. Name the key (e.g. ci-pipeline) and tick the scopes it needs. The full secret displays exactly once - copy it to your secrets manager immediately.

Programmatic creation:

curl

curl -X POST https://<your-deployment>.convex.site/api/v1/project/api-keys/generate \
  -H "Authorization: Bearer cc_live_<owner-key>" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "ci-pipeline",
    "scopes": ["runs:read", "runs:write", "workflows:read"]
  }'

TypeScript (fetch)

const res = await fetch(
  "https://<your-deployment>.convex.site/api/v1/project/api-keys/generate",
  {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${process.env.CC_OWNER_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      name: "ci-pipeline",
      scopes: ["runs:read", "runs:write", "workflows:read"],
    }),
  }
);
const { data } = await res.json();
console.log(data.key); // cc_live_... - shown once

Python (requests)

import os, requests

res = requests.post(
    "https://<your-deployment>.convex.site/api/v1/project/api-keys/generate",
    headers={
        "Authorization": f"Bearer {os.environ['CC_OWNER_KEY']}",
        "Content-Type": "application/json",
    },
    json={
        "name": "ci-pipeline",
        "scopes": ["runs:read", "runs:write", "workflows:read"],
    },
)
print(res.json()["data"]["key"])  # shown once

The 27-Scope Catalog

Scopes follow a resource:action pattern. read implies list + get; write implies create + update + delete.

  • Projects: projects:read, projects:write
  • Workflows: workflows:read, workflows:write
  • Runs: runs:read, runs:write, runs:cancel
  • Personas: personas:read, personas:write
  • Issues: issues:read, issues:write
  • Sandboxes: sandboxes:read, sandboxes:write, sandboxes:exec
  • Contexts: contexts:read, contexts:write
  • Assets: assets:read, assets:write
  • Learnings: learnings:read, learnings:write
  • Cost Rates: cost-rates:read, cost-rates:write
  • Recurring Tasks: recurring-tasks:read, recurring-tasks:write
  • Webhooks: webhooks:read, webhooks:write
  • Team: team:read, team:write
  • Meta: * (full access - use sparingly)

Scope Hierarchy

  • * satisfies every scope check. Reserve for owner-level automations.
  • resource:write does not imply resource:read. Grant both explicitly if the caller needs to list before mutating.
  • Legacy keys created before §1.5 were grandfathered with * - audit and tighten via /whoami.

/whoami - Key Introspection

GET /api/v1/whoamireturns the current key's identity and active scopes. Useful for CI preflight checks.

curl

curl https://<your-deployment>.convex.site/api/v1/whoami \
  -H "Authorization: Bearer cc_live_..."

TypeScript

const r = await fetch(
  "https://<your-deployment>.convex.site/api/v1/whoami",
  { headers: { "Authorization": `Bearer ${key}` } }
);
const { data } = await r.json();
// { keyId, projectId, name, scopes: [...], createdAt, lastUsedAt }
if (!data.scopes.includes("runs:write")) throw new Error("scope missing");

Python

import requests
r = requests.get(
    "https://<your-deployment>.convex.site/api/v1/whoami",
    headers={"Authorization": f"Bearer {key}"},
)
data = r.json()["data"]
assert "runs:write" in data["scopes"], "scope missing"

Rotation

  1. Generate a new key with the same (or tighter) scope set.
  2. Deploy the new key to your runtime; wait one full request cycle.
  3. POST /api/v1/project/api-keys/revoke on the old key.
  4. Confirm the revoked key returns 401 unauthorized.

Audit usage via lastUsedAt - keys idle for 90+ days should be revoked.

Related