API Contracts
:::info Source
Sourced from services/tenant-service/API_CONTRACTS.md in the documentation repo.
:::
Blueprint doc 4 of 17. Companion: 05 API Design | APPLICATION_LOGIC | SECURITY_MODEL
1. Conventions
All endpoints follow the platform API design spec (doc 05):
- Base path:
/api/v1 - JSON only (
Content-Type: application/json; charset=utf-8) - Cursor-based pagination on collections
Idempotency-Keyrequired on all writesIf-Matchrequired on PATCH/PUT (optimistic concurrency)- Problem+JSON (RFC 9457) error responses
X-Tenant-Idheader must match JWTtidclaim
2. Tenant Endpoints
2.1 POST /api/v1/tenants — Provision Tenant
Auth: platform_admin or unauthenticated (self-service signup flow with rate limit)
Request:
{
"name": "Acme Corp",
"slug": "acme-corp",
"type": "org",
"homeRegion": "eu",
"plan": { "id": "plan_starter", "addons": [] },
"ownerEmail": "admin@acme.com",
"settings": {
"defaultLocale": "en-US",
"allowedLocales": ["en-US", "ar-SA"],
"mfaRequired": false,
"offlineEnabled": true,
"aiEnabled": true
}
}
Response (201):
{
"data": {
"id": "tnt_01HX7QKFG8...",
"name": "Acme Corp",
"slug": "acme-corp",
"type": "org",
"homeRegion": "eu",
"status": "trial",
"plan": { "id": "plan_starter", "addons": [] },
"settings": { "...": "..." },
"createdAt": "2026-04-15T10:00:00Z"
},
"meta": { "requestId": "req_01HX...", "apiVersion": "v1.0" }
}
Errors:
| Status | Code | Description |
|---|---|---|
| 409 | SLUG_TAKEN | Slug already in use |
| 422 | INVALID_REGION | Unknown home region |
| 422 | INVALID_PLAN | Plan ID not found |
| 429 | RATE_LIMITED | Too many provisioning requests |
2.2 GET /api/v1/tenants/{id} — Get Tenant
Auth: Any member of the tenant, or platform_admin
Response (200):
{
"data": {
"id": "tnt_01HX...",
"name": "Acme Corp",
"slug": "acme-corp",
"type": "org",
"homeRegion": "eu",
"status": "active",
"plan": { "id": "plan_pro", "addons": ["ai_tutor"] },
"settings": {
"defaultLocale": "en-US",
"allowedLocales": ["en-US", "ar-SA"],
"brandingTheme": { "primaryColor": "#1a56db" },
"mfaRequired": true,
"offlineEnabled": true,
"aiEnabled": true,
"aiTutorEnabled": true,
"maxOrgDepth": 10,
"sessionTimeout": "PT8H",
"passwordPolicy": {
"minLength": 10,
"requireUppercase": true,
"requireNumber": true,
"requireSymbol": false
}
},
"ssoProviders": [
{ "id": "sso_01HX...", "protocol": "saml", "name": "Acme IdP", "enabled": true }
],
"createdAt": "2026-01-15T10:00:00Z"
},
"meta": { "requestId": "req_01HX...", "apiVersion": "v1.0" }
}
Headers: ETag: "v3" for cache validation.
2.3 PATCH /api/v1/tenants/{id} — Update Tenant
Auth: org_owner
Headers: If-Match: "v3" required.
Request (partial):
{
"name": "Acme Corporation"
}
Response (200): Updated tenant DTO.
Errors: 412 Precondition Failed if version mismatch.
2.4 PATCH /api/v1/tenants/{id}/settings — Update Settings
Auth: org_owner, org_admin
Request:
{
"mfaRequired": true,
"aiTutorEnabled": false,
"passwordPolicy": { "minLength": 12 }
}
Response (200): Full settings object.
2.5 PATCH /api/v1/tenants/{id}/mfa-policy — Passkey policy for admins (US-5)
Auth: Authenticated member with x-user-id (BFF); intended for org_owner / org_admin operators.
Request:
{
"passkeyRequiredForAdmins": true
}
Response (200):
{
"data": {
"tenantId": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"passkeyRequiredForAdmins": true
},
"meta": { "apiVersion": "v1.0" }
}
Emits tenant.policy.mfa_changed.v1.
3. Org Unit Endpoints
3.1 GET /api/v1/tenants/{tid}/org-units — List Org Units (tree)
Auth: Any member Response (200):
{
"data": [
{
"id": "ou_01HX...",
"parentId": null,
"name": { "en-US": "Acme Corp" },
"ltreePath": "acme_corp",
"children": [
{
"id": "ou_01HY...",
"parentId": "ou_01HX...",
"name": { "en-US": "Engineering", "ar-SA": "الهندسة" },
"ltreePath": "acme_corp.engineering",
"children": []
}
]
}
],
"meta": { "requestId": "req_01HX..." }
}
3.2 POST /api/v1/tenants/{tid}/org-units — Create Org Unit
Auth: org_owner, org_admin
Request:
{
"parentId": "ou_01HX...",
"name": { "en-US": "Marketing", "ar-SA": "التسويق" }
}
Response (201): Created org unit.
3.3 PATCH /api/v1/tenants/{tid}/org-units/{ouId} — Update/Move
Auth: org_owner, org_admin
Request:
{
"parentId": "ou_01HZ...",
"name": { "en-US": "Marketing & Sales" }
}
Errors: 422 CYCLE_DETECTED, 422 MAX_DEPTH_EXCEEDED
3.4 DELETE /api/v1/tenants/{tid}/org-units/{ouId}
Auth: org_owner
Errors: 422 HAS_CHILDREN (must re-parent first), 422 HAS_MEMBERS (must reassign first)
4. Membership Endpoints
4.1 GET /api/v1/tenants/{tid}/memberships — List Members
Auth: org_owner, org_admin, org_manager (scoped to own subtree)
Query params: ?status=active&orgUnitId=ou_01HX...&roleId=rol_01HX...&cursor=eyJ...&size=50
Response (200):
{
"data": [
{
"id": "mbr_01HX...",
"userId": "usr_01HX...",
"email": "alice@acme.com",
"displayName": "Alice Johnson",
"roleIds": ["rol_01HX..."],
"roleNames": ["org_admin"],
"orgUnitIds": ["ou_01HX..."],
"orgUnitNames": [{ "en-US": "Engineering" }],
"status": "active",
"joinedAt": "2026-02-01T10:00:00Z"
}
],
"meta": {
"page": { "size": 50, "cursor": "eyJ...", "nextCursor": "eyJ...", "totalApproximate": 342 }
}
}
4.2 POST /api/v1/tenants/{tid}/memberships — Invite User
Auth: org_owner, org_admin
Request:
{
"email": "bob@acme.com",
"roleIds": ["rol_01HX..."],
"orgUnitIds": ["ou_01HY..."]
}
Response (201): Membership DTO with status: "invited".
4.3 PATCH /api/v1/tenants/{tid}/memberships/{userId} — Update Member
Auth: org_owner, org_admin
Request:
{
"roleIds": ["rol_01HX...", "rol_01HY..."],
"orgUnitIds": ["ou_01HY...", "ou_01HZ..."]
}
4.4 POST /api/v1/tenants/{tid}/memberships/{userId}/suspend
Auth: org_owner, org_admin
Request:
{
"reason": "Employee offboarded"
}
4.5 DELETE /api/v1/tenants/{tid}/memberships/{userId} — Remove Member
Auth: org_owner
Errors: 422 LAST_OWNER (cannot remove last org_owner)
5. Role Endpoints
5.1 GET /api/v1/tenants/{tid}/roles — List Roles
Auth: org_owner, org_admin
Response: Array of Role DTOs (system roles + tenant custom roles).
5.2 POST /api/v1/tenants/{tid}/roles — Create Custom Role
Auth: org_owner
Request:
{
"name": "Department Lead",
"permissions": [
{ "resource": "enrollment", "action": "read", "condition": { "op": "in", "field": "resource.org_unit_id", "values": ["{{ctx.user.org_unit_ids}}"] } },
{ "resource": "progress", "action": "read" },
{ "resource": "assignment", "action": "write", "condition": { "op": "in", "field": "resource.org_unit_id", "values": ["{{ctx.user.org_unit_ids}}"] } }
]
}
Response (201): Role DTO.
5.3 PATCH /api/v1/tenants/{tid}/roles/{roleId} — Update Role
Auth: org_owner
Errors: 422 SYSTEM_ROLE_IMMUTABLE
5.4 DELETE /api/v1/tenants/{tid}/roles/{roleId} — Delete Role
Auth: org_owner
Errors: 422 SYSTEM_ROLE_IMMUTABLE, 422 ROLE_IN_USE (must unassign first)
6. Authorization Endpoint
6.1 POST /api/v1/authz/check — Authorization Decision
Auth: Any authenticated user (checks own permissions) or service-to-service
Request:
{
"tenantId": "tnt_01HX...",
"userId": "usr_01HX...",
"resource": "course",
"action": "write",
"resourceAttributes": {
"tenant_id": "tnt_01HX...",
"org_unit_id": "ou_01HY...",
"created_by": "usr_01HX...",
"visibility": "org"
}
}
Response (200):
{
"data": {
"allowed": true,
"decisionId": "dec_01HX...",
"matchedRoles": ["org_admin"],
"matchedPermissions": [
{ "resource": "course", "action": "write" }
],
"evaluatedAt": "2026-04-15T10:00:00Z",
"cached": false
}
}
Latency: ≤ 5ms cached, ≤ 20ms uncached.
7. Dynamic Group Endpoints
7.1 GET /api/v1/tenants/{tid}/dynamic-groups — List
Auth: org_owner, org_admin
7.2 POST /api/v1/tenants/{tid}/dynamic-groups — Create
Auth: org_owner, org_admin
Request:
{
"name": "Engineering Learners",
"query": {
"op": "and",
"conditions": [
{ "op": "in", "field": "membership.org_unit_paths", "values": ["acme_corp.engineering"] },
{ "op": "in", "field": "membership.role_names", "values": ["learner"] }
]
}
}
7.3 POST /api/v1/tenants/{tid}/dynamic-groups/{gid}/evaluate — Evaluate
Auth: org_owner, org_admin
Response (200):
{
"data": {
"groupId": "dg_01HX...",
"memberCount": 47,
"memberIds": ["usr_01HX...", "..."],
"evaluatedAt": "2026-04-15T10:00:00Z",
"cachedUntil": "2026-04-15T10:05:00Z"
}
}
8. Feature Flag Endpoints
8.1 PUT /api/v1/tenants/{tid}/feature-flags/{flag} — Set Override
Auth: org_owner, platform_admin
Request:
{
"value": true,
"reason": "Beta testing AI tutor for this org"
}
8.2 GET /api/v1/tenants/{tid}/feature-flags — List Overrides
Auth: org_owner, org_admin
8.3 DELETE /api/v1/tenants/{tid}/feature-flags/{flag} — Remove Override
Auth: org_owner, platform_admin
9. SSO Endpoints
9.1 POST /api/v1/tenants/{tid}/sso/providers — Configure SSO
Auth: org_owner
Request:
{
"protocol": "saml",
"name": "Corporate IdP",
"entityId": "https://idp.acme.com/metadata",
"metadataUrl": "https://idp.acme.com/metadata.xml",
"attributeMapping": {
"email": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress",
"firstName": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname",
"lastName": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname"
}
}
9.2 PATCH /api/v1/tenants/{tid}/sso/providers/{ssoId} — Update
9.3 POST /api/v1/tenants/{tid}/sso/providers/{ssoId}/rotate-secret — Rotate
10. Data Residency Endpoint (M5)
10.1 POST /api/v1/tenants/{tid}/data-residency/migrate
Auth: platform_admin
Request:
{
"targetRegion": "us",
"scheduledAt": "2026-06-01T02:00:00Z",
"notifyOwner": true
}
Response (202):
{
"data": {
"migrationId": "mig_01HX...",
"tenantId": "tnt_01HX...",
"sourceRegion": "eu",
"targetRegion": "us",
"status": "scheduled",
"scheduledAt": "2026-06-01T02:00:00Z"
}
}
11. User-Context Endpoints
11.1 GET /api/v1/me/tenants — My Tenants
Auth: Authenticated user (no tenant context required)
Response (200):
{
"data": [
{
"id": "tnt_01HX...",
"name": "Acme Corp",
"slug": "acme-corp",
"type": "org",
"status": "active",
"myRoles": ["org_admin"],
"myOrgUnits": [{ "id": "ou_01HX...", "name": { "en-US": "Engineering" } }]
}
]
}
11.2 POST /api/v1/me/active-tenant — Switch Active Tenant
Auth: Authenticated user
Request:
{
"tenantId": "tnt_01HX..."
}
Response (200): New JWT or session update with active tid claim.
12. Error Codes (Service-Specific)
| Code | HTTP | Description |
|---|---|---|
SLUG_TAKEN | 409 | Tenant slug already in use |
TENANT_NOT_FOUND | 404 | Tenant ID does not exist |
INVALID_REGION | 422 | Unknown home region |
INVALID_PLAN | 422 | Plan ID not found in billing |
ALREADY_MEMBER | 409 | User already has active membership |
INVITE_NOT_FOUND | 404 | No pending invitation found |
ROLE_NOT_FOUND | 404 | Role ID does not exist |
ORG_UNIT_NOT_FOUND | 404 | Org unit not found in tenant |
SYSTEM_ROLE_IMMUTABLE | 422 | Cannot modify system roles |
ROLE_IN_USE | 422 | Role assigned to members |
LAST_OWNER | 422 | Cannot remove last org_owner |
CYCLE_DETECTED | 422 | Org unit move creates cycle |
MAX_DEPTH_EXCEEDED | 422 | Org unit depth exceeds limit |
HAS_CHILDREN | 422 | Org unit has children |
HAS_MEMBERS | 422 | Org unit has members |
INVALID_STATUS_TRANSITION | 422 | Status transition not allowed |
INVALID_ABAC_QUERY | 422 | ABAC query DSL validation failed |