User Management API Reference
Base URL:
https://auth.calimatic.comAll endpoints return JSON with a standard envelope:
{ "success": true, "data": { ... } }on success, or{ "success": false, "error": "message" }on failure.
Authentication
All user management endpoints support four authentication methods:
| Method | Headers | Use Case |
|---|---|---|
| Session | Cookie-based session from Calimatic Auth login | Admin dashboard, internal tools |
| API Key | x-api-key: your-api-key | Server-to-server integration (org-scoped) |
| App Client (Basic) | x-client-id: cca_... + x-client-secret: ccas_... | App-initiated provisioning |
| OAuth2 Bearer | Authorization: Bearer <access_token> | Standard OAuth2 client_credentials flow |
Auth Mode Selection
- App Client (Basic) is the simplest mode for service-to-service calls — send the credentials directly on every request.
- OAuth2 Bearer is the standard OAuth 2.0 mode. First call
POST /api/v1/oidc/tokenwithgrant_type=client_credentialsto get an access token, then send it asAuthorization: Bearer <token>on subsequent requests. Tokens are JWTs signed by the platform and can be cached for their full TTL (default 1 hour). - API Key is org-scoped — the key cannot be used outside the organization it was issued for.
- Session is for browser-based flows (admin dashboard).
Only client_credentials Bearer tokens are accepted on these REST endpoints. User-context Bearer tokens (from authorization_code grant) are not accepted — use the NextAuth session cookie instead.
App clients are cross-org by design: the same client credentials work against any organization where the app is enabled. Required permissions are stored on the app_client record (not as OAuth scopes); ask a Calimatic admin to grant org:manage, org:users:manage, etc., as needed.
Most endpoints require the org:users:manage permission. Organization-create requires org:manage.
Endpoints Overview
Organizations
| Method | Endpoint | Description |
|---|---|---|
POST | /api/v1/organizations | Create a new organization |
GET | /api/v1/organizations?slug=<slug> | Look up organization by slug |
Org-scoped user management
| Method | Endpoint | Description |
|---|---|---|
GET | /api/v1/organizations/{orgId}/users | List members of an organization |
POST | /api/v1/organizations/{orgId}/users | Invite a user to an organization (auto-licenses calling app) |
POST | /api/v1/organizations/{orgId}/users/bulk-invite | Bulk invite up to 100 users |
GET | /api/v1/organizations/{orgId}/users/{userId} | Get user details in org context |
PATCH | /api/v1/organizations/{orgId}/users/{userId} | Update org role / user status |
DELETE | /api/v1/organizations/{orgId}/users/{userId} | Remove a user from an organization |
POST | /api/v1/organizations/{orgId}/users/{userId}/reset-password | Send a password reset email |
Global user management
| Method | Endpoint | Description |
|---|---|---|
POST | /api/v1/users/provision | Create or provision a single user |
POST | /api/v1/users/provision/bulk | Bulk provision up to 500 users |
POST | /api/v1/users/import | Import existing users (migration-friendly) |
GET | /api/v1/users/resolve | Resolve user by email or Keycloak ID |
GET | /api/v1/users/{id} | Get a user by platform identity ID |
PATCH | /api/v1/users/{id} | Update profile fields (incl. emailVerified) |
DELETE | /api/v1/users/{id} | Permanently delete a user |
POST | /api/v1/users/{id}/reset-password | Trigger password reset email |
POST | /api/v1/users/{id}/set-password | Set a temporary password |
Organization endpoints require org:manage permission. User endpoints require org:users:manage permission.
POST /api/v1/organizations
Create a new organization on the auth platform. Apps must create organizations before provisioning users into them.
When called by an app client, the calling app is automatically enabled for the new organization.
Request
POST /api/v1/organizations
Content-Type: application/json
Required permission: org:manage
Request Body
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
name | string | Yes | -- | Organization name. Max 255 characters. |
slug | string | Yes | -- | URL-friendly identifier. Lowercase alphanumeric with hyphens. Max 100 characters. |
type | string | No | "customer" | One of: customer, partner, internal. |
domain | string | No | -- | Organization domain (e.g., acme.com). Max 255 characters. |
plan | string | No | "free" | One of: free, starter, professional, enterprise. |
Example Request
curl -X POST https://auth.calimatic.com/api/v1/organizations \
-H "Content-Type: application/json" \
-H "x-client-id: cca_aBcDeFgHiJkL" \
-H "x-client-secret: ccas_xYzAbCdEfGhI" \
-d '{
"name": "Acme Corp",
"slug": "acme-corp",
"type": "customer",
"plan": "professional"
}'
Response (201 Created)
{
"success": true,
"data": {
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"name": "Acme Corp",
"slug": "acme-corp",
"type": "customer",
"plan": "professional",
"isActive": true,
"createdAt": "2025-01-15T10:00:00.000Z"
}
}
Response (409 Conflict -- Slug Already Exists)
If the slug already exists, the existing organization is returned with a 409 status. This allows idempotent org creation -- you can safely call this endpoint without checking first.
{
"success": true,
"data": {
"id": "existing-org-uuid",
"name": "Acme Corp",
"slug": "acme-corp",
"type": "customer",
"plan": "professional",
"alreadyExists": true
}
}
GET /api/v1/organizations
Look up an organization by slug. Useful for apps to check if an org exists before creating it, or to resolve the org ID after creation.
Request
GET /api/v1/organizations?slug=acme-corp
Required permission: org:manage
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
slug | string | Yes | The organization slug to look up. |
Response (200 OK)
{
"success": true,
"data": {
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"name": "Acme Corp",
"slug": "acme-corp",
"type": "customer",
"plan": "professional",
"domain": "acme.com",
"isActive": true
}
}
Response (404 Not Found)
{
"success": true,
"data": null
}
POST /api/v1/organizations/{orgId}/users
Invite a user to an organization. If the user does not yet exist in the platform, they are created in Keycloak and the platform database. If they exist, they are added as a member of the organization.
When called by an app client (Basic credentials or Bearer), the calling app's license is automatically assigned to the user — no separate license-assignment call is required.
This is the canonical "add a user to my org" endpoint for app clients implementing org-membership-based licensing.
Request
POST /api/v1/organizations/{orgId}/users
Content-Type: application/json
Required permission: none beyond authentication (any app client with valid credentials can invite to an org where the app is enabled).
Path Parameters
| Parameter | Type | Description |
|---|---|---|
orgId | string (UUID) | The organization to invite the user to. |
Request Body
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
email | string | Yes | -- | Valid email address. Lowercased and trimmed. |
firstName | string | Yes | -- | User's first name. 1-100 characters. |
lastName | string | Yes | -- | User's last name. 1-100 characters. |
applications | string[] | No | [] | Additional app licenses to assign. The calling app is auto-included. |
sendInviteEmail | boolean | No | true | If true, sends an invitation email with password setup link. |
Note: This endpoint takes
firstName/lastName, not{userId, role}. To resolve an existing user first, useGET /api/v1/users/resolve?email=....
Example: App Client (Basic)
curl -X POST https://auth.calimatic.com/api/v1/organizations/{orgId}/users \
-H "Content-Type: application/json" \
-H "x-client-id: cca_aBcDeFgHiJkL" \
-H "x-client-secret: ccas_xYzAbCdEfGhI" \
-d '{
"email": "jane@example.com",
"firstName": "Jane",
"lastName": "Smith",
"sendInviteEmail": true
}'
Example: App Client (Bearer token)
# First, get a client_credentials token
TOKEN=$(curl -s -X POST https://auth.calimatic.com/api/v1/oidc/token \
-d "grant_type=client_credentials" \
-d "client_id=cca_aBcDeFgHiJkL" \
-d "client_secret=ccas_xYzAbCdEfGhI" | jq -r .access_token)
# Then invite the user
curl -X POST https://auth.calimatic.com/api/v1/organizations/{orgId}/users \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{
"email": "jane@example.com",
"firstName": "Jane",
"lastName": "Smith"
}'
Response (201 Created)
{
"success": true,
"data": {
"message": "User invited successfully",
"userId": "f1g2h3i4-j5k6-7890-lmno-pq1234567890",
"email": "jane@example.com",
"isNewUser": true
}
}
Error Responses
| Status | Error | Description |
|---|---|---|
| 401 | Unauthorized | Missing or invalid authentication |
| 404 | Not found | Organization does not exist |
| 422 | Validation error | Invalid request body |
| 500 | Server error | Internal error during invitation |
GET /api/v1/organizations/{orgId}/users
List members of an organization with pagination and search.
Request
GET /api/v1/organizations/{orgId}/users?page=1&limit=20&search=jane
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
page | integer | 1 | Page number (1-indexed). |
limit | integer | 20 | Page size. Maximum 100. |
search | string | -- | Search by email, firstName, lastName, displayName. |
Example
curl "https://auth.calimatic.com/api/v1/organizations/{orgId}/users?page=1&limit=50" \
-H "x-client-id: cca_aBcDeFgHiJkL" \
-H "x-client-secret: ccas_xYzAbCdEfGhI"
Response (200 OK)
{
"success": true,
"data": {
"members": [
{
"id": "f1g2h3i4-...",
"email": "jane@example.com",
"firstName": "Jane",
"lastName": "Smith",
"role": "member",
"joinedAt": "2025-01-10T10:00:00.000Z"
}
],
"total": 1,
"page": 1,
"limit": 50,
"totalPages": 1
}
}
GET /api/v1/users/{userId}
Retrieve a single user by their platform identity ID. Returns the full profile including organization memberships and Keycloak-mirrored fields like emailVerified.
Request
GET /api/v1/users/{userId}
Path Parameters
| Parameter | Type | Description |
|---|---|---|
userId | string (UUID) | The platform identity ID (not the Keycloak ID). |
Example
curl https://auth.calimatic.com/api/v1/users/f1g2h3i4-j5k6-7890 \
-H "x-client-id: cca_aBcDeFgHiJkL" \
-H "x-client-secret: ccas_xYzAbCdEfGhI"
Response (200 OK)
{
"success": true,
"data": {
"id": "f1g2h3i4-j5k6-7890-lmno-pq1234567890",
"keycloakUserId": "kc-a1b2c3d4-...",
"email": "jane@example.com",
"emailVerified": true,
"firstName": "Jane",
"lastName": "Smith",
"displayName": "Jane Smith",
"avatarUrl": null,
"phone": null,
"timezone": "America/New_York",
"locale": "en",
"userType": "customer_admin",
"primaryOrganizationId": "org-uuid-...",
"organizationMemberships": [
{
"organization_id": "org-uuid-...",
"role": "member",
"permissions": [],
"joined_at": "2025-01-10T10:00:00.000Z"
}
],
"status": "active",
"lastLoginAt": "2025-01-15T14:30:00.000Z",
"loginCount": 7,
"createdAt": "2025-01-10T10:00:00.000Z",
"updatedAt": "2025-01-15T14:30:00.000Z"
}
}
Error Responses
| Status | Error | Description |
|---|---|---|
| 401 | Unauthorized | Missing or invalid authentication |
| 404 | User not found | No user matches the provided ID |
PATCH /api/v1/users/{userId}
Update a user's profile fields. Writable fields are mirrored to Keycloak where relevant (firstName, lastName, emailVerified).
This is the canonical endpoint for upstream apps to mirror profile state and email-verification status into the platform.
Request
PATCH /api/v1/users/{userId}
Content-Type: application/json
Request Body
All fields are optional — only the keys you send are updated.
| Field | Type | Description |
|---|---|---|
firstName | string | 1-100 chars. Mirrored to Keycloak. |
lastName | string | 1-100 chars. Mirrored to Keycloak. |
displayName | string | 1-255 chars. Platform-only. |
phone | string | null | Up to 50 chars. Pass null to clear. |
timezone | string | IANA timezone name. |
locale | string | IETF language tag (e.g., en, en-US). |
emailVerified | boolean | Mirrored to Keycloak. |
Example: Sync email verification from an upstream app
curl -X PATCH https://auth.calimatic.com/api/v1/users/f1g2h3i4-j5k6-7890 \
-H "Content-Type: application/json" \
-H "x-client-id: cca_aBcDeFgHiJkL" \
-H "x-client-secret: ccas_xYzAbCdEfGhI" \
-d '{ "emailVerified": true }'
Example: Update profile
curl -X PATCH https://auth.calimatic.com/api/v1/users/f1g2h3i4-j5k6-7890 \
-H "Content-Type: application/json" \
-H "x-client-id: cca_aBcDeFgHiJkL" \
-H "x-client-secret: ccas_xYzAbCdEfGhI" \
-d '{
"firstName": "Janet",
"displayName": "Janet S.",
"timezone": "America/Los_Angeles"
}'
Response (200 OK)
Returns the updated user record (same shape as GET /api/v1/users/{userId}).
Error Responses
| Status | Error | Description |
|---|---|---|
| 401 | Unauthorized | Missing or invalid authentication |
| 404 | User not found | No user matches the provided ID |
| 422 | Validation error | Invalid field values |
Note: Keycloak sync failures are logged but do not fail the request — the platform DB is the source of truth and is always updated first. If Keycloak rejects the mirror write, the API still returns 200 and you should reconcile on the next webhook event.
DELETE /api/v1/users/{userId}
Permanently delete a user from Keycloak and the platform database. Cascades to user_app_licenses, user_role_assignments, sessions, MFA configuration, and removes the user_identities row.
This is irreversible. For temporary disablement, use PATCH /api/v1/organizations/{orgId}/users/{userId} with { "status": "suspended" } instead.
Request
DELETE /api/v1/users/{userId}
Example
curl -X DELETE https://auth.calimatic.com/api/v1/users/f1g2h3i4-j5k6-7890 \
-H "x-client-id: cca_aBcDeFgHiJkL" \
-H "x-client-secret: ccas_xYzAbCdEfGhI"
Response (200 OK)
{
"success": true,
"data": {
"message": "User deleted",
"userId": "f1g2h3i4-j5k6-7890-lmno-pq1234567890"
}
}
Error Responses
| Status | Error | Description |
|---|---|---|
| 401 | Unauthorized | Missing or invalid authentication |
| 404 | User not found | No user matches the provided ID |
| 500 | Server error | Deletion failed in Keycloak or DB |
POST /api/v1/users/provision
Create or provision a single user. If the user already exists (by email), adds them to the specified organization instead of creating a duplicate.
When called by an app client, the calling app's license is automatically assigned to the user.
Request
POST /api/v1/users/provision
Content-Type: application/json
Request Body
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
email | string | Yes | -- | Valid email address. Lowercased and trimmed. |
firstName | string | Yes | -- | User's first name. Max 100 characters. |
lastName | string | Yes | -- | User's last name. Max 100 characters. |
organizationId | string (UUID) | No | Requestor's org | Organization to add the user to. |
role | string | No | "member" | Role within the organization. Max 50 characters. |
temporaryPassword | string | No | -- | Temporary password (min 8 chars). User must change on first login. |
passwordHash | string | No | -- | Existing bcrypt or PBKDF2 hash for seamless migration. When provided, the hash is stored directly and no invite email is sent. Users can log in immediately with their existing password. Max 1024 chars. |
applications | string[] | No | [] | App licenses to assign. Calling app's license is auto-included. |
sendInviteEmail | boolean | No | true | Send an invitation email with a password setup link. |
externalId | string | No | -- | External identifier for cross-referencing. Max 255 characters. |
metadata | object | No | -- | Arbitrary key-value pairs for custom data. |
Example Request
curl -X POST https://auth.calimatic.com/api/v1/users/provision \
-H "Content-Type: application/json" \
-H "x-client-id: cca_aBcDeFgHiJkL" \
-H "x-client-secret: ccas_xYzAbCdEfGhI" \
-d '{
"email": "jane@school.edu",
"firstName": "Jane",
"lastName": "Smith",
"organizationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"role": "member",
"sendInviteEmail": true,
"externalId": "usr_12345"
}'
const response = await fetch("https://auth.calimatic.com/api/v1/users/provision", {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-client-id": "cca_aBcDeFgHiJkL",
"x-client-secret": "ccas_xYzAbCdEfGhI",
},
body: JSON.stringify({
email: "jane@school.edu",
firstName: "Jane",
lastName: "Smith",
organizationId: "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
role: "member",
sendInviteEmail: true,
externalId: "usr_12345",
}),
});
Example: Migrate User with Existing Password
curl -X POST https://auth.calimatic.com/api/v1/users/provision \
-H "Content-Type: application/json" \
-H "x-client-id: cca_aBcDeFgHiJkL" \
-H "x-client-secret: ccas_xYzAbCdEfGhI" \
-d '{
"email": "jane@school.edu",
"firstName": "Jane",
"lastName": "Smith",
"organizationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"passwordHash": "$2b$12$LJ3m4ys3Lk0TSwHiPbUXR.ZaWgmFLfD5hsOqpDumNcBEkb/FumBcG",
"sendInviteEmail": false
}'
Response (201 Created -- New User)
{
"success": true,
"data": {
"userId": "f1g2h3i4-j5k6-7890-lmno-pq1234567890",
"keycloakUserId": "kc-a1b2c3d4-e5f6-...",
"email": "jane@school.edu",
"isNewUser": true,
"isNewKeycloakUser": true,
"status": "user_created"
}
}
Response (200 OK -- Existing User Updated)
{
"success": true,
"data": {
"userId": "f1g2h3i4-j5k6-7890-lmno-pq1234567890",
"keycloakUserId": "kc-a1b2c3d4-e5f6-...",
"email": "jane@school.edu",
"isNewUser": false,
"status": "existing_user_updated"
}
}
Error Responses
| Status | Error | Description |
|---|---|---|
| 401 | Unauthorized | Missing or invalid authentication |
| 403 | Forbidden | Missing org:users:manage permission |
| 404 | ORG_NOT_FOUND | Organization ID does not exist |
| 409 | USER_EXISTS | User exists (when deduplication is not desired) |
| 422 | Validation error | Invalid request body (bad email, missing fields) |
| 500 | Server error | Internal error during provisioning |
POST /api/v1/users/provision/bulk
Bulk provision up to 500 users in a single request. Each user is processed sequentially with error isolation -- failures on individual users do not block the rest.
Request
POST /api/v1/users/provision/bulk
Content-Type: application/json
Request Body
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
users | array | Yes | -- | Array of user objects (same schema as single provision). Min 1, max 500. |
defaultOrganizationId | string (UUID) | No | Requestor's org | Default organization for users that do not specify one. |
defaultApplications | string[] | No | [] | Default app licenses for all users. Calling app is auto-included. |
sendInviteEmails | boolean | No | false | Send invitation emails to all users. |
skipExisting | boolean | No | true | If true, update existing users instead of failing. |
Each user object in the users array supports:
| Field | Type | Required | Description |
|---|---|---|---|
email | string | Yes | Valid email address |
firstName | string | Yes | Max 100 characters |
lastName | string | Yes | Max 100 characters |
organizationId | string (UUID) | No | Override default org |
role | string | No | Override default role |
temporaryPassword | string | No | Min 8 characters |
passwordHash | string | No | Existing bcrypt/PBKDF2 hash for seamless migration. |
applications | string[] | No | Override default apps |
externalId | string | No | External identifier |
metadata | object | No | Custom key-value pairs |
Example Request
curl -X POST https://auth.calimatic.com/api/v1/users/provision/bulk \
-H "Content-Type: application/json" \
-H "x-client-id: cca_aBcDeFgHiJkL" \
-H "x-client-secret: ccas_xYzAbCdEfGhI" \
-d '{
"users": [
{ "email": "user1@school.edu", "firstName": "User", "lastName": "One", "role": "member" },
{ "email": "user2@school.edu", "firstName": "User", "lastName": "Two", "role": "admin" },
{ "email": "user3@school.edu", "firstName": "User", "lastName": "Three" }
],
"defaultOrganizationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"sendInviteEmails": true,
"skipExisting": true
}'
Response (200 OK)
{
"success": true,
"data": {
"total": 3,
"created": 2,
"updated": 1,
"skipped": 0,
"failed": 0,
"errors": [],
"users": [
{ "email": "user1@school.edu", "userId": "uuid-1", "status": "user_created" },
{ "email": "user2@school.edu", "userId": "uuid-2", "status": "user_created" },
{ "email": "user3@school.edu", "userId": "uuid-3", "status": "existing_user_updated" }
]
}
}
Partial Failure Response
{
"success": true,
"data": {
"total": 3,
"created": 2,
"updated": 0,
"skipped": 0,
"failed": 1,
"errors": [
{
"email": "bad-email",
"error": "Must be a valid email address",
"index": 2
}
],
"users": [
{ "email": "user1@school.edu", "userId": "uuid-1", "status": "user_created" },
{ "email": "user2@school.edu", "userId": "uuid-2", "status": "user_created" }
]
}
}
POST /api/v1/users/import
Alias for /api/v1/users/provision/bulk with migration-friendly defaults. Designed for importing existing users from other systems.
Differences from Bulk Provision
| Setting | Bulk Provision Default | Import Default |
|---|---|---|
sendInviteEmails | false | false |
skipExisting | true | true |
These defaults can still be overridden in the request body.
The import API also supports passwordHash per user for seamless password migration. See Password Hash Migration below.
Request
POST /api/v1/users/import
Content-Type: application/json
The request body schema is identical to bulk provision.
Example Request
curl -X POST https://auth.calimatic.com/api/v1/users/import \
-H "Content-Type: application/json" \
-H "x-client-id: cca_aBcDeFgHiJkL" \
-H "x-client-secret: ccas_xYzAbCdEfGhI" \
-d '{
"users": [
{
"email": "jane@example.com",
"firstName": "Jane",
"lastName": "Smith",
"role": "admin",
"externalId": "usr_12345"
},
{
"email": "bob@example.com",
"firstName": "Bob",
"lastName": "Jones",
"externalId": "usr_12346"
}
],
"defaultOrganizationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"defaultApplications": ["edtech"]
}'
Response (200 OK)
{
"success": true,
"data": {
"total": 2,
"created": 2,
"updated": 0,
"skipped": 0,
"failed": 0,
"message": "Import complete: 2 created, 0 updated, 0 failed",
"errors": [],
"users": [
{ "email": "jane@example.com", "userId": "uuid-1", "status": "user_created" },
{ "email": "bob@example.com", "userId": "uuid-2", "status": "user_created" }
]
}
}
GET /api/v1/users/resolve
Resolve a user by email address or Keycloak ID. Returns the user's full platform identity including organization memberships and app licenses.
This is the primary endpoint apps call after OIDC login to determine a user's permissions, org context, and license status.
Request
GET /api/v1/users/resolve?email=jane@example.com
GET /api/v1/users/resolve?keycloakId=kc-uuid-here
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
email | string | One of email or keycloakId | User's email address |
keycloakId | string | One of email or keycloakId | User's Keycloak user ID (the sub claim from OIDC) |
Example Request
curl "https://auth.calimatic.com/api/v1/users/resolve?email=jane@school.edu" \
-H "x-client-id: cca_aBcDeFgHiJkL" \
-H "x-client-secret: ccas_xYzAbCdEfGhI"
const response = await fetch(
`https://auth.calimatic.com/api/v1/users/resolve?email=${encodeURIComponent(email)}`,
{
headers: {
"x-client-id": process.env.CALIMATIC_CLIENT_ID!,
"x-client-secret": process.env.CALIMATIC_CLIENT_SECRET!,
},
}
);
Response (200 OK)
{
"success": true,
"data": {
"user": {
"id": "f1g2h3i4-j5k6-7890-lmno-pq1234567890",
"keycloakUserId": "kc-a1b2c3d4-e5f6-...",
"email": "jane@school.edu",
"emailVerified": true,
"firstName": "Jane",
"lastName": "Smith",
"displayName": "Jane Smith",
"avatarUrl": "https://lh3.googleusercontent.com/photo.jpg",
"phone": null,
"timezone": "America/New_York",
"locale": "en",
"userType": "customer_admin",
"primaryOrganizationId": "org-uuid-...",
"status": "active",
"isActive": true,
"source": "provisioning",
"lastLoginAt": "2025-01-15T14:30:00.000Z",
"createdAt": "2025-01-10T10:00:00.000Z"
},
"organizations": [
{
"id": "org-uuid-...",
"name": "Springfield Elementary",
"slug": "springfield-elementary",
"domain": "springfield.edu",
"logoUrl": "https://...",
"type": "school",
"plan": "professional",
"membershipRole": "admin",
"membershipPermissions": [],
"joinedAt": "2025-01-10T10:00:00.000Z",
"isPrimary": true
}
],
"licenses": [
{
"application": "edtech",
"organizationId": "org-uuid-...",
"assignedAt": "2025-01-10T10:00:00.000Z",
"source": "provisioning"
}
],
"hasLicense": true
}
}
The hasLicense field is only included when the request is made by an app client. It indicates whether the user has a license for the calling application.
Error Responses
| Status | Error | Description |
|---|---|---|
| 400 | Missing parameter | Neither email nor keycloakId provided |
| 401 | Unauthorized | Missing or invalid authentication |
| 404 | User not found | No user matches the provided email or Keycloak ID |
| 500 | Server error | Internal error during resolution |
POST /api/v1/users/{id}/reset-password
Trigger a password reset email for a user. The user receives a Keycloak-managed email with a link to set a new password.
Request
POST /api/v1/users/{userId}/reset-password
Path Parameters
| Parameter | Type | Description |
|---|---|---|
userId | string (UUID) | The platform user identity ID |
Example Request
curl -X POST https://auth.calimatic.com/api/v1/users/f1g2h3i4-j5k6-7890/reset-password \
-H "x-client-id: cca_aBcDeFgHiJkL" \
-H "x-client-secret: ccas_xYzAbCdEfGhI"
await fetch(`https://auth.calimatic.com/api/v1/users/${userId}/reset-password`, {
method: "POST",
headers: {
"x-client-id": process.env.CALIMATIC_CLIENT_ID!,
"x-client-secret": process.env.CALIMATIC_CLIENT_SECRET!,
},
});
Response (200 OK)
{
"success": true,
"data": {
"message": "Password reset email sent successfully",
"userId": "f1g2h3i4-j5k6-7890-lmno-pq1234567890"
}
}
Side Effects
- A
user.password_resetwebhook event is dispatched - An audit log entry is created
POST /api/v1/users/{id}/set-password
Set a temporary password for a user. The user will be required to change it on their next login.
Request
POST /api/v1/users/{userId}/set-password
Content-Type: application/json
Path Parameters
| Parameter | Type | Description |
|---|---|---|
userId | string (UUID) | The platform user identity ID |
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
temporaryPassword | string | Yes | Temporary password. Must meet the platform's password policy (min 8 chars, at least 1 digit). |
Example Request
curl -X POST https://auth.calimatic.com/api/v1/users/f1g2h3i4-j5k6-7890/set-password \
-H "Content-Type: application/json" \
-H "x-client-id: cca_aBcDeFgHiJkL" \
-H "x-client-secret: ccas_xYzAbCdEfGhI" \
-d '{ "temporaryPassword": "TempP@ss2024!" }'
await fetch(`https://auth.calimatic.com/api/v1/users/${userId}/set-password`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-client-id": process.env.CALIMATIC_CLIENT_ID!,
"x-client-secret": process.env.CALIMATIC_CLIENT_SECRET!,
},
body: JSON.stringify({ temporaryPassword: "TempP@ss2024!" }),
});
Response (200 OK)
{
"success": true,
"data": {
"message": "Temporary password set successfully",
"userId": "f1g2h3i4-j5k6-7890-lmno-pq1234567890"
}
}
Error Responses
| Status | Error | Description |
|---|---|---|
| 401 | Unauthorized | Missing or invalid authentication |
| 403 | Forbidden | Missing org:users:manage permission |
| 422 | Validation error | Password does not meet requirements |
| 500 | Server error | Failed to set password in Keycloak |
Side Effects
- A
user.password_resetwebhook event is dispatched (withtype: "temporary_password_set") - An audit log entry is created
- Keycloak marks the user with
UPDATE_PASSWORDrequired action
Password Hash Migration
When migrating users from an existing application, you can transfer their password hashes directly to avoid requiring password resets. This enables a seamless migration where users log in with their existing passwords.
Supported Hash Formats
| Format | Example Prefix | Notes |
|---|---|---|
| bcrypt | $2b$, $2a$ | Stored and verified as-is. Recommended. |
| PBKDF2 (Keycloak format) | {"algorithm":"pbkdf2-sha512",...} | Automatically upgraded to bcrypt on first login. |
How It Works
- Pass the user's existing password hash in the
passwordHashfield during provisioning - The hash is stored directly in the platform database
- No invite email is sent (regardless of
sendInviteEmailsetting) - The user can log in immediately using their existing password
- For PBKDF2 hashes, the platform transparently upgrades to bcrypt on successful login
Example: Bulk Migration with Password Hashes
curl -X POST https://auth.calimatic.com/api/v1/users/import \
-H "Content-Type: application/json" \
-H "x-client-id: cca_aBcDeFgHiJkL" \
-H "x-client-secret: ccas_xYzAbCdEfGhI" \
-d '{
"users": [
{
"email": "jane@example.com",
"firstName": "Jane",
"lastName": "Smith",
"passwordHash": "$2b$12$LJ3m4ys3Lk0TSwHiPbUXR.ZaWgmFLfD5hsOqpDumNcBEkb/FumBcG",
"externalId": "usr_123"
},
{
"email": "bob@example.com",
"firstName": "Bob",
"lastName": "Jones",
"passwordHash": "$2b$10$N9qo8uLOickgx2ZMRZoMye.IjqNvnAEOdO7s5X2G3FNGkLm7JRKWG",
"externalId": "usr_456"
}
],
"defaultOrganizationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"sendInviteEmails": false
}'
Important Notes
- The
passwordHashmust be a valid bcrypt string (starting with$2b$or$2a$) or a Keycloak PBKDF2 JSON object - When
passwordHashis provided,sendInviteEmailis automatically treated asfalsefor that user - If both
temporaryPasswordandpasswordHashare provided,temporaryPasswordtakes precedence for setting the Keycloak credential, andpasswordHashis stored in the platform database - This feature is designed for one-time migration -- going forward, users manage their passwords through the auth platform
Common Error Response Format
All error responses follow a consistent format:
{
"success": false,
"error": "Human-readable error message"
}
HTTP Status Codes
| Status | Meaning |
|---|---|
| 200 | Success (existing resource updated) |
| 201 | Created (new resource) |
| 400 | Bad request (missing/invalid parameters) |
| 401 | Unauthorized (no or invalid auth) |
| 403 | Forbidden (insufficient permissions) |
| 404 | Not found (user or org does not exist) |
| 409 | Conflict (user already exists) |
| 422 | Unprocessable entity (validation failure) |
| 500 | Internal server error |