Clerk Migration Guide

This guide covers how to migrate your Clerk users, organizations, and roles to Descope.

There are two main paths to migrate from Clerk to Descope:

  • Full Migration - Export your users (and organizations, roles, and metadata) from Clerk, import them into Descope, and cut over.
  • JIT Migration - Provision users in Descope one at a time as they sign in, without a bulk export.

You can also combine either path with real-time sync via webhooks during a phased rollout, and migrate Enterprise SSO connections for tenant-scoped SAML/OIDC.

Clerk vs. Descope Terminology

Clerk and Descope model identity in similar ways, so most of this migration is a direct mapping.

Clerk ConceptDescope EquivalentDescription
UserUserA person's identity record.
OrganizationTenantClerk Organizations map to Descope Tenants. Both scope users, roles, and, optionally, SSO to a group.
Organization Role (org:admin, org:member, custom roles)RoleClerk's built-in and custom organization roles map to Descope Roles.
Organization Permission (org:<feature>:<permission>)PermissionClerk permissions are tied to a role and a "Feature". Descope permissions are tied to a role.
Public / Private / Unsafe MetadataCustom AttributesArbitrary key-value data on the user record. Descope doesn't distinguish frontend-writable metadata from backend-only metadata the way Clerk does. Enforce that distinction in your own application logic.
External Account (social login)OAuth Login IDA linked Google/GitHub/etc. identity.
Enterprise Connection (org-scoped SAML/OIDC)Tenant SSO ConnectionBoth scope an IdP connection to a specific organization/tenant.
Session Token (JWT)Session Token (JWT)Both issue short-lived JWTs; each has its own issuer, claims, and JWKS endpoint.

Full Migration

Prerequisites

Step 1: Export Users from Clerk

You have two options, depending on whether you need password hashes and how much filtering/automation you need.

Option A: Dashboard CSV export (recommended for most migrations)

  1. In the Clerk Dashboard, go to instance Settings → User Exports.
  2. Click Export all users. This generates a downloadable CSV that includes each user's profile data and bcrypt password hash, with no support ticket required.
  3. Clerk logs every export in the download history so you can audit who ran it and when.

Option B: Backend API

Use GET /v1/users if you need to filter (by organization, creation date, etc.) or automate the export:

curl -s "https://api.clerk.com/v1/users?limit=500&offset=0" \
  -H "Authorization: Bearer ${CLERK_SECRET_KEY}"
  • Paginate with limit (max 500, default 10) and offset; Clerk doesn't support cursor-based pagination on this endpoint.
  • Useful filters: email_address[], phone_number[], organization_id[], created_at_before/created_at_after.
  • Rate limits: 1000 requests / 10 seconds on production instances, 100 requests / 10 seconds on development instances. Back off on 429 responses using the Retry-After header.

Note

The Backend API User object doesn't include the password hash; only the Dashboard CSV export does. If you need both bulk filtering and passwords, export via the API for user data and cross-reference against the CSV for hashes (matched by id).

Step 2: Map Clerk Fields to Descope

Clerk FieldDescope Field
email_addresses[].email_address (primary)email / loginIds
phone_numbers[].phone_number (primary)phone
usernameloginIds (if used as the primary identifier)
first_name, last_namegivenName, familyName
idcustomAttributes.clerkUserId (keep for traceability and idempotent re-runs)
external_id (your own legacy ID, if set)externalIds
public_metadata, private_metadata, unsafe_metadatacustomAttributes
created_atcreatedTime

Step 3: Import Passwords

Clerk always hashes passwords with bcrypt, so you only need to handle one format here, unlike providers that carry several legacy hash schemes:

"hashedPassword": {
  "bcrypt": {
    "hash": "$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy"
  }
}

See the Custom Data Store guide for the full request format and other supported algorithms, in case some users were originally migrated into Clerk from a system using a different hash.

Step 4: Handle MFA and Passkeys

  • TOTP / backup codes: Clerk's export doesn't expose raw TOTP secrets or backup codes either: totpEnabled and backupCodeEnabled are booleans, not the underlying values. If you have the raw secret from a system prior to Clerk, pass it as seed in Descope's Batch Create Users API so users keep using their existing authenticator app. Otherwise, users need to re-enroll TOTP in Descope.
  • Passkeys: Clerk doesn't expose passkey public-key credential data for export, so passkey users need to enroll a new passkey in Descope after migration.

Step 5: Migrate Social Logins

For each entry in a user's external_accounts[], note the provider (e.g. google) and provider_user_id. As long as the user's Descope login ID matches what they'd sign in with (their email, in most cases), social login continues to work with no separate re-import step. Descope links the identity the next time the user authenticates through that provider.

Step 6: Migrate Organizations to Tenants

  1. List organizations: GET /v1/organizations.
  2. List memberships per organization: GET /v1/organizations/{organization_id}/memberships. Each membership includes a role (org:admin, org:member, or a custom role) and any directly-granted permissions.
  3. Create the matching tenant: Create Tenant in Descope, using the Clerk organization id or slug as the tenant ID for a stable mapping.
  4. Create roles: Recreate Clerk's custom organization roles as Descope Roles. Clerk caps custom roles at 10 per instance, making this a small, one-time mapping exercise.
  5. Role Assignment: Assign a tenant + role to each user via userTenants on Create User or Batch Create Users:
"userTenants": [
  { "tenantId": "acme-corp", "roleNames": ["admin"] }
]

Step 7: Import Users into Descope

Use the Batch Create Users API for bulk import. A few things worth doing on the way in:

  • Set a freshlyMigrated: true custom attribute so you can branch on it in a Descope Flow (prompt for MFA re-enrollment, verify contact info, etc.) and clear it once onboarding finishes. See the Auth0 guide for a worked example of this pattern.
  • Set createdTime from Clerk's created_at so long-time users don't look like brand-new signups.
{
  "users": [
    {
      "loginId": "user@example.com",
      "email": "user@example.com",
      "verifiedEmail": true,
      "givenName": "Ada",
      "familyName": "Lovelace",
      "hashedPassword": {
        "bcrypt": {
          "hash": "$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy"
        }
      },
      "userTenants": [
        { "tenantId": "acme-corp", "roleNames": ["admin"] }
      ],
      "customAttributes": {
        "clerkUserId": "user_2abcXYZ",
        "freshlyMigrated": "true"
      },
      "createdTime": 1700000000
    }
  ]
}

Step 8: Verify and Cut Over

  1. Check Users, Roles, and Tenants in the Descope console to confirm the import matches Clerk's counts.
  2. Swap the Clerk SDK for the matching Descope SDK in your app and test sign-in with a few migrated accounts.
  3. Once you're confident, retire the Clerk integration.

JIT Migration

Note

Clerk doesn't expose a server-side password-verification endpoint you can call from your own backend. Credential verification stays inside Clerk's own SDKs and Frontend API. JIT for Clerk works by verifying a user's already-authenticated Clerk session server-side, not by checking a password.

This approach works well if you can run both SDKs side by side for a while, for example during a rollout where some users are still on an app version with Clerk initialized:

  1. On a user's next visit, while your app still has the Clerk SDK active, capture their current Clerk session.
  2. Send the request to your backend and verify it there using Clerk's backend SDK (authenticateRequest()) or by validating the session JWT against Clerk's JWKS (GET /v1/jwks, no rate limit).
  3. Once verified, pull the user's profile via the Backend API and create (or link to) the matching user in Descope with Create User, marking their email/phone as already verified.
  4. Complete sign-in through your normal Descope authentication method for that user going forward.
  5. After you provision a user this way, their later logins go straight to Descope with no further Clerk involvement.

This trades the zero-re-login experience of a password-based JIT flow, which Clerk doesn't support, for a still-seamless one: users never re-enter credentials, but they do need an app version that can still reach Clerk during the transition window.

Real-Time Sync with Webhooks

If you're doing a phased rollout rather than a single cutover, Clerk's webhooks can keep Descope up to date in real time:

// Example: sync a Clerk user.created webhook to Descope
app.post('/webhooks/clerk', async (req, res) => {
  const evt = clerkWebhooks.verify(req); // verify Svix signature
  if (evt.type === 'user.created') {
    await descopeClient.management.user.create(
      evt.data.email_addresses[0].email_address,
      { customAttributes: { clerkUserId: evt.data.id } }
    );
  }
  res.sendStatus(200);
});

This keeps both systems in sync for any users who sign up (or update their profile) between your bulk export and final cutover.

Migrating Enterprise SSO Connections

If any of your Clerk Organizations use Enterprise Connections (SAML or OIDC scoped to that organization), recreate them as Descope Tenant SSO connections (OIDC variant) on the matching tenant from Step 6.

  • Reuse the same client ID/secret (OIDC) or IdP metadata (SAML) where possible, adding Descope's redirect/ACS URL to the IdP's allow-list.
  • If you'd rather not ask each tenant admin to reconfigure their IdP at all, see the SSO Migration guide for consuming the existing IdP response without changing the ACS URL or Entity ID.

Additional Considerations

  • Passkeys and TOTP/backup codes are not portable from Clerk - see Step 4. Plan for re-enrollment as part of your post-migration onboarding flow.
  • Bulk export rate limits - Clerk caps production instances at 1000 requests per 10 seconds on the Backend API. Large user bases should paginate with backoff instead of firing many requests in parallel.
  • Transitional token validation - if your API needs to accept requests from both migrated and not-yet-migrated users, validate Clerk-issued JWTs against Clerk's JWKS (GET /v1/jwks) for the old cohort and Descope tokens via the Descope SDK for the new one, routing based on the token's issuer.
Was this helpful?

On this page