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 three 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 |
| App Client | x-client-id: cca_... + x-client-secret: ccas_... | App-initiated provisioning |
Most endpoints require the org:users:manage permission.
Endpoints Overview
| Method | Endpoint | Description |
|---|---|---|
POST | /api/v1/organizations | Create a new organization |
GET | /api/v1/organizations?slug=<slug> | Look up organization by slug |
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 |
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/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 |