API reference

The client ↔ server API surface

Transport: HTTPS REST, bearer token, tenant-scoped. All client input is untrusted — the server re-parses raw stdout, computes fingerprints itself, and echoes prompt_hash for integrity.

Endpoints

Enroll

POST /v1/enroll
     { enrollment_code }
  → { client_token, client_id, org_id }

Enrollment codes are minted in the web UI or via bun run src/mint-enrollment-code.ts — single-use, expiring.

Identity & catalog

GET /v1/me       → { org, projects[], models[], entitlements }
GET /v1/models   → [{ code, display_name, provider }]   # NO command_template — ever

The model catalog is identity-only; command_template is machine-specific and lives in the client's config.toml.

Campaigns

POST /v1/projects/{id}/campaigns
     { type, git:{branch,commit,remote_url,dirty}, base_commit?, changed_paths?, config_version? }
  → { campaign_id, config_version_id, status, reused }
The server has no source tree → the client must send changed_paths for incremental campaigns. The server intersects them with pass-scope globs + risk triggers. Paths travel, contents do not. Dedup identity is keyed by (project, branch, commit, type) so re-creating the same campaign reuses it.

Claim, heartbeat, results

POST /v1/campaigns/{id}/claim
     { client_id, models:[...], max_slices, lease_seconds }
  → { leases:[ { lease_id, slice_id, model_code, pass_code, bucket_code,
                 prompt:"<full rendered prompt.md>", prompt_hash, marker,
                 timeout_seconds, lease_expires_at } ] }

POST /v1/leases/{lease_id}/heartbeat  → { lease_expires_at }     # best-effort, online clients only

POST /v1/leases/{lease_id}/results
     { exit_code, duration_ms, model_code, client_version, prompt_hash,
       stdout:"<raw output.txt>",   # inline if KB-scale, else presigned upload
       stderr_tail }
  → { accepted, parsed_report_count, warnings[] }             # idempotent on (lease_id, prompt_hash)

Gate

GET /v1/campaigns/{id}/gate  → { status: pass|fail|pending, new_findings:[...] }

Cheap CI poll — no model creds or long runtimes in CI.

Lease policy

The client requests lease_seconds sized to its batch (server caps it, e.g. 24h). Heartbeats extend a lease when the client is online but are not requiredclaim and submit are the only calls that need connectivity. On expiry the slice requeues.

  • A late result is still accepted if no other lease has completed the slice.
  • If one has, first-accepted-wins and the late output is stored as a provenance artifact.
  • Never double-billed — billing happens once at claim, idempotent per slice_id.

Error semantics

StatusMeaningClient action
200accepted
202accepted-as-artifact / superseded
409prompt_hash mismatchre-claim
413payload too largeuse presigned upload / enable --redact
429rate limitedretry with backoff
5xxserver errorretry with backoff

Network errors, 429, and 5xx are retried with exponential backoff + jitter by the agent.

Webhooks (optional)

POST /v1/webhooks/github — enabled by GITHUB_WEBHOOK_SECRET. On pull_request opened/synchronized it maps the PR's repo_remote_url to a project, fetches changed paths from the forge API (paths only), and creates an incremental campaign.

Next steps