IAM REST API Reference
The IAM service handles authentication, RBAC, user management, and
pluggable auth providers. All endpoints are served at
https://api.septemcore.com/v1.
See the REST API Overview for authentication headers, error format, pagination, and rate limiting.
For layout brevity, the /api/v1 base path prefix is omitted from the
endpoint tables below.
Group 1: Authentication
| Endpoint | Auth | Description |
|---|---|---|
POST /auth/register | ❌ | Register new user (email + password). Creates tenant + Owner role. |
POST /auth/login | ❌ | Email + password login. Returns token pair or requiresTenantSelection. |
POST /auth/login/:providerId | ❌ | Login via OAuth/wallet provider. Returns redirect URL. |
GET /auth/callback/:providerId | ❌ | OAuth callback handler. Returns token pair or requiresTenantSelection. |
POST /auth/logout | ✅ | Invalidate refresh token. Subsequent refresh with this token → 401. |
POST /auth/refresh | ❌ | Rotate access token using refresh token. Returns new token pair. |
GET /auth/me | ✅ | Current authenticated user with roles and permissions. |
POST /auth/impersonate | users.impersonate | Platform Owner only. Get scoped JWT for a tenant. Requires reason. |
POST /api/v1/auth/login
Request:
{
"password": "S3cur3P@ssw0rd!",
"mfaCode": "123456"
}
Response (single-tenant user):
{
"accessToken": "eyJhbGci...",
"refreshToken": "rt_01j9p...",
"expiresAt": "2026-04-22T03:15:00Z"
}
Response (multi-tenant user — no lastTenantId):
{
"requiresTenantSelection": true,
"sessionToken": "st_01j9p...",
"tenants": [
{ "id": "01j9pten0000000000000001", "name": "Acme Corp", "logoUrl": "https://...", "role": "admin" },
{ "id": "01j9pten0000000000000002", "name": "Beta Inc", "logoUrl": "https://...", "role": "member" }
]
}
mfaCode is required only if the user has 2FA enabled. Omit when 2FA
is off.
POST /api/v1/auth/impersonate
Request:
{
"tenantId": "01j9pten0000000000000001",
"reason": "security_investigation",
"note": "Investigating suspicious login from IP 1.2.3.4 per ticket SEC-451"
}
Response: standard token pair with tenantId scoped to the target
tenant. TTL: 1 hour. Audit record: auth.impersonation.started.
Group 2: MFA (Two-Factor Authentication)
| Endpoint | Auth | Description |
|---|---|---|
POST /auth/mfa/enable | ✅ | Generate TOTP secret + QR code. User scans in Authenticator app. |
POST /auth/mfa/verify | ✅ | Confirm TOTP setup by entering a code. Activates 2FA. |
POST /auth/mfa/disable | ✅ | Disable 2FA. Requires current password + active TOTP code. |
POST /auth/recovery | ❌ | Emergency login via recovery code. Allows resetting email+password+2FA. |
GET /auth/recovery-codes | ✅ | Returns count of remaining recovery codes. |
POST /auth/recovery-codes/regenerate | ✅ | Generate 10 new recovery codes. Invalidates old codes. |
Recovery Code Security
| Parameter | Value |
|---|---|
| Count per user | 10 codes, generated at registration and at 2FA enable |
| Format | Random alphanumeric strings, each single-use |
| Storage | argon2id hash in PostgreSQL — plaintext shown only once at generation |
| Rate limit | 5 attempts / 15 minutes (RECOVERY_MAX_ATTEMPTS). Exceeded → 1-hour lock + Owner email alert |
| Hard lock | After 15 failed attempts (3 lockout cycles) → account status=locked + Platform Admin alert |
Group 3: Security Flows
| Endpoint | Auth | Description |
|---|---|---|
POST /auth/verify-email | ❌ | Confirm email address from verification token. TTL: 24h. |
POST /auth/request-reset | ❌ | Request password-reset email. One-time token sent via Notify. |
POST /auth/reset-password | ❌ | Reset password using one-time token from email. TTL: 1h. |
POST /auth/invite | users.create | Invite user by email with pre-assigned role. TTL: 72h. |
POST /auth/accept-invite | ❌ | Accept invitation — set password, complete registration. |
Request — POST /api/v1/auth/invite:
{
"roles": ["support-manager"]
}
Group 4: Auth Providers
| Endpoint | Auth | Description |
|---|---|---|
GET /auth/providers | ❌ | List installed auth providers for login page rendering. |
POST /users/:id/providers/:providerId | ✅ | Link an additional auth provider to an existing account. |
DELETE /users/:id/providers/:providerId | ✅ | Unlink a provider. Rejected if it's the last linked provider. |
GET /users/:id/providers | ✅ | List all provider links for a user. |
Provider Object
{
"id": "01j9pprov000000000000001",
"providerId": "google",
"providerType": "oauth",
"externalId": "accounts.google.com|117982...",
"linkedAt": "2026-04-01T09:00:00Z"
}
Uniqueness constraint:
(providerId, externalId)is unique across all users. Attempting to link a Google account already linked to another user →409 Conflict.
Group 5: Users
| Endpoint | Auth | Description |
|---|---|---|
POST /users | users.create | Create user in the current tenant. |
GET /users | users.list | List users (paginated). Default excludes deleted. |
GET /users/:id | users.list | User details with roles and permission summary. |
PATCH /users/:id | users.update | Partial update (name, email, status). |
DELETE /users/:id | users.delete | Soft delete (blocks login, retains data). |
PATCH /users/:id/restore | users.update | Restore soft-deleted user (re-enables login). |
POST /users/:id/roles | roles.assign | Assign a role to user. |
DELETE /users/:id/roles/:role | roles.assign | Revoke a role from user. |
User Object
{
"id": "01j9pusr0000000000000001",
"firstName": "Alice",
"lastName": "Johnson",
"avatarUrl": "https://cdn.platform.io/assets/avatars/alice.webp",
"tenantId": "01j9pten0000000000000001",
"roles": ["admin", "billing-viewer"],
"emailVerified": true,
"mfaEnabled": true,
"status": "active",
"createdAt": "2026-04-01T10:00:00Z",
"updatedAt": "2026-04-20T14:30:00Z",
"deletedAt": null
}
RBAC Limits
| Parameter | Limit | Notes |
|---|---|---|
| Max roles per user | 50 | Permissions union resolved at Gateway (Valkey cache) |
| Max permissions per role | 1,000 | Exceeding = 400 (rbac-limit-exceeded) |
| Max roles per tenant | 500 | Exceeding = 400 (rbac-limit-exceeded) |
Group 6: RBAC — Roles & Permissions
Roles are created from atomic permissions. There are no built-in non-platform roles — tenant admins compose roles from available permissions.
| Endpoint | Auth | Description |
|---|---|---|
GET /roles | roles.list | List all roles for this tenant (cursor-paginated). |
POST /roles | roles.create | Create a new role with a set of permissions. |
GET /roles/:id | roles.list | Role details with full permissions list. |
PATCH /roles/:id | roles.update | Update role permissions. |
DELETE /roles/:id | roles.delete | Delete custom role (built-in roles cannot be deleted). |
GET /permissions | roles.list | List all registered permissions from all modules. |
GET /users/:id/permissions | users.list | Resolved permission list for user (union of roles). |
POST /api/v1/roles
{
"name": "Support Manager",
"description": "Can read contacts, close tickets, cannot delete.",
"permissions": [
"crm.contacts.read",
"crm.tickets.read",
"crm.tickets.close",
"audit.read"
]
}
Wildcard Permissions
| Permission | Effect |
|---|---|
crm.* | Full access to all CRM module operations |
crm.contacts.read | Granular: read-only on contacts |
billing.* | Full billing access |
Resolution: Gateway checks exact match first, then wildcard {module}.*.
Platform-reserved permissions (system.*, platform.*) cannot be
registered by modules.
Role Object
{
"id": "01j9prole000000000000001",
"name": "Support Manager",
"description": "Can read contacts, close tickets.",
"permissions": ["crm.contacts.read", "crm.tickets.read"],
"tenantId": "01j9pten0000000000000001",
"createdBy": "01j9pusr0000000000000001",
"createdAt": "2026-04-10T08:00:00Z"
}
Group 7: Multi-Tenant
| Endpoint | Auth | Description |
|---|---|---|
GET /auth/tenants | ✅ | List all tenants the user belongs to. |
POST /auth/select-tenant | ❌ (session) | Select tenant during login flow. Returns full JWT. |
POST /auth/switch-tenant | ✅ | Switch active tenant without re-login. Returns new token pair. |
POST /api/v1/auth/select-tenant
Called after receiving requiresTenantSelection: true from login:
{
"sessionToken": "st_01j9p...",
"tenantId": "01j9pten0000000000000001",
"rememberChoice": true
}
rememberChoice: true saves lastTenantId — next login auto-selects
this tenant.
POST /api/v1/auth/switch-tenant
{
"tenantId": "01j9pten0000000000000002"
}
Response: new full token pair scoped to the target tenant. The refresh token is tenant-scoped — one stolen token does not give access to the user's other tenants.
Error Reference
| Error type | Status | Trigger |
|---|---|---|
problems/invalid-credentials | 401 | Wrong email or password |
problems/mfa-required | 401 | 2FA enabled but mfaCode not provided |
problems/mfa-invalid | 401 | TOTP code incorrect or expired |
problems/token-expired | 401 | Access or refresh token expired |
problems/account-locked | 403 | Account locked after failed recovery attempts |
problems/forbidden | 403 | Insufficient permission for operation |
problems/rbac-limit-exceeded | 400 | Role/permission count exceeds plan limit |
problems/provider-already-linked | 409 | OAuth provider already linked to another user |
problems/last-provider | 400 | Cannot unlink the last auth provider |
problems/transfer-in-progress | 409 | Tenant ownership transfer already pending |