Skip to content

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.

The Cloud Portal API uses two authentication methods:

MethodHeaderUsed for
Admin API KeyX-API-Key: <key>Administrative operations (create org, register outpost, issue tokens)
Org Bearer JWTAuthorization: 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.


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/orgs
X-API-Key: <admin-key>
Content-Type: application/json

Request body:

{
"name": "Acme Corp",
"slug": "acme-corp",
"plan_tier": "enterprise",
"contact_email": "admin@acme.com",
"billing_email": "billing@acme.com",
"tenant_id": "optional-uuid"
}
FieldRequiredDescription
nameYesDisplay name for the organization
slugYesURL-safe identifier, must be globally unique
plan_tierYesSubscription tier (starter, business, enterprise)
contact_emailNoOwner email — auto-added as owner role admin
billing_emailNoBilling contact address
tenant_idNoOptional 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:

StatusCondition
401Invalid or missing admin key
409Slug already taken

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:

ValueMeaning
"active"Latest provisioning job completed
"provisioning"Job queued or running
"failed"Latest job failed
"unknown"No provisioning job recorded

Error responses:

StatusCondition
403JWT org_id does not match URL org_id
404Org not found or cancelled

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.


Org members can update their own organization’s display name without an admin key.

PATCH /v1/orgs/{org_id}/settings
Authorization: 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.


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:

StatusCondition
404Org not found
409Org already cancelled

Issue a new RS256 Bearer JWT for an org. Used to bootstrap auth for newly created orgs.

POST /v1/orgs/{org_id}/tokens
X-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"
}

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/revoke
X-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 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:

StatusCondition
409JTI already revoked

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.

GET /v1/orgs/{org_id}/members
Authorization: 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"
}
]

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}/members
Authorization: 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:

StatusCondition
409Email already a member of this org
PUT /v1/orgs/{org_id}/members/{user_id}
Authorization: Bearer <org-jwt>
Content-Type: application/json
{
"role": "admin"
}

Response 200 OK — updated OrgMemberResponse.

DELETE /v1/orgs/{org_id}/members/{user_id}
Authorization: Bearer <org-jwt>

Response 204 No Content


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%2F

Redirects to Platform login URL with state and redirect_uri parameters.

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.

When a user belongs to multiple organizations, this endpoint finalizes login for the selected org.

POST /v1/auth/sso/select-org
Authorization: Bearer <partial-sso-token>
Content-Type: application/json
{
"org_id": "org-uuid"
}

Response — issues a full Org Bearer JWT for the selected organization.

Revokes the SSO session JWT (JTI blacklisting).

POST /v1/auth/sso/logout
Authorization: Bearer <org-jwt>

Response 200 OK

GET /v1/auth/sso/session
Authorization: Bearer <org-jwt>

Returns current session details including org ID, plan, and JWT expiry.

Refresh an SSO session JWT before expiry.

POST /v1/auth/sso/refresh
Authorization: Bearer <org-jwt>

Response 200 OK — new JWT with refreshed expiry.


All outpost management endpoints require the admin API key. The Cloud Portal is the authority for outpost registration and certificate lifecycle.

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}/outposts
X-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_URL is configured; placeholder PEM in dev mode.
GET /v1/orgs/{org_id}/outposts
X-API-Key: <admin-key>

Returns all outposts for the org, including deregistered ones, ordered by created_at descending.

Response 200 OK — array of OutpostResponse.

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:

StatusCondition
404Outpost not found
409Outpost already deregistered

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}/renew
X-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 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-bundle
X-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"
}

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

POST /v1/orgs/{org_id}/outposts/{outpost_id}/heartbeat
X-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"
}

All outposts can POST to a single global endpoint with outpost_id in the body.

POST /v1/outpost/heartbeat
X-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.


List all outposts across all organizations for the admin monitoring dashboard.

GET /v1/admin/outposts
X-API-Key: <admin-key>

Returns outposts ordered by last_heartbeat_at DESC NULLS LAST (most recently active first).

Paginated heartbeat history for a specific outpost.

GET /v1/admin/outposts/{outpost_id}/heartbeats?limit=50&offset=0
X-API-Key: <admin-key>

Query parameters:

ParameterDefaultMaxDescription
limit50200Records per page
offset0Records 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
}

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-serve
Content-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.


All Cloud Portal API errors return a consistent envelope:

{
"detail": "Human-readable error description"
}

Common status codes:

StatusMeaning
400Invalid request body or parameters
401Missing or invalid authentication credential
403Authenticated but not authorized (e.g., org_id mismatch)
404Resource not found
409Conflict (e.g., slug taken, already cancelled, JTI already revoked)
500Internal server error