API Reference Batch 8 — Cloud Portal API
API Reference Batch 8 — Cloud Portal API
Section titled “API Reference Batch 8 — Cloud Portal API”This batch documents the Arbitex Cloud Portal API (arbitex-cloud), which is a separate service from the Platform API (/api/). The Cloud Portal API uses the base URL of the cloud service (e.g., https://cloud.arbitex.io) with a /v1/ prefix.
Authentication
Section titled “Authentication”The Cloud Portal API uses two authentication methods:
| Method | Header | Used for |
|---|---|---|
| Admin API Key | X-API-Key: <key> | Administrative operations (create org, register outpost, issue tokens) |
| Org Bearer JWT | Authorization: Bearer <jwt> | Org-scoped operations (read org details, manage members) |
Org Bearer JWTs are RS256 tokens with claims: sub=org_id, plan=<plan_tier>, iat, exp, jti.
Organization Management
Section titled “Organization Management”Create Organization
Section titled “Create Organization”Create a new organization. Automatically creates an initial API key, sets the owner email as the first admin, and queues an org_provision background job.
POST /v1/orgsX-API-Key: <admin-key>Content-Type: application/jsonRequest body:
{ "name": "Acme Corp", "slug": "acme-corp", "plan_tier": "enterprise", "contact_email": "admin@acme.com", "billing_email": "billing@acme.com", "tenant_id": "optional-uuid"}| Field | Required | Description |
|---|---|---|
name | Yes | Display name for the organization |
slug | Yes | URL-safe identifier, must be globally unique |
plan_tier | Yes | Subscription tier (starter, business, enterprise) |
contact_email | No | Owner email — auto-added as owner role admin |
billing_email | No | Billing contact address |
tenant_id | No | Optional UUID for multi-tenant isolation |
Response 201 Created:
{ "org": { "id": "org-uuid", "name": "Acme Corp", "slug": "acme-corp", "plan_tier": "enterprise", "status": "active", "provisioning_status": "provisioning", "contact_email": "admin@acme.com", "created_at": "2026-03-12T14:00:00Z" }, "api_key": "arb_base64url...", "provisioning_job_id": "job-uuid"}Note: api_key is returned once only and not stored in plaintext. Save it immediately.
Error responses:
| Status | Condition |
|---|---|
401 | Invalid or missing admin key |
409 | Slug already taken |
Get Organization
Section titled “Get Organization”Retrieve organization details. The JWT’s org_id must match the URL parameter (IDOR prevention).
GET /v1/orgs/{org_id}Authorization: Bearer <org-jwt>Response 200 OK:
{ "id": "org-uuid", "name": "Acme Corp", "slug": "acme-corp", "plan_tier": "enterprise", "status": "active", "provisioning_status": "active", "contact_email": "admin@acme.com", "billing_email": "billing@acme.com", "tenant_id": null, "created_at": "2026-03-12T14:00:00Z", "updated_at": "2026-03-12T14:00:00Z"}provisioning_status values:
| Value | Meaning |
|---|---|
"active" | Latest provisioning job completed |
"provisioning" | Job queued or running |
"failed" | Latest job failed |
"unknown" | No provisioning job recorded |
Error responses:
| Status | Condition |
|---|---|
403 | JWT org_id does not match URL org_id |
404 | Org not found or cancelled |
Update Organization
Section titled “Update Organization”Admin-only update. Supports partial updates. If plan_tier changes, an entitlement_update job is queued to cascade the new tier to the Platform service.
PATCH /v1/orgs/{org_id}X-API-Key: <admin-key>Content-Type: application/json
{ "plan_tier": "business", "name": "Acme Corp (renamed)"}Response 200 OK — updated OrgResponse.
Update Org Settings (Self-Service)
Section titled “Update Org Settings (Self-Service)”Org members can update their own organization’s display name without an admin key.
PATCH /v1/orgs/{org_id}/settingsAuthorization: Bearer <org-jwt>Content-Type: application/json
{ "name": "Acme Corp Display Name"}Only name is updatable via this endpoint. Administrative fields (plan_tier, status, billing_email, tenant_id) require the admin API key.
Response 200 OK — updated OrgResponse.
Delete (Cancel) Organization
Section titled “Delete (Cancel) Organization”Soft-delete an organization by setting its status to cancelled. Queues an org_deprovision background job.
DELETE /v1/orgs/{org_id}X-API-Key: <admin-key>Response 204 No Content
Error responses:
| Status | Condition |
|---|---|
404 | Org not found |
409 | Org already cancelled |
JWT Token Management
Section titled “JWT Token Management”Issue Org JWT
Section titled “Issue Org JWT”Issue a new RS256 Bearer JWT for an org. Used to bootstrap auth for newly created orgs.
POST /v1/orgs/{org_id}/tokensX-API-Key: <admin-key>Content-Type: application/json
{ "plan": "enterprise", "expires_in": 86400}Response 201 Created:
{ "token": "eyJ...", "expires_in": 86400, "token_type": "Bearer", "jti": "unique-jwt-id"}Revoke All Org JWTs
Section titled “Revoke All Org JWTs”Invalidate all existing JWTs for an org by setting tokens_revoked_before to the current timestamp. Any JWT with iat ≤ tokens_revoked_before is rejected. Use during security incidents.
POST /v1/orgs/{org_id}/tokens/revokeX-API-Key: <admin-key>Response 200 OK:
{ "org_id": "org-uuid", "tokens_revoked_before": "2026-03-12T14:22:00Z", "message": "All tokens issued before this timestamp are now invalid. Re-authenticate to obtain new tokens."}Redis cache for the org’s revocation state is invalidated immediately.
Revoke Single JWT by JTI
Section titled “Revoke Single JWT by JTI”Revoke a specific JWT by its jti claim. The JTI is inserted into the jwt_revocations table.
POST /v1/orgs/{org_id}/tokens/revoke/{jti}X-API-Key: <admin-key>Content-Type: application/json
{ "reason": "Token suspected compromised"}Response 200 OK:
{ "jti": "jwt-unique-id", "org_id": "org-uuid", "revoked_at": "2026-03-12T14:22:00Z", "reason": "Token suspected compromised", "message": "Token jwt-unique-id has been revoked. It will be rejected on next use."}Error responses:
| Status | Condition |
|---|---|
409 | JTI already revoked |
Member Management
Section titled “Member Management”Members are stored in the org_admins table. The portal maps owner role to admin for display — only admin and member are exposed via this API.
List Members
Section titled “List Members”GET /v1/orgs/{org_id}/membersAuthorization: Bearer <org-jwt>Response 200 OK — array of OrgMemberResponse:
[ { "id": "member-uuid", "org_id": "org-uuid", "email": "admin@acme.com", "role": "admin", "invited_at": null, "accepted_at": null, "created_at": "2026-03-12T14:00:00Z" }]Invite Member
Section titled “Invite Member”Creates an OrgAdmin record and returns a one-time invite token. The invite token is generated in-memory (inv_ + 24 random bytes, base64url-encoded) and not stored — it must be delivered to the invitee out-of-band.
POST /v1/orgs/{org_id}/membersAuthorization: Bearer <org-jwt>Content-Type: application/json
{ "email": "newuser@acme.com", "role": "member"}Response 201 Created:
{ "member": { "id": "member-uuid", "org_id": "org-uuid", "email": "newuser@acme.com", "role": "member", "created_at": "2026-03-12T14:22:00Z" }, "invite_token": "inv_base64url..."}Error responses:
| Status | Condition |
|---|---|
409 | Email already a member of this org |
Update Member Role
Section titled “Update Member Role”PUT /v1/orgs/{org_id}/members/{user_id}Authorization: Bearer <org-jwt>Content-Type: application/json
{ "role": "admin"}Response 200 OK — updated OrgMemberResponse.
Remove Member
Section titled “Remove Member”DELETE /v1/orgs/{org_id}/members/{user_id}Authorization: Bearer <org-jwt>Response 204 No Content
SSO Authentication
Section titled “SSO Authentication”Initiate SSO Login
Section titled “Initiate SSO Login”Redirects the user to the Platform’s identity provider login page. Stores a CSRF state parameter.
GET /v1/auth/sso/login?redirect_uri=https%3A%2F%2Fportal.example.com%2FRedirects to Platform login URL with state and redirect_uri parameters.
SSO Callback
Section titled “SSO Callback”Handles the redirect back from the Platform. Validates the state parameter, exchanges the authorization code for user identity, resolves org membership, and issues an Org Bearer JWT.
GET /v1/auth/sso/callback?code=<auth_code>&state=<csrf_state>On success, sets a session cookie and redirects to the portal dashboard. For users with multiple org memberships, redirects to the org-selector page.
Multi-Org Selector
Section titled “Multi-Org Selector”When a user belongs to multiple organizations, this endpoint finalizes login for the selected org.
POST /v1/auth/sso/select-orgAuthorization: Bearer <partial-sso-token>Content-Type: application/json
{ "org_id": "org-uuid"}Response — issues a full Org Bearer JWT for the selected organization.
SSO Logout
Section titled “SSO Logout”Revokes the SSO session JWT (JTI blacklisting).
POST /v1/auth/sso/logoutAuthorization: Bearer <org-jwt>Response 200 OK
Get Session Info
Section titled “Get Session Info”GET /v1/auth/sso/sessionAuthorization: Bearer <org-jwt>Returns current session details including org ID, plan, and JWT expiry.
SSO Token Refresh
Section titled “SSO Token Refresh”Refresh an SSO session JWT before expiry.
POST /v1/auth/sso/refreshAuthorization: Bearer <org-jwt>Response 200 OK — new JWT with refreshed expiry.
Outpost Management
Section titled “Outpost Management”All outpost management endpoints require the admin API key. The Cloud Portal is the authority for outpost registration and certificate lifecycle.
Register Outpost
Section titled “Register Outpost”Register a new Hybrid Outpost and issue its mTLS certificate. The certificate bundle (cert + key + CA chain) is returned once only — the Cloud Portal does not store the private key.
POST /v1/orgs/{org_id}/outpostsX-API-Key: <admin-key>Content-Type: application/json
{ "outpost_name": "us-east-production", "region": "us-east-1", "deployment_env": "kubernetes"}Response 201 Created:
{ "outpost": { "id": "outpost-uuid", "org_id": "org-uuid", "outpost_name": "us-east-production", "region": "us-east-1", "deployment_env": "kubernetes", "status": "active", "cert_serial": "abc123def456", "cert_issued_at": "2026-03-12T14:00:00Z", "cert_expires_at": "2026-06-10T14:00:00Z" }, "client_cert": "-----BEGIN CERTIFICATE-----\n...", "client_key": "-----BEGIN PRIVATE KEY-----\n...", "ca_bundle": "-----BEGIN CERTIFICATE-----\n..."}Certificate details:
- Validity: 90 days (
OUTPOST_CERT_VALIDITY_DAYS=90) - CN format:
outpost-{org_id}-{outpost_id}.arbitex.internal - Issued via step-ca when
STEP_CA_URLis configured; placeholder PEM in dev mode.
List Outposts
Section titled “List Outposts”GET /v1/orgs/{org_id}/outpostsX-API-Key: <admin-key>Returns all outposts for the org, including deregistered ones, ordered by created_at descending.
Response 200 OK — array of OutpostResponse.
Deregister Outpost
Section titled “Deregister Outpost”Revokes the outpost’s mTLS certificate (via step-ca) and marks the outpost as deregistered.
DELETE /v1/orgs/{org_id}/outposts/{outpost_id}X-API-Key: <admin-key>Response 204 No Content
Error responses:
| Status | Condition |
|---|---|
404 | Outpost not found |
409 | Outpost already deregistered |
Renew Outpost Certificate
Section titled “Renew Outpost Certificate”Issue a renewal certificate before the current one expires. The old certificate remains valid until its natural expiry (CRL grace period overlap). The new certificate has a fresh 90-day validity window.
POST /v1/orgs/{org_id}/outposts/{outpost_id}/renewX-API-Key: <admin-key>Response 200 OK:
{ "outpost": { ... }, "client_cert": "-----BEGIN CERTIFICATE-----\n...", "client_key": "-----BEGIN PRIVATE KEY-----\n...", "ca_bundle": "-----BEGIN CERTIFICATE-----\n..."}Download CA Bundle
Section titled “Download CA Bundle”Download the CA bundle PEM without re-issuing the certificate. Safe to re-download at any time (no private key involved).
GET /v1/orgs/{org_id}/outposts/{outpost_id}/cert-bundleX-API-Key: <admin-key>Response 200 OK:
{ "outpost_id": "outpost-uuid", "cert_bundle_pem": "-----BEGIN CERTIFICATE-----\n...", "cert_serial": "abc123def456", "cert_expires_at": "2026-06-10T14:00:00Z"}Outpost Heartbeat Endpoints
Section titled “Outpost Heartbeat Endpoints”Outposts send periodic heartbeats to report health and telemetry. Authentication accepts mTLS client certificates (injected as X-SSL-Client-Cert by Nginx in production) or admin key (for testing).
Heartbeat (Org-Scoped)
Section titled “Heartbeat (Org-Scoped)”POST /v1/orgs/{org_id}/outposts/{outpost_id}/heartbeatX-SSL-Client-Cert: <cert> (production mTLS)Content-Type: application/json
{ "version": "2.4.1", "status": "healthy", "uptime": 86400, "policy_version": "v42", "dlp_model_version": "deberta-v3-2026-03", "pending_audit_events": 0, "tier3_active": true, "last_sync_at": "2026-03-12T13:00:00Z", "metrics": { "cpu_pct": 12.5, "mem_mb": 512 }}Response 200 OK:
{ "outpost_id": "outpost-uuid", "received_at": "2026-03-12T14:22:00Z"}Heartbeat V2 (Global Endpoint)
Section titled “Heartbeat V2 (Global Endpoint)”All outposts can POST to a single global endpoint with outpost_id in the body.
POST /v1/outpost/heartbeatX-SSL-Client-Cert: <cert>Content-Type: application/json
{ "outpost_id": "outpost-uuid", "version": "2.4.1", "uptime_seconds": 86400, "last_policy_sync": "2026-03-12T13:00:00Z", "dlp_tiers_active": [1, 2, 3], "cert_expiry": "2026-06-10T14:00:00Z", "resource_usage": { "cpu_pct": 12.5, "mem_mb": 512 }}Response 200 OK:
{ "outpost_id": "outpost-uuid", "received_at": "2026-03-12T14:22:00Z", "status": "healthy"}The V2 endpoint supports additional fields: dlp_tiers_active, cert_expiry, resource_usage. Prefer V2 for new outpost deployments.
Admin Outpost Monitoring
Section titled “Admin Outpost Monitoring”List All Outposts (Cross-Org)
Section titled “List All Outposts (Cross-Org)”List all outposts across all organizations for the admin monitoring dashboard.
GET /v1/admin/outpostsX-API-Key: <admin-key>Returns outposts ordered by last_heartbeat_at DESC NULLS LAST (most recently active first).
Outpost Heartbeat History
Section titled “Outpost Heartbeat History”Paginated heartbeat history for a specific outpost.
GET /v1/admin/outposts/{outpost_id}/heartbeats?limit=50&offset=0X-API-Key: <admin-key>Query parameters:
| Parameter | Default | Max | Description |
|---|---|---|---|
limit | 50 | 200 | Records per page |
offset | 0 | — | Records to skip |
Response 200 OK:
{ "items": [ { "id": "hb-uuid", "outpost_id": "outpost-uuid", "received_at": "2026-03-12T14:22:00Z", "status": "healthy", "version": "2.4.1", "uptime_seconds": 86400, "policy_version": "v42", "last_sync_at": "2026-03-12T13:00:00Z", "metrics": { "cpu_pct": 12.5 } } ], "total": 1440, "limit": 50, "offset": 0}Self-Serve Organization Registration
Section titled “Self-Serve Organization Registration”Create a new organization and owner account in a single request without an admin key. Used for the public sign-up flow.
POST /v1/orgs/self-serveContent-Type: application/json
{ "org_name": "My Company", "owner_email": "me@mycompany.com", "plan_tier": "starter"}Response 201 Created — returns an Org Bearer JWT for immediate access, along with the new org details.
Error Responses
Section titled “Error Responses”All Cloud Portal API errors return a consistent envelope:
{ "detail": "Human-readable error description"}Common status codes:
| Status | Meaning |
|---|---|
400 | Invalid request body or parameters |
401 | Missing or invalid authentication credential |
403 | Authenticated but not authorized (e.g., org_id mismatch) |
404 | Resource not found |
409 | Conflict (e.g., slug taken, already cancelled, JTI already revoked) |
500 | Internal server error |