OID API Reference

OID API Reference

OID (Orbus Identity) is the multi-tenant OIDC-compliant identity service for ODS Platform.

Base URL: https://oid.staging.orbusdigital.com

Generated: 2026-03-30


Table of Contents


Authentication

Most endpoints require a Bearer JWT in the Authorization header:

Authorization: Bearer <access_token>

The JWT is issued by OID itself and contains the following claims:

Claim Type Description
sub UUID User ID
iss string Issuer URL
aud string Audience (ods-platform)
exp integer Expiration (Unix timestamp)
iat integer Issued at (Unix timestamp)
tenant_id UUID Tenant the user belongs to
email string User email
name string Full name
roles string[] Role names
permissions string[] Permission strings

Tokens are signed with RS256. Public keys are available at /.well-known/jwks.json.

Auth levels used in this document: - Public – no authentication required - Bearer – requires valid JWT access token - M2M – machine-to-machine via client_credentials grant


OpenID Connect / Discovery

GET /.well-known/openid-configuration

Auth: Public

Returns the OIDC Discovery document (RFC 8414).

Response: 200 OK

{
  "issuer": "https://oid.staging.orbusdigital.com",
  "authorization_endpoint": "https://oid.staging.orbusdigital.com/api/oauth/authorize",
  "token_endpoint": "https://oid.staging.orbusdigital.com/api/oauth/token",
  "userinfo_endpoint": "https://oid.staging.orbusdigital.com/api/oidc/userinfo",
  "jwks_uri": "https://oid.staging.orbusdigital.com/.well-known/jwks.json",
  "login_endpoint": "https://oid.staging.orbusdigital.com/api/auth/login",
  "response_types_supported": ["code"],
  "grant_types_supported": ["authorization_code", "client_credentials", "refresh_token"],
  "subject_types_supported": ["public"],
  "id_token_signing_alg_values_supported": ["RS256"],
  "token_endpoint_auth_methods_supported": ["client_secret_post"],
  "scopes_supported": ["openid", "profile", "email"],
  "code_challenge_methods_supported": ["S256"],
  "claims_supported": ["sub", "iss", "aud", "exp", "iat", "tenant_id", "email", "name", "roles", "permissions"]
}

Example:

curl -s https://oid.staging.orbusdigital.com/.well-known/openid-configuration | jq .

GET /.well-known/jwks.json

Auth: Public

Returns the JSON Web Key Set (JWKS) containing the RSA public key used to verify JWT signatures.

Response: 200 OK

{
  "keys": [
    {
      "kty": "RSA",
      "use": "sig",
      "alg": "RS256",
      "kid": "<key-id>",
      "n": "<modulus-base64url>",
      "e": "AQAB"
    }
  ]
}

Example:

curl -s https://oid.staging.orbusdigital.com/.well-known/jwks.json | jq .

GET /api/oidc/userinfo

Auth: Bearer

Returns claims about the authenticated user from the JWT. Conforms to OIDC Core 5.3.

Response: 200 OK

{
  "sub": "550e8400-e29b-41d4-a716-446655440000",
  "email": "alice@example.com",
  "name": "Alice Doe",
  "tenant_id": "660e8400-e29b-41d4-a716-446655440000",
  "roles": ["admin"]
}

Error Responses:

Status Condition
401 Unauthorized Missing or invalid Bearer token

Example:

curl -s -H "Authorization: Bearer $TOKEN" \
  https://oid.staging.orbusdigital.com/api/oidc/userinfo | jq .

Auth

POST /api/auth/login

Auth: Public (rate-limited)

Authenticate a user with email and password. Returns a JWT access token and refresh token.

Request Body: application/json

Field Type Required Description
email string yes User email address
password string yes User password
tenant_id UUID yes Tenant to authenticate against

Response: 200 OK

{
  "access_token": "eyJhbGciOiJSUzI1NiIs...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "opaque-refresh-token-string",
  "user_id": "550e8400-e29b-41d4-a716-446655440000",
  "tenant_id": "660e8400-e29b-41d4-a716-446655440000"
}

Error Responses:

Status Error Code Condition
400 Bad Request invalid_request Validation error (missing fields, bad format)
401 Unauthorized invalid_credentials Wrong email or password (does not leak whether email exists)
429 Too Many Requests Rate limit exceeded
500 Internal Server Error server_error Unexpected server error

Example:

curl -s -X POST https://oid.staging.orbusdigital.com/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{
    "email": "alice@example.com",
    "password": "SecurePass1!",
    "tenant_id": "660e8400-e29b-41d4-a716-446655440000"
  }' | jq .

POST /api/auth/refresh

Auth: Public (rate-limited)

Exchange a refresh token for a new access token.

Request Body: application/json

Field Type Required Description
refresh_token string yes Previously issued refresh token

Response: 200 OK

{
  "access_token": "eyJhbGciOiJSUzI1NiIs...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "new-opaque-refresh-token"
}

Error Responses:

Status Error Code Condition
400 Bad Request invalid_request Missing or malformed refresh token
401 Unauthorized invalid_grant Refresh token expired, revoked, or not found
429 Too Many Requests Rate limit exceeded
500 Internal Server Error server_error Unexpected server error

Example:

curl -s -X POST https://oid.staging.orbusdigital.com/api/auth/refresh \
  -H "Content-Type: application/json" \
  -d '{
    "refresh_token": "opaque-refresh-token-string"
  }' | jq .

POST /api/auth/logout

Auth: Public

Revoke a refresh token. Idempotent – returns 200 OK even if the token was already revoked or does not exist (per OAuth2 spec).

Request Body: application/json

Field Type Required Description
refresh_token string yes Refresh token to revoke

Response: 200 OK

{
  "status": "ok",
  "message": "Refresh token revoked"
}

Error Responses:

Status Error Code Condition
500 Internal Server Error server_error Unexpected server error

Example:

curl -s -X POST https://oid.staging.orbusdigital.com/api/auth/logout \
  -H "Content-Type: application/json" \
  -d '{
    "refresh_token": "opaque-refresh-token-string"
  }' | jq .

OAuth 2.0

GET /api/oauth/authorize

Auth: Bearer

OAuth2 authorization endpoint. Creates an authorization code and redirects to the client’s redirect_uri. Supports PKCE (S256).

Query Parameters:

Parameter Type Required Description
response_type string yes Must be code
client_id string yes Registered OAuth client ID
redirect_uri string yes Must match a registered redirect URI
scope string no Space-separated scopes (default: openid)
state string no Opaque value for CSRF protection
code_challenge string no PKCE code challenge (recommended)
code_challenge_method string no Must be S256 (default if code_challenge provided)

Response: 302 Found

Redirects to {redirect_uri}?code={authorization_code}&state={state}.

Error Responses:

If the client_id is unknown or redirect_uri is invalid, returns a direct error:

Status Error Code Condition
400 Bad Request invalid_client Unknown client ID

For other errors, redirects back to redirect_uri with error parameters:

{redirect_uri}?error={error_code}&error_description={message}&state={state}
Error Code Condition
invalid_request Validation error (bad response_type, missing fields)
unauthorized_client Client not authorized for this grant
server_error Unexpected server error

Example:

# Browser redirect flow -- open in browser while authenticated
curl -s -v -H "Authorization: Bearer $TOKEN" \
  "https://oid.staging.orbusdigital.com/api/oauth/authorize?\
response_type=code&\
client_id=oid_abc123&\
redirect_uri=https://app.example.com/callback&\
scope=openid%20profile&\
state=xyz123&\
code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&\
code_challenge_method=S256"

POST /api/oauth/token

Auth: Public (rate-limited)

OAuth2 token endpoint. Exchanges credentials for tokens.

Content-Type: application/x-www-form-urlencoded

Supported Grant Types:

authorization_code

Field Type Required Description
grant_type string yes authorization_code
code string yes Authorization code from /authorize
redirect_uri string yes Must match the original authorize request
code_verifier string conditional Required if PKCE was used in /authorize
client_id string no Client identifier
client_secret string no Client secret

client_credentials (M2M)

Field Type Required Description
grant_type string yes client_credentials
client_id string yes OAuth client ID
client_secret string yes OAuth client secret
scope string no Requested scopes

refresh_token

Field Type Required Description
grant_type string yes refresh_token
refresh_token string yes Previously issued refresh token

Response: 200 OK

{
  "access_token": "eyJhbGciOiJSUzI1NiIs...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "new-opaque-refresh-token"
}

Note: refresh_token may be null for client_credentials grants.

Error Responses:

Status Error Code Condition
400 Bad Request invalid_request Missing grant_type or validation error
400 Bad Request invalid_grant Authorization code not found, expired, or already used
401 Unauthorized invalid_client Invalid client credentials
429 Too Many Requests Rate limit exceeded
500 Internal Server Error server_error Unexpected server error

Examples:

# Authorization code exchange
curl -s -X POST https://oid.staging.orbusdigital.com/api/oauth/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=authorization_code&code=AUTH_CODE&redirect_uri=https://app.example.com/callback&code_verifier=VERIFIER" | jq .

# Client credentials (M2M)
curl -s -X POST https://oid.staging.orbusdigital.com/api/oauth/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials&client_id=oid_abc123&client_secret=SECRET&scope=doc:read" | jq .

# Refresh token
curl -s -X POST https://oid.staging.orbusdigital.com/api/oauth/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=refresh_token&refresh_token=REFRESH_TOKEN" | jq .

Signup

POST /api/signup

Auth: Public (rate-limited by IP)

Self-service tenant registration. Creates a new tenant and admin user in a single transaction. Returns a JWT access token so the user is immediately authenticated.

Request Body: application/json

Field Type Required Description
email string yes Admin user email (must be globally unique)
password string yes Password (validation enforced)
first_name string yes Admin user first name (non-empty)
last_name string yes Admin user last name
organization_name string yes Tenant name (1-255 chars, used to generate slug)

Response: 201 Created

{
  "token": "eyJhbGciOiJSUzI1NiIs...",
  "refresh_token": "opaque-refresh-token-string",
  "user": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "tenant_id": "660e8400-e29b-41d4-a716-446655440000",
    "email": "alice@example.com",
    "first_name": "Alice",
    "last_name": "Doe",
    "roles": ["admin"]
  },
  "tenant": {
    "id": "660e8400-e29b-41d4-a716-446655440000",
    "name": "Acme Corp",
    "slug": "acme-corp",
    "status": "active"
  }
}

Error Responses:

Status Error Code Condition
400 Bad Request validation_error Invalid email, weak password, empty fields
409 Conflict conflict Email already registered
429 Too Many Requests rate_limited Too many signup attempts from this IP. Includes Retry-After header.
500 Internal Server Error server_error Unexpected server error

Rate limiting response:

{
  "error": "rate_limited",
  "error_description": "Too many signup attempts. Please try again later.",
  "retry_after": 3600
}

Example:

curl -s -X POST https://oid.staging.orbusdigital.com/api/signup \
  -H "Content-Type: application/json" \
  -d '{
    "email": "alice@example.com",
    "password": "SecurePass1!",
    "first_name": "Alice",
    "last_name": "Doe",
    "organization_name": "Acme Corp"
  }' | jq .

Tenants

POST /api/tenants

Auth: Bearer

Create a new tenant.

Request Body: application/json

Field Type Required Default Description
name string yes Tenant display name
slug string yes URL-safe slug (unique)
plan string no "free" Subscription plan

Response: 201 Created

{
  "id": "660e8400-e29b-41d4-a716-446655440000",
  "name": "Acme Corp",
  "slug": "acme-corp",
  "plan": "free",
  "status": "active",
  "created_at": "2026-03-30T12:00:00+00:00",
  "updated_at": "2026-03-30T12:00:00+00:00"
}

Error Responses:

Status Error Code Condition
400 Bad Request validation_error Invalid name or slug
401 Unauthorized Missing or invalid token
409 Conflict conflict Slug already exists
500 Internal Server Error internal_error Unexpected server error

Example:

curl -s -X POST https://oid.staging.orbusdigital.com/api/tenants \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Acme Corp",
    "slug": "acme-corp",
    "plan": "pro"
  }' | jq .

GET /api/tenants

Auth: Bearer

List all tenants. Admin-only.

Response: 200 OK

[
  {
    "id": "660e8400-e29b-41d4-a716-446655440000",
    "name": "Acme Corp",
    "slug": "acme-corp",
    "plan": "free",
    "status": "active",
    "created_at": "2026-03-30T12:00:00+00:00",
    "updated_at": "2026-03-30T12:00:00+00:00"
  }
]

Error Responses:

Status Condition
401 Unauthorized Missing or invalid token
500 Internal Server Error Unexpected server error

Example:

curl -s -H "Authorization: Bearer $TOKEN" \
  https://oid.staging.orbusdigital.com/api/tenants | jq .

GET /api/tenants/{id}

Auth: Bearer

Get a tenant by UUID. Restricted to the caller’s own tenant.

Path Parameters:

Parameter Type Description
id UUID Tenant ID

Response: 200 OK

{
  "id": "660e8400-e29b-41d4-a716-446655440000",
  "name": "Acme Corp",
  "slug": "acme-corp",
  "plan": "free",
  "status": "active",
  "created_at": "2026-03-30T12:00:00+00:00",
  "updated_at": "2026-03-30T12:00:00+00:00"
}

Error Responses:

Status Error Code Condition
401 Unauthorized Missing or invalid token
403 Forbidden forbidden Requesting a tenant the caller does not belong to
404 Not Found not_found Tenant does not exist
500 Internal Server Error internal_error Unexpected server error

Example:

curl -s -H "Authorization: Bearer $TOKEN" \
  https://oid.staging.orbusdigital.com/api/tenants/660e8400-e29b-41d4-a716-446655440000 | jq .

Users

GET /api/me

Auth: Bearer

Get the current authenticated user’s profile (fetched from the database, not just JWT claims).

Response: 200 OK

{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "tenant_id": "660e8400-e29b-41d4-a716-446655440000",
  "email": "alice@example.com",
  "first_name": "Alice",
  "last_name": "Doe",
  "name": "Alice Doe",
  "status": "active",
  "active": true,
  "email_verified": false,
  "roles": ["admin"],
  "created_at": "2026-03-30T12:00:00+00:00",
  "updated_at": "2026-03-30T12:00:00+00:00"
}

Error Responses:

Status Condition
401 Unauthorized Missing or invalid token
404 Not Found User not found in database

Example:

curl -s -H "Authorization: Bearer $TOKEN" \
  https://oid.staging.orbusdigital.com/api/me | jq .

POST /api/users

Auth: Bearer

Create a new user within the caller’s tenant. The tenant_id is derived from the JWT – not from the request body.

Request Body: application/json

Field Type Required Default Description
email string yes User email (unique within tenant)
first_name string yes First name
last_name string no "" Last name
password string yes Password (validation enforced)

Response: 201 Created

{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "tenant_id": "660e8400-e29b-41d4-a716-446655440000",
  "email": "bob@example.com",
  "first_name": "Bob",
  "last_name": "Smith",
  "name": "Bob Smith",
  "status": "active",
  "active": true,
  "email_verified": false,
  "roles": [],
  "created_at": "2026-03-30T12:00:00+00:00",
  "updated_at": "2026-03-30T12:00:00+00:00"
}

Error Responses:

Status Error Code Condition
400 Bad Request validation_error Invalid email, weak password
401 Unauthorized Missing or invalid token
409 Conflict conflict Email already exists in the tenant
500 Internal Server Error internal_error Unexpected server error

Example:

curl -s -X POST https://oid.staging.orbusdigital.com/api/users \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "bob@example.com",
    "first_name": "Bob",
    "last_name": "Smith",
    "password": "SecurePass1!"
  }' | jq .

GET /api/users

Auth: Bearer

List all users in the caller’s tenant.

Response: 200 OK

[
  {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "tenant_id": "660e8400-e29b-41d4-a716-446655440000",
    "email": "alice@example.com",
    "first_name": "Alice",
    "last_name": "Doe",
    "name": "Alice Doe",
    "status": "active",
    "active": true,
    "email_verified": false,
    "roles": ["admin"],
    "created_at": "2026-03-30T12:00:00+00:00",
    "updated_at": "2026-03-30T12:00:00+00:00"
  }
]

Error Responses:

Status Condition
401 Unauthorized Missing or invalid token
500 Internal Server Error Unexpected server error

Example:

curl -s -H "Authorization: Bearer $TOKEN" \
  https://oid.staging.orbusdigital.com/api/users | jq .

GET /api/users/{id}

Auth: Bearer

Get a user by UUID. Restricted to the caller’s own tenant.

Path Parameters:

Parameter Type Description
id UUID User ID

Response: 200 OK

Same schema as POST /api/users response.

Error Responses:

Status Error Code Condition
401 Unauthorized Missing or invalid token
403 Forbidden forbidden User belongs to a different tenant
404 Not Found not_found User does not exist

Example:

curl -s -H "Authorization: Bearer $TOKEN" \
  https://oid.staging.orbusdigital.com/api/users/550e8400-e29b-41d4-a716-446655440000 | jq .

PUT /api/users/{id}

Auth: Bearer

Update a user’s profile fields. Restricted to the caller’s own tenant.

Path Parameters:

Parameter Type Description
id UUID User ID

Request Body: application/json

Field Type Required Description
first_name string no New first name
last_name string no New last name
status string no "active" or "inactive"

Response: 200 OK

Same schema as POST /api/users response.

Error Responses:

Status Error Code Condition
400 Bad Request validation_error Invalid field values
401 Unauthorized Missing or invalid token
403 Forbidden forbidden User belongs to a different tenant
404 Not Found not_found User does not exist

Example:

curl -s -X PUT https://oid.staging.orbusdigital.com/api/users/550e8400-e29b-41d4-a716-446655440000 \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "first_name": "Alice",
    "last_name": "Updated",
    "status": "active"
  }' | jq .

DELETE /api/users/{id}

Auth: Bearer

Deactivate a user (soft delete). Restricted to the caller’s own tenant.

Path Parameters:

Parameter Type Description
id UUID User ID

Response: 200 OK

Returns the deactivated user object (with status: "inactive" and active: false).

Error Responses:

Status Error Code Condition
401 Unauthorized Missing or invalid token
403 Forbidden forbidden User belongs to a different tenant
404 Not Found not_found User does not exist

Example:

curl -s -X DELETE -H "Authorization: Bearer $TOKEN" \
  https://oid.staging.orbusdigital.com/api/users/550e8400-e29b-41d4-a716-446655440000 | jq .

PUT /api/users/{id}/roles

Auth: Bearer

Assign roles to a user. Replaces the user’s current role set. Restricted to the caller’s own tenant.

Path Parameters:

Parameter Type Description
id UUID User ID

Request Body: application/json

Field Type Required Description
role_ids UUID[] yes Array of role UUIDs to assign

Response: 200 OK

Returns the updated user object with new roles.

Error Responses:

Status Error Code Condition
400 Bad Request validation_error Invalid role IDs
401 Unauthorized Missing or invalid token
403 Forbidden forbidden User or role belongs to a different tenant
404 Not Found not_found User or role does not exist

Example:

curl -s -X PUT https://oid.staging.orbusdigital.com/api/users/550e8400-e29b-41d4-a716-446655440000/roles \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "role_ids": ["770e8400-e29b-41d4-a716-446655440000"]
  }' | jq .

Roles

POST /api/roles

Auth: Bearer

Create a new role within the caller’s tenant. The tenant_id is derived from the JWT.

Request Body: application/json

Field Type Required Default Description
name string yes Role name (unique within tenant)
description string no "" Human-readable description
permissions string[] no [] List of permission strings

Response: 201 Created

{
  "id": "770e8400-e29b-41d4-a716-446655440000",
  "tenant_id": "660e8400-e29b-41d4-a716-446655440000",
  "name": "editor",
  "description": "Can edit documents",
  "permissions": ["doc:read", "doc:write"],
  "created_at": "2026-03-30T12:00:00+00:00",
  "updated_at": "2026-03-30T12:00:00+00:00"
}

Error Responses:

Status Error Code Condition
400 Bad Request validation_error Invalid name
401 Unauthorized Missing or invalid token
409 Conflict conflict Role name already exists in the tenant
500 Internal Server Error internal_error Unexpected server error

Example:

curl -s -X POST https://oid.staging.orbusdigital.com/api/roles \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "editor",
    "description": "Can edit documents",
    "permissions": ["doc:read", "doc:write"]
  }' | jq .

GET /api/roles?tenant_id={uuid}

Auth: Bearer

List all roles for a tenant. The caller must belong to the requested tenant.

Query Parameters:

Parameter Type Required Description
tenant_id UUID yes Tenant to list roles for

Response: 200 OK

[
  {
    "id": "770e8400-e29b-41d4-a716-446655440000",
    "tenant_id": "660e8400-e29b-41d4-a716-446655440000",
    "name": "admin",
    "description": "Tenant administrator with full access",
    "permissions": ["tenant:manage", "users:manage", "roles:manage", "clients:manage"],
    "created_at": "2026-03-30T12:00:00+00:00",
    "updated_at": "2026-03-30T12:00:00+00:00"
  }
]

Error Responses:

Status Error Code Condition
401 Unauthorized Missing or invalid token
403 Forbidden forbidden Requesting roles for a different tenant
500 Internal Server Error internal_error Unexpected server error

Example:

curl -s -H "Authorization: Bearer $TOKEN" \
  "https://oid.staging.orbusdigital.com/api/roles?tenant_id=660e8400-e29b-41d4-a716-446655440000" | jq .

Clients

POST /api/clients

Auth: Bearer

Create a new OAuth2 client within the caller’s tenant. The tenant_id is derived from the JWT. The client_secret is shown only once in the response – store it securely.

Request Body: application/json

Field Type Required Default Description
name string yes Client display name
redirect_uris string[] no [] Allowed redirect URIs
scopes string[] no [] Allowed scopes
grant_types string[] no [] Allowed grant types

Response: 201 Created

{
  "id": "880e8400-e29b-41d4-a716-446655440000",
  "tenant_id": "660e8400-e29b-41d4-a716-446655440000",
  "client_id": "oid_abc123def456",
  "client_secret": "secret_SHOWN_ONLY_ONCE",
  "name": "My SPA",
  "redirect_uris": ["https://app.example.com/callback"],
  "scopes": ["openid", "profile"],
  "status": "active",
  "created_at": "2026-03-30T12:00:00+00:00"
}

Error Responses:

Status Error Code Condition
400 Bad Request validation_error Invalid fields
401 Unauthorized Missing or invalid token
403 Forbidden forbidden Attempting cross-tenant operation
409 Conflict conflict Client name already exists
500 Internal Server Error internal_error Unexpected server error

Example:

curl -s -X POST https://oid.staging.orbusdigital.com/api/clients \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "My SPA",
    "redirect_uris": ["https://app.example.com/callback"],
    "scopes": ["openid", "profile"],
    "grant_types": ["authorization_code"]
  }' | jq .

GET /api/clients?tenant_id={uuid}

Auth: Bearer

List OAuth2 clients for a tenant. The caller must belong to the requested tenant. Does not return client secrets.

Query Parameters:

Parameter Type Required Description
tenant_id UUID yes Tenant to list clients for

Response: 200 OK

[
  {
    "id": "880e8400-e29b-41d4-a716-446655440000",
    "tenant_id": "660e8400-e29b-41d4-a716-446655440000",
    "client_id": "oid_abc123def456",
    "name": "My SPA",
    "redirect_uris": ["https://app.example.com/callback"],
    "scopes": ["openid", "profile"],
    "status": "active",
    "created_at": "2026-03-30T12:00:00+00:00"
  }
]

Error Responses:

Status Error Code Condition
401 Unauthorized Missing or invalid token
403 Forbidden forbidden Requesting clients for a different tenant
500 Internal Server Error internal_error Unexpected server error

Example:

curl -s -H "Authorization: Bearer $TOKEN" \
  "https://oid.staging.orbusdigital.com/api/clients?tenant_id=660e8400-e29b-41d4-a716-446655440000" | jq .

DELETE /api/clients/{id}

Auth: Bearer

Deactivate (revoke) an OAuth2 client. Restricted to the caller’s own tenant.

Path Parameters:

Parameter Type Description
id UUID Client internal UUID

Response: 200 OK

Returns the deactivated client object (with status: "inactive").

Error Responses:

Status Error Code Condition
401 Unauthorized Missing or invalid token
403 Forbidden forbidden Client belongs to a different tenant
404 Not Found not_found Client does not exist
500 Internal Server Error internal_error Unexpected server error

Example:

curl -s -X DELETE -H "Authorization: Bearer $TOKEN" \
  https://oid.staging.orbusdigital.com/api/clients/880e8400-e29b-41d4-a716-446655440000 | jq .

Health

GET /health

Auth: Public

Health check endpoint.

Response: 200 OK

Example:

curl -s https://oid.staging.orbusdigital.com/health

Common Error Format

All error responses follow one of two formats depending on the endpoint domain:

OAuth2 endpoints (/api/auth/*, /api/oauth/*)

{
  "error": "error_code",
  "error_description": "Human-readable message"
}

CRUD endpoints (/api/tenants/*, /api/users/*, /api/roles/*, /api/clients/*)

{
  "error": "error_code",
  "message": "Human-readable message"
}

Common error codes

Error Code HTTP Status Description
invalid_request 400 Malformed request, missing required fields
validation_error 400 Business validation failed
invalid_credentials 401 Wrong email or password
invalid_client 401 Invalid OAuth client credentials
invalid_grant 400/401 Authorization code or refresh token invalid
forbidden 403 Cross-tenant access denied
not_found 404 Resource does not exist
conflict 409 Duplicate resource (email, slug, name)
rate_limited 429 Too many requests from this IP
server_error 500 Unexpected internal error
internal_error 500 Unexpected internal error (CRUD endpoints)

Rate Limiting

The following endpoints are rate-limited per IP using actix-governor:

When rate-limited, the response is 429 Too Many Requests. The /api/signup endpoint additionally returns a Retry-After header.


Multi-Tenancy

All authenticated endpoints enforce tenant isolation:


Events (Redpanda / CloudEvents)

OID emits the following CloudEvents to Redpanda on state changes:

Event Type Trigger
oid.auth.login_succeeded Successful login
oid.auth.login_failed Failed login attempt
oid.auth.token_issued Token granted via OAuth token endpoint
oid.signup.completed New tenant + user created via signup
oid.signup.rate_limited Signup blocked by rate limiter

All events include tenant_id in the payload.