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 three authentication methods:

MethodHeadersUse Case
SessionCookie-based session from Calimatic Auth loginAdmin dashboard, internal tools
API Keyx-api-key: your-api-keyServer-to-server integration
App Clientx-client-id: cca_... + x-client-secret: ccas_...App-initiated provisioning

Most endpoints require the org:users:manage permission.


Endpoints Overview

MethodEndpointDescription
POST/api/v1/organizationsCreate a new organization
GET/api/v1/organizations?slug=<slug>Look up organization by slug
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
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/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