User Management API Reference

Base URL: https://auth.calimatic.com

All 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:

MethodHeadersUse Case
SessionCookie-based session from Calimatic Auth loginAdmin dashboard, internal tools
API Keyx-api-key: your-api-keyServer-to-server integration (org-scoped)
App Client (Basic)x-client-id: cca_... + x-client-secret: ccas_...App-initiated provisioning
OAuth2 BearerAuthorization: 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/token with grant_type=client_credentials to get an access token, then send it as Authorization: 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

MethodEndpointDescription
POST/api/v1/organizationsCreate a new organization
GET/api/v1/organizations?slug=<slug>Look up organization by slug

Org-scoped user management

MethodEndpointDescription
GET/api/v1/organizations/{orgId}/usersList members of an organization
POST/api/v1/organizations/{orgId}/usersInvite a user to an organization (auto-licenses calling app)
POST/api/v1/organizations/{orgId}/users/bulk-inviteBulk 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-passwordSend a password reset email

Global user management

MethodEndpointDescription
POST/api/v1/users/provisionCreate or provision a single user
POST/api/v1/users/provision/bulkBulk provision up to 500 users
POST/api/v1/users/importImport existing users (migration-friendly)
GET/api/v1/users/resolveResolve 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-passwordTrigger password reset email
POST/api/v1/users/{id}/set-passwordSet 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

FieldTypeRequiredDefaultDescription
namestringYes--Organization name. Max 255 characters.
slugstringYes--URL-friendly identifier. Lowercase alphanumeric with hyphens. Max 100 characters.
typestringNo"customer"One of: customer, partner, internal.
domainstringNo--Organization domain (e.g., acme.com). Max 255 characters.
planstringNo"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

ParameterTypeRequiredDescription
slugstringYesThe 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

ParameterTypeDescription
orgIdstring (UUID)The organization to invite the user to.

Request Body

FieldTypeRequiredDefaultDescription
emailstringYes--Valid email address. Lowercased and trimmed.
firstNamestringYes--User's first name. 1-100 characters.
lastNamestringYes--User's last name. 1-100 characters.
applicationsstring[]No[]Additional app licenses to assign. The calling app is auto-included.
sendInviteEmailbooleanNotrueIf true, sends an invitation email with password setup link.

Note: This endpoint takes email/firstName/lastName, not {userId, role}. To resolve an existing user first, use GET /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

StatusErrorDescription
401UnauthorizedMissing or invalid authentication
404Not foundOrganization does not exist
422Validation errorInvalid request body
500Server errorInternal 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

ParameterTypeDefaultDescription
pageinteger1Page number (1-indexed).
limitinteger20Page size. Maximum 100.
searchstring--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

ParameterTypeDescription
userIdstring (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

StatusErrorDescription
401UnauthorizedMissing or invalid authentication
404User not foundNo 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.

FieldTypeDescription
firstNamestring1-100 chars. Mirrored to Keycloak.
lastNamestring1-100 chars. Mirrored to Keycloak.
displayNamestring1-255 chars. Platform-only.
phonestring | nullUp to 50 chars. Pass null to clear.
timezonestringIANA timezone name.
localestringIETF language tag (e.g., en, en-US).
emailVerifiedbooleanMirrored 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

StatusErrorDescription
401UnauthorizedMissing or invalid authentication
404User not foundNo user matches the provided ID
422Validation errorInvalid 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

StatusErrorDescription
401UnauthorizedMissing or invalid authentication
404User not foundNo user matches the provided ID
500Server errorDeletion 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

FieldTypeRequiredDefaultDescription
emailstringYes--Valid email address. Lowercased and trimmed.
firstNamestringYes--User's first name. Max 100 characters.
lastNamestringYes--User's last name. Max 100 characters.
organizationIdstring (UUID)NoRequestor's orgOrganization to add the user to.
rolestringNo"member"Role within the organization. Max 50 characters.
temporaryPasswordstringNo--Temporary password (min 8 chars). User must change on first login.
passwordHashstringNo--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.
applicationsstring[]No[]App licenses to assign. Calling app's license is auto-included.
sendInviteEmailbooleanNotrueSend an invitation email with a password setup link.
externalIdstringNo--External identifier for cross-referencing. Max 255 characters.
metadataobjectNo--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

StatusErrorDescription
401UnauthorizedMissing or invalid authentication
403ForbiddenMissing org:users:manage permission
404ORG_NOT_FOUNDOrganization ID does not exist
409USER_EXISTSUser exists (when deduplication is not desired)
422Validation errorInvalid request body (bad email, missing fields)
500Server errorInternal 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

FieldTypeRequiredDefaultDescription
usersarrayYes--Array of user objects (same schema as single provision). Min 1, max 500.
defaultOrganizationIdstring (UUID)NoRequestor's orgDefault organization for users that do not specify one.
defaultApplicationsstring[]No[]Default app licenses for all users. Calling app is auto-included.
sendInviteEmailsbooleanNofalseSend invitation emails to all users.
skipExistingbooleanNotrueIf true, update existing users instead of failing.

Each user object in the users array supports:

FieldTypeRequiredDescription
emailstringYesValid email address
firstNamestringYesMax 100 characters
lastNamestringYesMax 100 characters
organizationIdstring (UUID)NoOverride default org
rolestringNoOverride default role
temporaryPasswordstringNoMin 8 characters
passwordHashstringNoExisting bcrypt/PBKDF2 hash for seamless migration.
applicationsstring[]NoOverride default apps
externalIdstringNoExternal identifier
metadataobjectNoCustom 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

SettingBulk Provision DefaultImport Default
sendInviteEmailsfalsefalse
skipExistingtruetrue

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

ParameterTypeRequiredDescription
emailstringOne of email or keycloakIdUser's email address
keycloakIdstringOne of email or keycloakIdUser'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

StatusErrorDescription
400Missing parameterNeither email nor keycloakId provided
401UnauthorizedMissing or invalid authentication
404User not foundNo user matches the provided email or Keycloak ID
500Server errorInternal 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

ParameterTypeDescription
userIdstring (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_reset webhook 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

ParameterTypeDescription
userIdstring (UUID)The platform user identity ID

Request Body

FieldTypeRequiredDescription
temporaryPasswordstringYesTemporary 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

StatusErrorDescription
401UnauthorizedMissing or invalid authentication
403ForbiddenMissing org:users:manage permission
422Validation errorPassword does not meet requirements
500Server errorFailed to set password in Keycloak

Side Effects

  • A user.password_reset webhook event is dispatched (with type: "temporary_password_set")
  • An audit log entry is created
  • Keycloak marks the user with UPDATE_PASSWORD required 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

FormatExample PrefixNotes
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

  1. Pass the user's existing password hash in the passwordHash field during provisioning
  2. The hash is stored directly in the platform database
  3. No invite email is sent (regardless of sendInviteEmail setting)
  4. The user can log in immediately using their existing password
  5. 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 passwordHash must be a valid bcrypt string (starting with $2b$ or $2a$) or a Keycloak PBKDF2 JSON object
  • When passwordHash is provided, sendInviteEmail is automatically treated as false for that user
  • If both temporaryPassword and passwordHash are provided, temporaryPassword takes precedence for setting the Keycloak credential, and passwordHash is 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

StatusMeaning
200Success (existing resource updated)
201Created (new resource)
400Bad request (missing/invalid parameters)
401Unauthorized (no or invalid auth)
403Forbidden (insufficient permissions)
404Not found (user or org does not exist)
409Conflict (user already exists)
422Unprocessable entity (validation failure)
500Internal server error