OID (Orbus Identity) is the multi-tenant OIDC-compliant identity service for ODS Platform.
Base URL:
https://oid.staging.orbusdigital.comGenerated: 2026-03-30
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
/.well-known/openid-configurationAuth: 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 ./.well-known/jwks.jsonAuth: 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 ./api/oidc/userinfoAuth: 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 ./api/auth/loginAuth: 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 ./api/auth/refreshAuth: 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 ./api/auth/logoutAuth: 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 ./api/oauth/authorizeAuth: 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"/api/oauth/tokenAuth: 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 ./api/signupAuth: 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 ./api/tenantsAuth: 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 ./api/tenantsAuth: 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 ./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 ./api/meAuth: 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 ./api/usersAuth: 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 ./api/usersAuth: 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 ./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 ./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 ./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 ./api/users/{id}/rolesAuth: 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 ./api/rolesAuth: 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 ./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 ./api/clientsAuth: 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 ./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 ./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 ./healthAuth: Public
Health check endpoint.
Response: 200 OK
Example:
curl -s https://oid.staging.orbusdigital.com/healthAll error responses follow one of two formats depending on the endpoint domain:
/api/auth/*, /api/oauth/*){
"error": "error_code",
"error_description": "Human-readable message"
}/api/tenants/*, /api/users/*,
/api/roles/*, /api/clients/*){
"error": "error_code",
"message": "Human-readable message"
}| 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) |
The following endpoints are rate-limited per IP using
actix-governor:
POST /api/auth/loginPOST /api/auth/refreshPOST /api/oauth/tokenPOST /api/signup (additional DB-backed rate limiting
per IP)When rate-limited, the response is
429 Too Many Requests. The /api/signup
endpoint additionally returns a Retry-After header.
All authenticated endpoints enforce tenant isolation:
tenant_id is extracted from the JWT
tenant_id claim403 ForbiddenGET /api/tenants/{id} endpoint only allows viewing
the caller’s own tenantOID 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.