Service API Reference

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
FieldTypeRequiredDescription
tokenstringYes*User session token (opaque)
access_tokenstringYes*JWT access token (from orbit-oauth2)
tenant_idstringYesTarget tenant UUID
permissionstringNoSingle permission to check
permissionsstring[]NoBatch permission check
resourceobjectNoResource context for policy evaluation
includestring[]NoExtra fields to return: permissions, roles, flags
flagsstring[]NoFeature 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"
}
curl Example
curl -X POST https://orbit.example.com/check \
  -H "Authorization: Bearer ${API_KEY}" \
  -H "Content-Type: application/json" \
  -d '{
    "token": "user-session-token",
    "tenant_id": "tenant-uuid",
    "permission": "documents.edit",
    "resource": { "owner_id": "user-123" }
  }'

Authentication

MethodPathAuthDescription
POST/auth/signupNoneCreate root tenant + owner user. Returns { token, expires_at }.
POST/auth/loginNoneAuthenticate. With tenant_id: single-step. Without: pre-tenant session (next: "tenant_select").
POST/auth/logoutSessionDelete current session. Returns 204.
POST/auth/logout/allSessionDelete all sessions for the user. Returns 204.
GET/auth/meSessionCurrent user + tenant + effective permissions.
GET/auth/membershipsSessionList tenants the user belongs to. Returns TenantMembership[].
POST/auth/session/exchangePre-tenantSwap pre-tenant session for tenant-scoped session. Body: { tenant_slug }.
POST/auth/invite/acceptNoneAccept invite, set password, get session. Body: { token, password }.
POST/auth/password/resetNoneRequest password reset. Body: { email }, optional tenant_id.
POST/auth/password/reset/confirmNoneConfirm reset. Body: { token, password }. Returns 204.
Login Flow
# Step 1: Login (no tenant_id → pre-tenant session)
curl -X POST https://orbit.example.com/auth/login \
  -H "Content-Type: application/json" \
  -d '{ "email": "admin@example.com", "password": "secret" }'
# → { "token": "pre-token", "expires_at": "...", "next": "tenant_select" }

# Step 2: List memberships
curl https://orbit.example.com/auth/memberships \
  -H "Authorization: Bearer ${PRE_TOKEN}"
# → { "data": [{ "tenant_id": "...", "tenant_slug": "acme", "tenant_name": "Acme Corp", "roles": ["admin"] }] }

# Step 3: Exchange for tenant-scoped session
curl -X POST https://orbit.example.com/auth/session/exchange \
  -H "Authorization: Bearer ${PRE_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{ "tenant_slug": "acme" }'
# → { "token": "tenant-scoped-token", "expires_at": "...", "next": "complete" }

Tenants

All routes require a tenant-scoped session. Tenant isolation is absolute — you can only access your own tenant's data.

MethodPathPermissionDescription
GET/tenantstenants.read (system)List all root tenants. System tenant only.
POST/tenantstenants.createCreate child tenant. Body: { name }. Caller mirrored as owner.
GET/tenants/:idtenants.readGet own tenant.
PATCH/tenants/:idtenants.updateUpdate tenant. Body: { name?, plan?, metadata? }. Slug is immutable.
DELETE/tenants/:idtenants.deleteSoft-deactivate. Returns 204.
GET/tenants/:id/childrentenants.readList direct children. Paginated.

Users

MethodPathPermissionDescription
GET/tenants/:id/usersusers.readList users in tenant.
POST/tenants/:id/usersusers.createCreate user. Body: { email, name }, optional password.
POST/tenants/:id/users/inviteusers.createInvite by email. Body: { email, role_id }. Creates invite token.
GET/tenants/:id/users/:uidusers.readGet user.
PATCH/tenants/:id/users/:uidusers.updateUpdate user. Body: { name?, email?, metadata?, password? }.
DELETE/tenants/:id/users/:uidusers.deleteSoft-deactivate. Returns 204.

User Roles

MethodPathPermissionDescription
GET/tenants/:id/users/:uid/rolesusers.readList roles assigned to user.
POST/tenants/:id/users/:uid/roles/:roleIdroles.updateAssign role. Returns 204.
DELETE/tenants/:id/users/:uid/roles/:roleIdroles.updateRevoke role. Returns 204.

User Direct Permissions

MethodPathPermissionDescription
GET/tenants/:id/users/:uid/permissionsusers.readEffective permission names (role + direct, minus explicit deny).
POST/tenants/:id/users/:uid/permissionspermissions.attachGrant direct permission. Body: { permission_id, granted?, expires_at? }.
PATCH/tenants/:id/users/:uid/permissions/:permIdpermissions.attachUpdate granted/expires_at.
DELETE/tenants/:id/users/:uid/permissions/:permIdpermissions.detachRemove direct permission. Returns 204.

Roles

MethodPathPermissionDescription
GET/tenants/:id/rolesroles.readList all roles in tenant.
POST/tenants/:id/rolesroles.createCreate role. Body: { name, description? }.
GET/tenants/:id/roles/:roleIdroles.readGet role.
PATCH/tenants/:id/roles/:roleIdroles.updateUpdate. Body: { name?, description? }.
DELETE/tenants/:id/roles/:roleIdroles.deleteDelete (cascades). Returns 204.

Role Permissions

MethodPathPermissionDescription
GET/tenants/:id/roles/:roleId/permissionsroles.readList permissions assigned to role.
POST/tenants/:id/roles/:roleId/permissions/:permIdroles.updateAssign permission. Returns 204.
DELETE/tenants/:id/roles/:roleId/permissions/:permIdroles.updateRemove permission. Returns 204.

Permissions

Permissions are global (not tenant-scoped). Creating permissions requires the system tenant.

MethodPathPermissionDescription
GET/permissionspermissions.readList all. Optional query: module_id filter.
POST/permissionspermissions.create (system)Create permission. Body: { module_id, name, description? }.
GET/permissions/:idpermissions.readGet permission.

Modules

Modules group permissions (e.g. users, roles, oauth2).

MethodPathPermissionDescription
GET/modulesmodules.readList all modules.
POST/modulesmodules.create (system)Create module. Body: { name, slug, description? }.
GET/modules/:idmodules.readGet module.
PATCH/modules/:idmodules.update (system)Update. Slug is immutable.

Policies

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.

MethodPathPermissionDescription
GET/policiespolicies.readList all policies.
POST/policiespolicies.create (system)Create. Body: { name, type, parameters?, description? }.
GET/policies/:idpolicies.readGet policy.
PATCH/policies/:idpolicies.update (system)Update. Type is immutable.
DELETE/policies/:idpolicies.delete (system)Delete (cascades). Returns 204.

Policy Attachment

MethodPathPermissionDescription
GET/permissions/:id/policiespermissions.readList policies attached to a permission.
POST/permissions/:id/policies/:policyIdpolicies.attach (system)Attach policy to permission. Returns 204.
DELETE/permissions/:id/policies/:policyIdpolicies.detach (system)Detach. Returns 204.

Built-in Policy Types

TypeDescriptionParameters
ownershipChecks if the user owns the resource{ owner_field: "owner_id" }
department_matchChecks if user's department matches the resource{ field: "department_id" }
resource_stateChecks resource state against allowed values{ field: "status", allowed: ["draft"] }
business_hoursTime-of-day restriction{ start: "09:00", end: "17:00", tz: "America/Toronto" }

API Keys

API keys authenticate consuming applications against the introspect endpoint. The raw key is returned once at creation and stored as a SHA-256 hash.

MethodPathPermissionDescription
GET/tenants/:id/api-keysapi-keys.readList API keys (never exposes key hash).
POST/tenants/:id/api-keysapi-keys.createCreate. Body: { name, expires_at? }. Returns raw key once.
DELETE/tenants/:id/api-keys/:keyIdapi-keys.revokeSoft-revoke. Returns 204.

Audit Logs

Every mutation is recorded. Logs capture old/new values, the acting user, IP address, and timestamp.

MethodPathPermissionDescription
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)

MethodPathPermissionDescription
GET/feature-flagsfeature-flags.read (system)List all global flags.
POST/feature-flagsfeature-flags.create (system)Create. Body: { name, enabled?, description? }.
GET/feature-flags/:namefeature-flags.read (system)Get global flag.
PATCH/feature-flags/:namefeature-flags.update (system)Update global default.

Tenant Flags

MethodPathPermissionDescription
GET/tenants/:id/feature-flagsfeature-flags.readEffective flags (override + global merged).
GET/tenants/:id/feature-flags/:namefeature-flags.readSingle effective flag value.
PUT/tenants/:id/feature-flags/:namefeature-flags.updateSet 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.

MethodPathPermissionDescription
GET/tenants/:id/oauth2-clientsoauth2.readList clients.
POST/tenants/:id/oauth2-clientsoauth2.createCreate client. Secret returned once for confidential clients.
GET/tenants/:id/oauth2-clients/:clientIdoauth2.readGet client.
PATCH/tenants/:id/oauth2-clients/:clientIdoauth2.updateUpdate client.
DELETE/tenants/:id/oauth2-clients/:clientIdoauth2.deleteDelete (cascades). Returns 204.
POST/tenants/:id/oauth2-clients/:clientId/rotate-secretoauth2.updateRotate secret (confidential only).
POST/tenants/:id/oauth2-clients/:clientId/revokeoauth2.updateRevoke all tokens for client.
GET/tenants/:id/oauth2-clients/:clientId/grantsoauth2.readList grants (subject + scopes).
DELETE/tenants/:id/oauth2-clients/:clientId/grants/:subjectoauth2.updateRevoke specific grant. Returns 204.

Signing Keys (System Tenant)

MethodPathPermissionDescription
GET/tenants/system/oauth2-keysoauth2.readList signing keys.
POST/tenants/system/oauth2-keys/rotateoauth2.createGenerate new EC P-521 keypair.
POST/tenants/system/oauth2-keys/:kid/retireoauth2.updateRetire key (stays in JWKS for validation).