Fleet API reference
fleet.ochk.io exposes a REST API for managing projects, registering signed delegations / actions / revocations / sub-delegations / federation envelopes, ingesting from CI/CD, downloading audit bundles, and managing webhooks.
OpenAPI spec
A hand-maintained OpenAPI 3.1 spec is served at:
https://fleet.ochk.io/api/openapi
Browse it interactively with try-it-out at:
https://fleet.ochk.io/api-explorer
Generate a typed client for any language with openapi-generator:
openapi-generator-cli generate \
-i https://fleet.ochk.io/api/openapi \
-g python \
-o ./oc-fleet-py
Authentication
Two schemes, mutually exclusive per request:
| Scheme | Header | When to use |
|---|---|---|
| Cookie | Cookie: oc_session=<JWT> | Browser flows. Set automatically when you sign in at ochk.io/signin; valid for every *.ochk.io subdomain. |
| Bearer token | Authorization: Bearer ock_<64-hex> | Server-side / CI / any non-browser caller. Mint at /settings § 03; plaintext returned ONCE at create. Authenticates as the project owner. |
Tokens take precedence over the cookie if both are supplied. A revoked token returns 401; a malformed Bearer falls through to anonymous.
Route catalog
| Group | Routes |
|---|---|
| Projects | GET/POST /api/projects, GET/PATCH/DELETE /api/projects/{id} |
| Members | GET/POST /api/members, PATCH/DELETE /api/members/{id} |
| Delegations | GET/POST /api/delegations, GET /api/delegations/{id}, POST /api/delegations/federation |
| Sub-delegations | GET/POST /api/subdelegations (kind 30086, OC Agent v1.1) |
| Actions | GET/POST /api/actions |
| Revocations | GET/POST /api/revocations |
| Audit | GET /api/audit/export?format=ndjson|json|csv, GET /api/audit/bundles |
| API tokens | GET/POST /api/tokens, PATCH/DELETE /api/tokens/{id} |
| Webhooks | GET/POST /api/webhooks/endpoints, GET/PATCH/DELETE /api/webhooks/endpoints/{id}, GET /api/webhooks/deliveries, POST /api/webhooks/deliveries/{id}/retry |
| Admin log | GET /api/admin/log?project_id=&limit=&before=&event_type= — append-only audit trail of privilege-changing operations (rename / archive, member CRUD, token CRUD, webhook endpoint CRUD). |
| Billing | POST /api/billing/portal (Stripe Customer Portal redirect), POST /api/checkout/{stripe,lightning} |
| Public | GET /api/health, GET /api/health/deep, GET /api/openapi, GET /api/auth/me, POST /api/auth/logout |
Error envelope
Every non-2xx response uses a uniform shape:
{
"ok": false,
"reason": "agent_must_match_delegation",
"detail": "optional human-readable detail"
}
reason is a stable enum-like string — safe to switch on. detail is
human-readable and may change without notice.
Reason catalog
The full set of stable reason strings the API can return, grouped by class.
This table is auto-generated at build time from
https://fleet.ochk.io/api/reasons — the
typed source of truth lives at src/server/api/reasons.ts in
orangecheck/oc-fleet-web. Adding
a new reason there flows here on the next deploy.
auth · 401/403
| reason | http | meaning |
|---|---|---|
agent_must_match_delegation | 403 | Action: signing agent ≠ parent delegation's agent. |
cross_site_blocked | 403 | Browser-driven state-changing request without same-origin Sec-Fetch-Site. |
honeypot | 403 | Contact form: hidden honeypot field was filled (bot signal). |
invalid_signature | 401 | Webhook receiver: HMAC verification failed. |
missing_signature | 401 | Webhook receiver: signature header absent. |
owner_required | 403 | Operation requires owner role (e.g. project archive). |
owner_role_requires_owner | 403 | Privilege escalation: only an owner can mint or remove another owner. |
principal_must_be_parent_agent | 403 | Sub-delegation: signing principal is not the parent's agent. |
principal_must_match_session | 403 | Body declares principal_address ≠ session.addr (forgery defense). |
role_forbidden | 403 | Caller's project role doesn't permit this operation. |
signer_must_be_principal | 403 | Revocation: signer is not the parent delegation's principal (v1.1). |
signer_must_match_session | 403 | Revocation: session.addr ≠ signer_address. |
unauthenticated | 401 | No valid cookie or Bearer token. |
unauthorized | 401 | Cron-secret or webhook receiver auth missing/invalid. |
input · 400
| reason | http | meaning |
|---|---|---|
bad_company | 400 | Contact form: company field too long. |
bad_email | 400 | Contact form: email failed RFC validation. |
bad_message | 400 | Contact form: message empty or too long. |
bad_name | 400 | Contact form: name field empty or too long. |
bad_request | 400 | Generic input-shape failure (only used where context makes it obvious). |
bad_topic | 400 | Contact form: topic not in the allowed enum. |
canonicalization_failed | 400 | Inputs don't form a valid canonical message. |
descriptor_id_mismatch | 400 | Federation: declared descriptor_id ≠ recomputed from inlined descriptor. |
descriptor_invalid | 400 | Federation: descriptor JSON failed schema validation. |
duplicate_guardian | 400 | Federation: two signatures share a guardian_address. |
federation_not_yet_supported | 400 | Sub-delegation: federation principals not yet allowed at this verb (v1.2 follow-up). |
federation_principal_wrong_endpoint | 400 | POST a federation principal to /api/delegations/federation, not /api/delegations. |
id_mismatch | 400 | Declared envelope id ≠ recomputed id from canonical inputs (tamper). |
insufficient_signatures | 400 | Federation: len(signatures) < M. |
invalid_body | 400 | Request body failed zod validation. See `detail`. |
invalid_id | 400 | Path :id was empty or malformed. |
invalid_query | 400 | Query string failed zod validation. See `detail`. |
malformed_json | 400 | Body wasn't parseable as JSON. |
malformed_signature | 400 | Federation: a signature value was missing or non-string. |
no_fields_to_update | 400 | PATCH body had no recognized fields. |
threshold_mismatch | 400 | Federation: sig.threshold ≠ descriptor.threshold. |
unknown_guardian | 400 | Federation: a signing guardian is not in the descriptor. |
tenancy · 404
| reason | http | meaning |
|---|---|---|
delegation_not_found | 404 | Action / revocation: delegation_id doesn't exist in this project. |
endpoint_not_in_project | 404 | Webhook delivery retry: endpoint belongs to a different project. |
not_found | 404 | Resource not visible to caller (or doesn't exist — we don't leak existence). |
parent_not_found | 404 | Sub-delegation: parent_id doesn't exist in this project. |
method · 405
| reason | http | meaning |
|---|---|---|
method_not_allowed | 405 | HTTP verb not allowed on this route. See `Allow` header. |
conflict · 409
| reason | http | meaning |
|---|---|---|
already_a_member | 409 | (project_id, address) duplicate on member invite. |
already_registered | 409 | Content-addressed PK collision — exact envelope already in registry. |
already_revoked | 409 | Revocation already applied to this delegation. |
already_succeeded | 409 | Webhook delivery retry: this delivery already succeeded. |
delegation_inactive | 409 | Action: parent delegation has been revoked or expired. |
endpoint_paused | 409 | Webhook delivery retry: endpoint.active = false. |
expired_cannot_restore | 409 | API token: past expires_at, restoration would be useless. |
last_owner_cannot_demote | 409 | Same — can't demote the last owner. |
last_owner_cannot_remove | 409 | Project foot-gun: refused to remove the last owner. |
parent_inactive | 409 | Sub-delegation: parent.status ≠ 'active'. |
tier_not_self_serve | 409 | Checkout: enterprise tier requires sales contact. |
rate · 429
| reason | http | meaning |
|---|---|---|
rate_limited | 429 | Per-IP, per-route token bucket exceeded. Backoff + jitter on retry. |
config · 503
| reason | http | meaning |
|---|---|---|
btcpay_not_configured | 503 | BTCPay Lightning checkout disabled — BTCPAY_* env unset. |
checkout_not_configured | 503 | Generic checkout config absent (price ids, etc). |
cron_secret_unset | 503 | CRON_SECRET unset — cron endpoint refuses to run. |
database_not_configured | 503 | DATABASE_URL unset — fleet is in v1.1 preview mode. |
mailer_not_configured | 503 | Resend API not configured — contact form can't send. |
no_stripe_customer | 503 | Customer Portal: this project hasn't paid via Stripe yet. |
stripe_not_configured | 503 | Stripe checkout / portal disabled — STRIPE_SECRET_KEY unset. |
webhook_master_key_unset | 503 | WEBHOOK_MASTER_KEY unset — endpoint registration disabled. |
webhook_not_configured | 503 | Stripe/BTCPay webhook receiver: signing secret env var unset. |
external · 5xx
| reason | http | meaning |
|---|---|---|
btcpay_error | 502 | BTCPay Server returned an error. |
btcpay_fetch_error | 502 | Failed to reach BTCPay Server (network/DNS). |
mailer_error | 502 | Resend API rejected the contact-form send. |
stripe_error | 502 | Stripe API returned an error; see `detail`. |
stripe_no_url | 502 | Stripe Customer Portal returned no `url` field. |
internal · 500
| reason | http | meaning |
|---|---|---|
apply_failed | 500 | Webhook delivery retry: apply step failed unexpectedly. |
insert_failed | 500 | Database insert returned no row (unexpected). |
update_failed | 500 | Database update returned no row (unexpected). |
70 reasons · auto-generated from /api/reasons at build time. Source of truth: src/server/api/reasons.ts in orangecheck/oc-fleet-web.
Rate limiting
Every route uses a per-IP, per-route token bucket. Limits are intentionally generous; you'll only see 429 if a single IP is hammering one route.
HTTP/1.1 429 Too Many Requests
{ "ok": false, "reason": "rate_limited" }
Apply jitter + exponential backoff in your retries.
Same-origin enforcement
Browser-driven POST / PATCH / DELETE requires the
Sec-Fetch-Site: same-origin header (modern browsers send it automatically).
Bearer-authenticated calls from a server runtime are exempt — the Bearer token
is sufficient evidence of intent. Cross-origin browser POSTs return 403
cross_site_blocked.
Idempotency
Every accepted envelope is content-addressed: its sha256 is its primary key.
Re-POSTing the exact same envelope returns 409 already_registered. This makes
client-side retry safe — you can fire the same POST twice without persisting
twice.
See also
- Integrations — the framework adapters that wrap these endpoints.
- Webhooks — the inbound side: subscribe to events.
/agent/spec— the underlying OC Agent envelope spec (canonical messages, scope grammar, signature rules)./agent/sub-delegation— kind 30086 chain semantics.