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.
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 oncePython (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 onceThe 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:writedoes not implyresource: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
- Generate a new key with the same (or tighter) scope set.
- Deploy the new key to your runtime; wait one full request cycle.
POST /api/v1/project/api-keys/revokeon the old key.- Confirm the revoked key returns
401 unauthorized.
Audit usage via lastUsedAt - keys idle for 90+ days should be revoked.