Skip to documentation
ScrumPulseDocumentation
v2.0 docsSign in
Developers

Machine API keys

Give an external overseer such as Hermes authenticated, least-privilege access to ScrumPulse from its own VM.

Last updated August 8, 2026

An external supervisor — Hermes on its own VM, a monitoring job, a CI step — cannot mint a Clerk session token. It authenticates with a machine API key instead.

A key carries identity and tenancy only. Its role lives in the same org_role_assignments table that governs humans, under the synthetic principal apikey:<id>. Machines therefore authorize through exactly the same RBAC path as people, and there is no second privilege system to audit.

Two protections follow from that automatically:

  • A machine can never be an admin. get_access_context() demotes any database-sourced admin to member, because admin is a Clerk-owned boundary.
  • Machine principals cannot be retargeted through the member endpoints. /api/rbac/members iterates Clerk organization memberships, so a machine never appears there and PUT /api/rbac/members/{id} cannot reach it.

Minting a key#

Requires roles:manage, which is not delegable — so key management stays with Clerk organization admins.

curl -X POST https://scrumpulse-api.vercel.app/api/org/api-keys \
  -H "Authorization: Bearer <clerk-session-token>" \
  -H "Content-Type: application/json" \
  -d '{
        "name": "hermes-overseer",
        "role": "scrum_master",
        "allowed_ips": ["203.0.113.7"],
        "expires_in_days": 90
      }'
FieldNotes
nameLabel shown in listings. Required.
rolemanager, scrum_master, product_manager, or member. admin is rejected.
allowed_ipsOptional IP/CIDR allowlist. See the caveat below.
expires_in_daysOptional, 1–3650. Prefer a bounded lifetime and rotate.

The response contains secret exactly once. Only a SHA-256 hash is stored and lookup is by hash, so it cannot be recovered later and a stolen database yields no usable keys. Put it straight into the VM's secret store.

Using a key#

Either header works:

curl https://scrumpulse-api.vercel.app/api/oversight/summary -H "X-API-Key: sp_live_..."
curl https://scrumpulse-api.vercel.app/api/oversight/summary -H "Authorization: Bearer sp_live_..."

Clerk sessions are unaffected: only credentials carrying the sp_ prefix take the machine path, so JWTs fall through untouched.

Choosing a role#

Pick the least privilege that does the job.

RoleCanCannot
memberRead everything (app:read)Any mutation
scrum_masterApprove/reject actions, trigger and execute runs, scheduleBacklog authoring, credentials, billing
product_managerBacklog and work itemsApprovals, runs, credentials, billing
managerThe above plus org settings and feature flagsCredentials, billing, role administration

An overseer that only watches should be member. One that also clears the approval queue should be scrum_master. No role can manage keys or billing — those need Clerk admin.

The oversight endpoint#

GET /api/oversight/summary?window_hours=24 answers "is the bot healthy, and is anything waiting on a human?" in one call, so a supervisor does not have to stitch together six endpoints:

{
  "health": "healthy",
  "agent": { "is_running": false, "write_mode": "dry_run", "is_suspended": false },
  "runs": { "total": 12, "failed": 0, "by_status": { "completed": 12 }, "last_run_at": "..." },
  "attention_required": { "actions_pending_approval": 3, "actions_failed": 0 },
  "caller": { "auth_method": "api_key", "role": "scrum_master", "principal_id": "apikey:..." }
}

health is one of healthy, warning, degraded, idle, or suspended, computed server-side so the overseer does not encode its own thresholds. From there Hermes acts through the normal endpoints its role permits — /api/runs, /api/approvals/inbox, /api/runs/{id}/actions/{id}/approve.

IP allowlisting caveat#

allowed_ips compares against the source address the API observes. X-Forwarded-For is honored only when the immediate peer is listed in TRUSTED_PROXY_CIDRS; otherwise any caller could spoof past the allowlist by setting the header themselves.

Behind a platform edge with TRUSTED_PROXY_CIDRS unset, the observed address is the edge, not your VM — so the allowlist will not do what you expect. Either set TRUSTED_PROXY_CIDRS to the platform's proxy range, or rely on the secret plus a short expiry instead.

Managing keys#

GET    /api/org/api-keys        # list; never returns secrets
POST   /api/org/api-keys        # mint
DELETE /api/org/api-keys/{id}   # revoke

Revocation also deletes the key's role assignment, so the principal holds no permissions even if a stale credential were somehow presented.

Security properties#

  • Secret shown once; only a SHA-256 hash persisted, and lookup is by hash.
  • Revocation and optional expiry, both checked on every request.
  • Optional per-key IP/CIDR allowlist.
  • Unknown, revoked, expired, and IP-blocked keys all return an identical bare 401 — the response never reveals which condition failed.
  • Each key gets its own rate-limit bucket, so a runaway polling loop cannot exhaust the limit for the humans in its organization.
  • last_used_at and last_used_ip are stamped on every accepted request for audit.