Importing Existing Users
Guide for third-party applications that already have users and need to import them into the Calimatic Auth platform.
This guide is for Category 4 apps: existing third-party applications that have their own user database and want to bring those users into the Calimatic Auth platform for centralized identity management.
Prerequisites
- A registered app client with
org:users:managepermission (see Integrate Third-Party) - An organization created in Calimatic Auth for your users
- Access to your existing user database for data export
- User data exported in JSON format
1. Prepare User Data
Required Fields
Each user record must include these fields:
| Field | Type | Required | Max Length | Description |
|---|---|---|---|---|
email | string | Yes | 255 | Valid email address. Used as the unique identifier for deduplication. |
firstName | string | Yes | 100 | User's first name. |
lastName | string | Yes | 100 | User's last name. |
organizationId | string (UUID) | No | -- | Override the default organization for this user. |
role | string | No | 50 | Role within the organization (e.g., member, admin). Defaults to member. |
temporaryPassword | string | No | 128 | Min 8 characters. If set, user must change on first login. |
passwordHash | string | No | 1024 | Existing bcrypt or PBKDF2 hash. Enables seamless migration -- users log in with existing password. |
applications | string[] | No | -- | App licenses to assign. Defaults to the calling app's license. |
externalId | string | No | 255 | Your app's internal user ID. Stored for cross-referencing. |
metadata | object | No | -- | Arbitrary key-value pairs for custom data. |
Data Format
Structure your import payload as a JSON object:
{
"users": [
{
"email": "jane@example.com",
"firstName": "Jane",
"lastName": "Smith",
"role": "admin",
"externalId": "usr_12345",
"metadata": {
"legacyPlan": "premium",
"signupDate": "2023-06-15"
}
},
{
"email": "bob@example.com",
"firstName": "Bob",
"lastName": "Jones",
"role": "member",
"externalId": "usr_12346"
}
],
"defaultOrganizationId": "your-org-uuid",
"defaultApplications": ["your-app-name"],
"sendInviteEmails": false,
"skipExisting": true
}
Data Cleaning Checklist
Before importing, clean your data:
- Remove duplicates: Deduplicate by email address. The import API deduplicates for you (
skipExisting: true), but cleaning first avoids unnecessary API calls. - Validate emails: Remove records with invalid or empty email addresses.
- Normalize emails: Lowercase and trim all email addresses.
- Trim names: Remove leading/trailing whitespace from names. Ensure no empty names.
- Map roles: Convert your internal role identifiers to Calimatic role names.
- Preserve IDs: Store your internal user IDs in the
externalIdfield for cross-referencing.
Example: Transform CSV to Import Format
import fs from "fs";
import { parse } from "csv-parse/sync";
const csv = fs.readFileSync("users.csv", "utf-8");
const records = parse(csv, { columns: true, skip_empty_lines: true });
const ROLE_MAP: Record<string, string> = {
"admin": "org_admin",
"user": "member",
"viewer": "member",
};
const importPayload = {
users: records
.filter((r: any) => r.email && r.email.includes("@"))
.map((r: any) => ({
email: r.email.toLowerCase().trim(),
firstName: r.first_name?.trim() || "Unknown",
lastName: r.last_name?.trim() || "User",
role: ROLE_MAP[r.role] || "member",
externalId: r.id,
metadata: {
legacyRole: r.role,
createdAt: r.created_at,
},
})),
defaultOrganizationId: "your-org-uuid",
sendInviteEmails: false,
skipExisting: true,
};
fs.writeFileSync("import-payload.json", JSON.stringify(importPayload, null, 2));
console.log(`Prepared ${importPayload.users.length} users for import.`);
2. Using the Import API
Endpoint
POST /api/v1/users/import
The Import API is an alias for bulk provisioning with migration-friendly defaults:
sendInviteEmailsdefaults tofalseskipExistingdefaults totrue
Authentication
Authenticate with app client credentials or an API key:
# App client credentials
-H "x-client-id: cca_your_client_id"
-H "x-client-secret: ccas_your_client_secret"
# OR API key
-H "x-api-key: your-api-key"
Execute the Import
curl -X POST https://auth.calimatic.com/api/v1/users/import \
-H "Content-Type: application/json" \
-H "x-client-id: cca_your_client_id" \
-H "x-client-secret: ccas_your_client_secret" \
-d @import-payload.json
const response = await fetch("https://auth.calimatic.com/api/v1/users/import", {
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(importPayload),
});
const result = await response.json();
console.log(result.data);
Response
{
"success": true,
"data": {
"total": 150,
"created": 142,
"updated": 5,
"skipped": 0,
"failed": 3,
"message": "Import complete: 142 created, 5 updated, 3 failed",
"errors": [
{
"email": "bad-email",
"error": "Must be a valid email address",
"index": 23
},
{
"email": "duplicate@example.com",
"error": "Organization not found: invalid-uuid",
"index": 87
}
],
"users": [
{
"email": "jane@example.com",
"userId": "a1b2c3d4-...",
"status": "user_created"
},
{
"email": "existing@example.com",
"userId": "e5f6g7h8-...",
"status": "existing_user_updated"
}
]
}
}
Batch Large Imports
The API accepts up to 500 users per request. For larger datasets, batch your imports:
const BATCH_SIZE = 500;
async function importAllUsers(users: any[], orgId: string) {
const allResults = { created: 0, updated: 0, failed: 0, errors: [] as any[] };
for (let i = 0; i < users.length; i += BATCH_SIZE) {
const batch = users.slice(i, i + BATCH_SIZE);
const batchNumber = Math.floor(i / BATCH_SIZE) + 1;
const totalBatches = Math.ceil(users.length / BATCH_SIZE);
console.log(`Batch ${batchNumber}/${totalBatches}: ${batch.length} users`);
const response = await fetch("https://auth.calimatic.com/api/v1/users/import", {
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({
users: batch,
defaultOrganizationId: orgId,
sendInviteEmails: false,
skipExisting: true,
}),
});
const result = await response.json();
allResults.created += result.data.created;
allResults.updated += result.data.updated;
allResults.failed += result.data.failed;
allResults.errors.push(...result.data.errors);
// Wait between batches to respect rate limits
await new Promise((resolve) => setTimeout(resolve, 2000));
}
return allResults;
}
3. Deduplication by Email
When skipExisting is true (the default for the import endpoint), the API deduplicates by email address:
- If a user with the same email already exists in Calimatic Auth, the existing user is updated (added to the organization if not already a member) rather than creating a duplicate.
- The existing user's
keycloakUserIdand profile are preserved. - App licenses are assigned if not already present.
Behavior Summary
| Scenario | skipExisting: true | skipExisting: false |
|---|---|---|
| Email not found | Create new user | Create new user |
| Email exists, different org | Add org membership | Add org membership |
| Email exists, same org | Update (no-op) | Update (no-op) |
This makes the import API idempotent -- you can safely re-run the same import without creating duplicates.
4. Handling Password Migration
Option A: Password Hash Migration (Recommended)
If your app uses bcrypt for password hashing, migrate the hashes directly. Users log in immediately with their existing passwords -- no reset email needed, no friction.
{
"users": [
{
"email": "jane@example.com",
"firstName": "Jane",
"lastName": "Smith",
"passwordHash": "$2b$12$LJ3m4ys3Lk0TSwHiPbUXR.ZaWgmFLfD5hsOqpDumNcBEkb/FumBcG",
"externalId": "usr_12345"
}
],
"defaultOrganizationId": "your-org-uuid",
"sendInviteEmails": false
}
Supported formats:
- bcrypt (
$2b$,$2a$) -- stored and verified as-is - PBKDF2 (Keycloak JSON format) -- automatically upgraded to bcrypt on first login
When passwordHash is provided, sendInviteEmail is automatically treated as false for that user.
Option B: Password Reset Emails
Import users without passwords, then send password reset emails. Users set their own password on the Calimatic Auth platform:
// Step 1: Import without passwords
const importResult = await importUsers(users, orgId);
// Step 2: Send password reset emails for successfully created users
for (const user of importResult.users) {
if (user.status === "user_created") {
await fetch(
`https://auth.calimatic.com/api/v1/users/${user.userId}/reset-password`,
{
method: "POST",
headers: {
"x-client-id": process.env.CALIMATIC_CLIENT_ID!,
"x-client-secret": process.env.CALIMATIC_CLIENT_SECRET!,
},
}
);
// Throttle to avoid overwhelming the email service
await new Promise((resolve) => setTimeout(resolve, 500));
}
}
Option C: Temporary Passwords
Set a temporary password for each user during import. Useful when you want users to log in immediately but still force a password change:
{
"users": [
{
"email": "jane@example.com",
"firstName": "Jane",
"lastName": "Smith",
"temporaryPassword": "Welcome2024!"
}
]
}
Users will be prompted to change the temporary password on first login.
You can also set a temporary password after import:
curl -X POST https://auth.calimatic.com/api/v1/users/{userId}/set-password \
-H "Content-Type: application/json" \
-H "x-client-id: cca_your_client_id" \
-H "x-client-secret: ccas_your_client_secret" \
-d '{ "temporaryPassword": "Welcome2024!" }'
Option D: SSO (No Passwords Needed)
If users already authenticate via Google Workspace, Microsoft 365, or another identity provider, configure SSO for the organization. Users sign in with their existing credentials -- no password migration is required.
5. Organization and Role Mapping
Single Organization
If all your users belong to one organization, use defaultOrganizationId:
{
"users": [...],
"defaultOrganizationId": "your-org-uuid"
}
Multiple Organizations
If users belong to different organizations, specify organizationId per user:
{
"users": [
{
"email": "jane@acme.com",
"firstName": "Jane",
"lastName": "Smith",
"organizationId": "acme-org-uuid",
"role": "admin"
},
{
"email": "bob@widgets.com",
"firstName": "Bob",
"lastName": "Jones",
"organizationId": "widgets-org-uuid",
"role": "member"
}
]
}
Role Mapping
Map your internal roles to Calimatic role names. Common mappings:
| Your Role | Calimatic Role | Description |
|---|---|---|
| Super Admin | org_owner | Full organization control |
| Admin | org_admin | User and settings management |
| Manager | org_manager | Limited admin capabilities |
| User / Staff | member | Standard member |
| Teacher | edtech_teacher | EdTech-specific teacher role |
| Student | edtech_student | EdTech-specific student role |
| Parent | edtech_parent | EdTech-specific parent role |
If no role is specified, users default to member.
6. Verifying Imported Users
Check Individual Users
Use the resolve endpoint to verify a specific user was imported correctly:
curl "https://auth.calimatic.com/api/v1/users/resolve?email=jane@example.com" \
-H "x-client-id: cca_your_client_id" \
-H "x-client-secret: ccas_your_client_secret"
Response:
{
"success": true,
"data": {
"user": {
"id": "a1b2c3d4-...",
"keycloakUserId": "kc-uuid-...",
"email": "jane@example.com",
"firstName": "Jane",
"lastName": "Smith",
"status": "active",
"isActive": true,
"source": "provisioning",
"createdAt": "2025-01-15T10:30:00.000Z"
},
"organizations": [
{
"id": "org-uuid-...",
"name": "Acme Corp",
"membershipRole": "admin",
"isPrimary": true
}
],
"licenses": [
{
"application": "your-app-name",
"organizationId": "org-uuid-...",
"assignedAt": "2025-01-15T10:30:00.000Z",
"source": "provisioning"
}
],
"hasLicense": true
}
}
Check Migration Status
For an overview of all imported users:
curl https://auth.calimatic.com/api/v1/tools/migration-status \
-H "Cookie: your-session-cookie"
This returns counts by source (users with source: "provisioning" are your imported users), status, email verification rates, and login activity.
Verification Checklist
- Total imported count matches expected count
- No failed imports (check
errorsarray in response) - Users are assigned to the correct organization
- Roles are mapped correctly
- App licenses are assigned (check
hasLicensein resolve response) -
externalIdvalues are preserved for cross-referencing - At least one user can successfully log in
7. Setting Up Ongoing Sync via Webhooks
After the initial import, keep your app in sync with Calimatic Auth using webhooks.
Register a Webhook
curl -X POST https://auth.calimatic.com/api/v1/admin/webhooks \
-H "Content-Type: application/json" \
-H "x-client-id: cca_your_client_id" \
-H "x-client-secret: ccas_your_client_secret" \
-d '{
"url": "https://myapp.com/webhooks/calimatic",
"events": [
"user.created",
"user.updated",
"user.deleted",
"user.suspended",
"user.deactivated",
"user.reactivated",
"license.assigned",
"license.revoked"
],
"secret": "whsec_your_secret_at_least_16_chars",
"description": "Sync user lifecycle events to MyApp"
}'
Handle Events in Your App
app.post("/webhooks/calimatic", async (req, res) => {
// Verify signature (see Webhooks API reference)
const signature = req.headers["x-webhook-signature"] as string;
if (!verifySignature(req.body, signature, WEBHOOK_SECRET)) {
return res.status(401).send("Invalid signature");
}
const { event, data } = req.body;
switch (event) {
case "user.created":
await createLocalUser(data.userId, data.email, data.firstName, data.lastName);
break;
case "user.updated":
await updateLocalUser(data.userId, data);
break;
case "user.suspended":
case "user.deactivated":
await deactivateLocalUser(data.userId);
break;
case "user.reactivated":
await reactivateLocalUser(data.userId);
break;
case "license.revoked":
await revokeLocalAccess(data.userId, data.application);
break;
}
res.status(200).send("OK");
});
Webhook Reliability
- Webhooks retry up to 3 times with increasing delays (1s, 5s, 30s)
- After 10 consecutive failures, the webhook is automatically disabled
- Check delivery history via
GET /api/v1/admin/webhooks/{id}/deliveries - Re-enable a disabled webhook via
PATCH /api/v1/admin/webhooks/{id}with{ "isActive": true }
For the full webhook API reference, see Webhooks API.
Related Documentation
- User Management API -- Full API reference for provisioning, import, and user management endpoints
- Webhooks API -- Webhook registration, event types, and signature verification
- User Provisioning Guide -- Conceptual overview of provisioning methods and organization membership
- Integrate Third-Party -- Setting up OIDC authentication for your app