Skip to main content

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-Key required on all writes
  • If-Match required on PATCH/PUT (optimistic concurrency)
  • Problem+JSON (RFC 9457) error responses
  • X-Tenant-Id header must match JWT tid claim

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:

StatusCodeDescription
409SLUG_TAKENSlug already in use
422INVALID_REGIONUnknown home region
422INVALID_PLANPlan ID not found
429RATE_LIMITEDToo 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)

CodeHTTPDescription
SLUG_TAKEN409Tenant slug already in use
TENANT_NOT_FOUND404Tenant ID does not exist
INVALID_REGION422Unknown home region
INVALID_PLAN422Plan ID not found in billing
ALREADY_MEMBER409User already has active membership
INVITE_NOT_FOUND404No pending invitation found
ROLE_NOT_FOUND404Role ID does not exist
ORG_UNIT_NOT_FOUND404Org unit not found in tenant
SYSTEM_ROLE_IMMUTABLE422Cannot modify system roles
ROLE_IN_USE422Role assigned to members
LAST_OWNER422Cannot remove last org_owner
CYCLE_DETECTED422Org unit move creates cycle
MAX_DEPTH_EXCEEDED422Org unit depth exceeds limit
HAS_CHILDREN422Org unit has children
HAS_MEMBERS422Org unit has members
INVALID_STATUS_TRANSITION422Status transition not allowed
INVALID_ABAC_QUERY422ABAC query DSL validation failed