live · mainnetoc · docs
specs · api · guides
docs / api reference

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:

SchemeHeaderWhen to use
CookieCookie: oc_session=<JWT>Browser flows. Set automatically when you sign in at ochk.io/signin; valid for every *.ochk.io subdomain.
Bearer tokenAuthorization: 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

GroupRoutes
ProjectsGET/POST /api/projects, GET/PATCH/DELETE /api/projects/{id}
MembersGET/POST /api/members, PATCH/DELETE /api/members/{id}
DelegationsGET/POST /api/delegations, GET /api/delegations/{id}, POST /api/delegations/federation
Sub-delegationsGET/POST /api/subdelegations (kind 30086, OC Agent v1.1)
ActionsGET/POST /api/actions
RevocationsGET/POST /api/revocations
AuditGET /api/audit/export?format=ndjson|json|csv, GET /api/audit/bundles
API tokensGET/POST /api/tokens, PATCH/DELETE /api/tokens/{id}
WebhooksGET/POST /api/webhooks/endpoints, GET/PATCH/DELETE /api/webhooks/endpoints/{id}, GET /api/webhooks/deliveries, POST /api/webhooks/deliveries/{id}/retry
Admin logGET /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).
BillingPOST /api/billing/portal (Stripe Customer Portal redirect), POST /api/checkout/{stripe,lightning}
PublicGET /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

reasonhttpmeaning
agent_must_match_delegation403Action: signing agent ≠ parent delegation's agent.
cross_site_blocked403Browser-driven state-changing request without same-origin Sec-Fetch-Site.
honeypot403Contact form: hidden honeypot field was filled (bot signal).
invalid_signature401Webhook receiver: HMAC verification failed.
missing_signature401Webhook receiver: signature header absent.
owner_required403Operation requires owner role (e.g. project archive).
owner_role_requires_owner403Privilege escalation: only an owner can mint or remove another owner.
principal_must_be_parent_agent403Sub-delegation: signing principal is not the parent's agent.
principal_must_match_session403Body declares principal_address ≠ session.addr (forgery defense).
role_forbidden403Caller's project role doesn't permit this operation.
signer_must_be_principal403Revocation: signer is not the parent delegation's principal (v1.1).
signer_must_match_session403Revocation: session.addr ≠ signer_address.
unauthenticated401No valid cookie or Bearer token.
unauthorized401Cron-secret or webhook receiver auth missing/invalid.

input · 400

reasonhttpmeaning
bad_company400Contact form: company field too long.
bad_email400Contact form: email failed RFC validation.
bad_message400Contact form: message empty or too long.
bad_name400Contact form: name field empty or too long.
bad_request400Generic input-shape failure (only used where context makes it obvious).
bad_topic400Contact form: topic not in the allowed enum.
canonicalization_failed400Inputs don't form a valid canonical message.
descriptor_id_mismatch400Federation: declared descriptor_id ≠ recomputed from inlined descriptor.
descriptor_invalid400Federation: descriptor JSON failed schema validation.
duplicate_guardian400Federation: two signatures share a guardian_address.
federation_not_yet_supported400Sub-delegation: federation principals not yet allowed at this verb (v1.2 follow-up).
federation_principal_wrong_endpoint400POST a federation principal to /api/delegations/federation, not /api/delegations.
id_mismatch400Declared envelope id ≠ recomputed id from canonical inputs (tamper).
insufficient_signatures400Federation: len(signatures) < M.
invalid_body400Request body failed zod validation. See `detail`.
invalid_id400Path :id was empty or malformed.
invalid_query400Query string failed zod validation. See `detail`.
malformed_json400Body wasn't parseable as JSON.
malformed_signature400Federation: a signature value was missing or non-string.
no_fields_to_update400PATCH body had no recognized fields.
threshold_mismatch400Federation: sig.threshold ≠ descriptor.threshold.
unknown_guardian400Federation: a signing guardian is not in the descriptor.

tenancy · 404

reasonhttpmeaning
delegation_not_found404Action / revocation: delegation_id doesn't exist in this project.
endpoint_not_in_project404Webhook delivery retry: endpoint belongs to a different project.
not_found404Resource not visible to caller (or doesn't exist — we don't leak existence).
parent_not_found404Sub-delegation: parent_id doesn't exist in this project.

method · 405

reasonhttpmeaning
method_not_allowed405HTTP verb not allowed on this route. See `Allow` header.

conflict · 409

reasonhttpmeaning
already_a_member409(project_id, address) duplicate on member invite.
already_registered409Content-addressed PK collision — exact envelope already in registry.
already_revoked409Revocation already applied to this delegation.
already_succeeded409Webhook delivery retry: this delivery already succeeded.
delegation_inactive409Action: parent delegation has been revoked or expired.
endpoint_paused409Webhook delivery retry: endpoint.active = false.
expired_cannot_restore409API token: past expires_at, restoration would be useless.
last_owner_cannot_demote409Same — can't demote the last owner.
last_owner_cannot_remove409Project foot-gun: refused to remove the last owner.
parent_inactive409Sub-delegation: parent.status ≠ 'active'.
tier_not_self_serve409Checkout: enterprise tier requires sales contact.

rate · 429

reasonhttpmeaning
rate_limited429Per-IP, per-route token bucket exceeded. Backoff + jitter on retry.

config · 503

reasonhttpmeaning
btcpay_not_configured503BTCPay Lightning checkout disabled — BTCPAY_* env unset.
checkout_not_configured503Generic checkout config absent (price ids, etc).
cron_secret_unset503CRON_SECRET unset — cron endpoint refuses to run.
database_not_configured503DATABASE_URL unset — fleet is in v1.1 preview mode.
mailer_not_configured503Resend API not configured — contact form can't send.
no_stripe_customer503Customer Portal: this project hasn't paid via Stripe yet.
stripe_not_configured503Stripe checkout / portal disabled — STRIPE_SECRET_KEY unset.
webhook_master_key_unset503WEBHOOK_MASTER_KEY unset — endpoint registration disabled.
webhook_not_configured503Stripe/BTCPay webhook receiver: signing secret env var unset.

external · 5xx

reasonhttpmeaning
btcpay_error502BTCPay Server returned an error.
btcpay_fetch_error502Failed to reach BTCPay Server (network/DNS).
mailer_error502Resend API rejected the contact-form send.
stripe_error502Stripe API returned an error; see `detail`.
stripe_no_url502Stripe Customer Portal returned no `url` field.

internal · 500

reasonhttpmeaning
apply_failed500Webhook delivery retry: apply step failed unexpectedly.
insert_failed500Database insert returned no row (unexpected).
update_failed500Database 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.