The orbit-service management API runs on port 3001. The introspect hot path runs separately on port 3000.
All management endpoints require a tenant-scoped session token via Authorization: Bearer <token> unless noted.
Pagination: All list endpoints support cursor-based pagination via ?cursor=&limit= query parameters.
Responses return { data: T[], next_cursor: string | null }. Default limit is 20, max is 100.
Introspect (Port 3000)
The hot-path endpoint for real-time authorization checks. Authenticated with an API key, not a session token.
POST /check
Auth:Authorization: Bearer <api_key>
Resolves a user session into an authorization decision. Single CTE query: session → user → roles → role permissions → direct overrides → merged set → optional policy evaluation.
Request Body
Field
Type
Required
Description
token
string
Yes*
User session token (opaque)
access_token
string
Yes*
JWT access token (from orbit-oauth2)
tenant_id
string
Yes
Target tenant UUID
permission
string
No
Single permission to check
permissions
string[]
No
Batch permission check
resource
object
No
Resource context for policy evaluation
include
string[]
No
Extra fields to return: permissions, roles, flags
flags
string[]
No
Feature flags to evaluate
* Provide token (session-based) or access_token (JWT-based), not both.
Response
{
"authenticated": true,
"user": { "id": "...", "email": "...", "name": "...", "tenant_id": "..." },
"granted": true, // present if "permission" was requested
"grants": { "perm": true },// present if "permissions" batch was requested
"permissions": ["..."], // present if include contains "permissions"
"roles": ["..."], // present if include contains "roles"
"feature_flags": { "flag": true } // present if include contains "flags"
}
Stage a self-signup and email a verification link. Nothing is created yet — returns 202 { email, message }. See /auth/verify/confirm.
POST
/auth/verify/confirm
None
Confirm a signup or identity verification token. For a pending signup this creates the tenant + owner user and returns 200 { token, expires_at }; for an already-existing identity it just flips the verified flag and returns 204.
POST
/auth/verify/resend
None
Re-send the verification email for a pending signup or unverified identity. Body: { email }. Always returns 204 (no enumeration).
POST
/auth/login
None
Authenticate. With tenant_id: single-step. Without: pre-tenant session (next: "tenant_select").
POST
/auth/logout
Session
Delete current session. Returns 204.
POST
/auth/logout/all
Session
Delete all sessions for the user. Returns 204.
GET
/auth/me
Session
Current user + tenant + effective permissions.
GET
/auth/memberships
Session
List tenants the user belongs to. Returns TenantMembership[].
POST
/auth/session/exchange
Pre-tenant
Swap pre-tenant session for tenant-scoped session. Body: { tenant_slug }.
POST
/auth/invite/check
None
Check an invite token without consuming it. Body: { token }. Returns { status: "valid" | "used" | "expired" | "unknown" }.
POST
/auth/invite/accept
None
Accept invite, set password, get session. Body: { token, password }.
Policies add runtime conditions to permission checks. When a permission has attached policies,
the introspect endpoint evaluates each policy handler. AND logic: all must pass.
API keys authenticate consuming applications against the introspect endpoint.
The raw key is returned once at creation and stored as a SHA-256 hash.
Method
Path
Permission
Description
GET
/tenants/:id/api-keys
api-keys.read
List API keys (never exposes key hash).
POST
/tenants/:id/api-keys
api-keys.create
Create. Body: { name, expires_at? }. Returns raw key once.
DELETE
/tenants/:id/api-keys/:keyId
api-keys.revoke
Soft-revoke. Returns 204.
Audit Logs
Every mutation is recorded. Logs capture old/new values, the acting user, IP address, and timestamp.
Method
Path
Permission
Description
GET
/tenants/:id/audit-logs
audit.read
List logs. Filters: action, user_id, from, to (ISO timestamps), model_type. Default limit 50, max 100.
Feature Flags
Flags are defined globally and can be overridden per-tenant. Effective value = tenant override > global default.
Global Flags (System Tenant)
Method
Path
Permission
Description
GET
/feature-flags
feature-flags.read (system)
List all global flags.
POST
/feature-flags
feature-flags.create (system)
Create. Body: { name, enabled?, description? }.
GET
/feature-flags/:name
feature-flags.read (system)
Get global flag.
PATCH
/feature-flags/:name
feature-flags.update (system)
Update global default.
Tenant Flags
Method
Path
Permission
Description
GET
/tenants/:id/feature-flags
feature-flags.read
Effective flags (override + global merged).
GET
/tenants/:id/feature-flags/:name
feature-flags.read
Single effective flag value.
PUT
/tenants/:id/feature-flags/:name
feature-flags.update
Set tenant override. Body: { enabled }.
OAuth2 Clients
OAuth2 client management is proxied through orbit-service to orbit-oauth2's internal API. All mutations are audit-logged.
Clients created here are tenant-owned: only your tenant can see or mutate them, and POST accepts an optional
environment string that gets stamped into the client's issued tokens as the env claim. Clients with
a third-party consent_uri/consent_jwk aren't creatable here yet — see
POST /oauth2/register.
Method
Path
Permission
Description
GET
/tenants/:id/oauth2-clients
oauth2.read
List clients.
POST
/tenants/:id/oauth2-clients
oauth2.create
Create client. Secret returned once for confidential clients.
An issuer is either an acme directory (Let's Encrypt or any RFC 8555 CA) or an internal-ca
self-signed root for private/internal domains. Secret material (ACME account key, EAB HMAC) is referenced by
secret_id, never accepted or returned inline. An internal-ca issuer may bring its own
root (ca_certificate_pem + ca_private_key_pem, P-256 ECDSA only) or self-generate one.
Method
Path
Permission
Description
GET
/tenants/:id/issuers
issuers.read
List issuers. Lazily seeds a default internal-ca issuer if none exists.
Get issuer, including ca_certificate_pem for internal-ca issuers.
DELETE
/tenants/:id/issuers/:name
issuers.delete
Delete issuer. Returns 204. The default issuer cannot be deleted.
Certificates
Certificate orders are created and read through their owning domain. Issuance runs asynchronously on the job
worker: POST enqueues certificate.issue and returns 202 immediately; poll the
order or list endpoint for status to reach valid or failed.
certificate_pem/chain_pem are public once issued; the leaf private key is never returned,
only a reference to its secret id. A separate tenant-wide roll-up endpoint lists orders across every domain at
once — order-level history (which issuer signed what, failed orders anywhere in the fleet), complementing
the per-domain expiry snapshot at GET /domains.
Method
Path
Permission
Description
GET
/tenants/:id/certificates
certificates.read
Tenant-wide order roll-up across every domain, newest first. Optional ?status= filter (issuing/valid/failed).
GET
/tenants/:id/domains/:hostname/certificates
certificates.read
List orders for the domain, newest first.
POST
/tenants/:id/domains/:hostname/certificates
certificates.create
Request issuance. Domain must be verified. Returns 202 { job_id }.
Read-only visibility into the cert-module job queue: domain.verify, domain.probe-expiry,
and certificate.issue (renewals reuse the same type — there is no separate renewal job type).
This is the only place a job that failed before a certificate_orders row existed, or one sitting in
retry backoff, is visible at all.
Method
Path
Permission
Description
GET
/tenants/:id/jobs
jobs.read
List cert-module jobs, most recently updated first. Optional ?status= filter (pending/running/succeeded/failed).
GET
/tenants/:id/jobs/health
jobs.read
Worker fleet liveness (last heartbeat, last renewal/probe sweep) plus this tenant's own overdue-pending/stuck-running job counts. No worker ids/hostnames/pids — that system-wide detail is operator-only, via scripts/diagnose-workers.ts.
Secrets
Encrypted key/value storage, scoped by tenant and optionally by environment. Session (dashboard) endpoints never
return plaintext, only ciphertext metadata (version, key version, active flag) — the only place plaintext is
ever decrypted is POST /tenants/:id/secrets/lease, a separate JWT-authed endpoint for machine clients
(no session token, no dashboard route). A secret only becomes leasable once escrow_enabled is set via
the escrow endpoint; cert-module private keys (leaf keys, ACME account keys, internal-CA root keys) are created
with escrow off. POST .../rotate and PUT .../values both write a new version, but differ
in whether a value must already exist: PUT requires the environment to be new (422 otherwise, meaning
"use rotate"); POST rotate works whether or not one exists, deactivating the prior active version for
that environment.
Method
Path
Permission
Description
GET
/tenants/:id/secrets
secrets.read
List secrets (metadata + active environments only, no values).
POST
/tenants/:id/secrets
secrets.create
Create a secret. Body: { name, description?, value?, environment? }. Name must start with a letter (alphanumeric, _, -, . only).
GET
/tenants/:id/secrets/:name
secrets.read
Get secret metadata plus its full version history per environment (still no plaintext).
PATCH
/tenants/:id/secrets/:name
secrets.write
Update description.
DELETE
/tenants/:id/secrets/:name
secrets.delete
Delete the secret and all its versions (cascade). Returns 204.
PUT
/tenants/:id/secrets/:name/values
secrets.write
Set the first value for an environment. Body: { value, environment? }. 422 if one already exists.
POST
/tenants/:id/secrets/:name/rotate
secrets.rotate
Write a new version for an environment, deactivating the prior active one. Body: { value, environment? }.
PATCH
/tenants/:id/secrets/:name/escrow
secrets.escrow
Opt the secret in or out of the machine lease endpoint. Body: { enabled }.
POST
/tenants/:id/secrets/lease
JWT scope secrets:read (not a session permission)
Machine-only: decrypt and return plaintext for one or more escrowed secrets, scoped to the JWT's env claim. Body: { name } or { names: string[] }.