Learn how to migrate users and organizations from Stytch.
The WorkOS API allows you to migrate your existing user data from a variety of existing sources. In this guide, we will walk through the steps to export and import your B2B users, organizations, and enterprise configurations from Stytch.
Stytch allows customers to export organization and member data using their API endpoints. You can export most data programmatically, though password hashes and complete SSO/SCIM connection configurations require contacting Stytch support.
Use the Stytch Search Organizations API to retrieve all organizations in your Stytch project, then use the Stytch Search Members API to export members for each organization. Both endpoints support pagination for projects with more than 1000 records and have a rate limit of 100 requests per minute.
The following exports B2B Users. To export Consumer Users, consult this utility from Stytch.
import { B2BClient } from 'stytch'; import { writeFile } from 'fs/promises'; const client = new B2BClient({ project_id: process.env.STYTCH_PROJECT_ID, secret: process.env.STYTCH_SECRET, }); async function exportOrganizations() { const allOrganizations: any[] = []; let cursor = ''; do { const response = await client.organizations.search({ cursor, limit: 1000, }); allOrganizations.push(...response.organizations); cursor = response.results_metadata.next_cursor; } while (cursor); return allOrganizations; } async function exportMembers(organizationIds: string[]) { const allMembers: any[] = []; let cursor = ''; do { const response = await client.organizations.members.search({ organization_ids: organizationIds, cursor, limit: 1000, }); allMembers.push(...response.members); cursor = response.results_metadata.next_cursor; } while (cursor); return allMembers; } (async () => { const organizations = await exportOrganizations(); const organizationIds = organizations.map((org) => org.organization_id); console.log(`Found ${organizations.length} organizations`); const members = await exportMembers(organizationIds); console.log(`Found ${members.length} members`); // Export all data to a JSON file await writeFile( 'stytch-b2b-export.json', JSON.stringify({ organizations, members }, null, 2), ); })(); 
The Organization object includes organization_id, organization_name, email_allowed_domains, sso_active_connections, and scim_active_connection fields. The Member object includes member_id, email_address, name, status, oauth_registrations, sso_registrations, and roles fields.
If your Stytch members sign in using password-based authentication, you will need to contact Stytch support to export password hashes. After opening a request, they will provide an export of your hashed password data. The timeline for this process can vary.
Stytch uses the scrypt password hashing algorithm for password storage. When you export passwords through Stytch support, verify the hash format they provide, as WorkOS supports multiple algorithms including scrypt, bcrypt, and argon2.
Once you’ve obtained the necessary export data from Stytch, you can import it into WorkOS via the API. We recommend importing organizations first, then users with their organization memberships.
Use the Create Organization API to import each Stytch organization. Map organization_name to name and email_allowed_domains to domainData (with appropriate state values).
import { WorkOS } from '@workos-inc/node'; const workos = new WorkOS(process.env.WORKOS_API_KEY); async function importOrganization(stytchOrg: any) { const domainData = stytchOrg.email_allowed_domains?.map((domain: string) => ({ domain, state: 'pending', // or 'verified' if domains are pre-verified })) || []; const org = await workos.organizations.createOrganization({ name: stytchOrg.organization_name, domainData, }); return org; } // Import all organizations const orgIdMapping = new Map(); for (const stytchOrg of stytchOrganizations) { try { const workosOrg = await importOrganization(stytchOrg); orgIdMapping.set(stytchOrg.organization_id, workosOrg.id); } catch (error: any) { console.error(`[FAILED] ${stytchOrg.organization_name}`, error.message); } } 
Use the Create User API to import each Stytch member, then use the Organization Membership API to link users to their organizations. You should filter members by status – typically only importing active members and potentially re-inviting invited or pending members.
async function importUser(stytchMember: any) { // Parse name into first and last name const nameParts = stytchMember.name?.split(' ') || []; const firstName = nameParts[0] || ''; const lastName = nameParts.slice(1).join(' ') || ''; const user = await workos.userManagement.createUser({ email: stytchMember.email_address, emailVerified: stytchMember.email_address_verified, firstName, lastName, }); return user; } async function createMembership( workosUserId: string, workosOrgId: string, roleSlug: string = 'member', ) { return await workos.userManagement.createOrganizationMembership({ userId: workosUserId, organizationId: workosOrgId, roleSlug, }); } const userIdMapping = new Map(); for (const stytchMember of stytchMembers) { try { const workosUser = await importUser(stytchMember); userIdMapping.set(stytchMember.member_id, workosUser.id); const workosOrgId = orgIdMapping.get(stytchMember.organization_id); if (workosOrgId) { await createMembership(workosUser.id, workosOrgId); } else { console.warn(`No WorkOS org found for ${stytchMember.email_address}`); } } catch (error: any) { console.error(`[FAILED] ${stytchMember.email_address}`, error.message); } } 
User creation is rate-limited. See the rate limits documentation for details. Consider implementing retry logic and batching for large imports, or reach out to support@workos.com to process large datasets in the background.
If you exported password hashes from Stytch, you can import them during user creation or later using the Update User API. Pass the passwordHashType parameter (e.g., 'scrypt', 'bcrypt') and the passwordHash value from your Stytch export. Once imported, users can sign in with their existing passwords without performing a password reset.
const user = await workos.userManagement.createUser({ email: stytchMember.email_address, emailVerified: stytchMember.email_address_verified, firstName, lastName, passwordHash: stytchPasswordHash, // From Stytch support export passwordHashType: 'scrypt', // Verify the actual format with Stytch support }); 
Enterprise SSO connections cannot be exported from Stytch, but you can manually reconfigure connections. You have two options: configure each connection through the dashboard, or use the Admin Portal to let customers self-service their SSO setup. The Admin Portal approach is recommended for larger migrations, as it reduces manual work and provides a familiar self-service experience for IT administrators.
To manually configure a SAML connection, create the organization (if not already created), then set up a new SAML connection in the dashboard.
import { WorkOS } from '@workos-inc/node'; const workos = new WorkOS(process.env.WORKOS_API_KEY); // Generate Admin Portal link for customer to self-configure SSO const { link } = await workos.portal.generateLink({ organization: 'org_12345', intent: 'sso', returnUrl: 'https://yourapp.com/admin', }); // Send this link to the customer's IT administrator console.log('Admin Portal link:', link); 
The OIDC process is similar: create the organization, configure a new OIDC connection, obtain the callback URL, and work with the customer’s IT team to update their OIDC application configuration. See the OIDC integration guide for detailed instructions. For provider-specific guidance, consult the integration guides for Okta SAML, Microsoft Entra ID SAML, Google Workspace SAML, or Generic SAML.
Like SSO connections, SCIM directory sync connections must be reconfigured. Directory Sync provides real-time user and group provisioning, automatic deprovisioning, custom attribute mapping, and support for 20+ identity providers including Okta SCIM, Microsoft Entra ID SCIM, Google Workspace Directory, and Custom SCIM v2.0 providers.
For each Stytch SCIM connection, create a corresponding Directory Sync connection in the dashboard or using the Admin Portal for self-service setup.
const { link } = await workos.portal.generateLink({ organization: 'org_12345', intent: 'dsync', returnUrl: 'https://yourapp.com/admin', }); 
WorkOS sends webhooks for directory sync events like dsync.user.created, dsync.user.updated, dsync.user.deleted, and dsync.group.updated. Configure webhooks in the dashboard to receive these events and keep your application in sync with directory changes.
app.post('/webhooks/workos', async (req, res) => { const event = req.body; switch (event.event) { case 'dsync.user.created': await createUserFromDirectory(event.data); break; case 'dsync.user.deleted': await deactivateUser(event.data.id); break; case 'dsync.group.updated': await syncGroupMemberships(event.data); break; } res.status(200).send(); }); 
See the Directory Sync guide for complete implementation details.
Stytch B2B supports several authentication methods and access control features that have equivalent capabilities in AuthKit, though some migration adjustments are necessary.
Both Stytch and WorkOS support traditional email and password authentication. After importing your users with their password hashes, users can sign in immediately with their existing credentials. Enable password authentication in the dashboard under the Authentication tab and configure password strength requirements as needed.
Stytch’s magic link authentication can be replaced with Magic Auth. While Stytch sends clickable magic links via email, Magic Auth sends a six-digit one-time code that users enter manually. Magic Auth codes expire after 10 minutes and are automatically validated by AuthKit. Stytch’s email OTP authentication is functionally identical to Magic Auth, so no application logic changes are needed.
If your Stytch users sign in through OAuth providers like Google, Microsoft, or GitHub, they can continue using the same providers. After configuring OAuth providers in the dashboard, users can sign in with their social credentials and will be automatically linked to their user account based on email address matching.
In the dashboard:
Users authenticate through AuthKit, which handles the OAuth flow.
const { user } = await workos.userManagement.authenticateWithCode({ code: authorizationCode, clientId: process.env.WORKOS_CLIENT_ID, }); 
Check the integrations page for the complete list of supported OAuth providers, including Google OAuth, Microsoft OAuth, and GitHub OAuth.
Both Stytch and WorkOS support TOTP authenticators like Google Authenticator, Authy, and 1Password. WorkOS supports importing existing TOTP secrets during migration through the developer-provided TOTP secrets feature, which allows users to keep their existing authenticator app configurations without re-enrollment.
However, Stytch cannot export TOTP secrets. Users who have TOTP MFA enrolled in Stytch will need to re-enroll their authenticator apps with WorkOS during their next sign-in. Enable MFA in the dashboard under the Authentication tab and configure whether MFA is optional or required. Communicate this change to your users before migration so they’re prepared to scan a new QR code or enter a new secret key.
While Stytch supports SMS-based MFA, WorkOS does not due to known security vulnerabilities with SMS (SIM swapping, interception, etc.). Users who currently have SMS-based MFA will need to switch to TOTP authenticators, email-based Magic Auth, or modern biometric Passkeys. See the MFA guide for enrollment flows and implementation details.
Stytch B2B provides RBAC with custom resources, roles, and actions. WorkOS offers similar capabilities through roles and permissions. When migrating, identify your roles defined in Stytch, create equivalent roles in the dashboard, and assign roles during migration by specifying the roleSlug parameter when creating organization memberships. If your Stytch implementation uses complex RBAC policies with custom resources and actions, you may need to simplify to standard roles or implement custom authorization in your application logic.
const roleMapping = { stytch_admin: 'admin', stytch_member: 'member', custom_role_123: 'manager', }; await workos.userManagement.createOrganizationMembership({ userId: workosUserId, organizationId: workosOrgId, roleSlug: roleMapping[stytchMember.roles[0].role_id] || 'member', }); 
JWT-based session tokens are used for authentication state. Your application will need to handle these session tokens. Use an SDK to verify, extract user context (user ID, organization ID, role), and implement token refresh logic if using long-lived sessions.
import { WorkOS } from '@workos-inc/node'; const workos = new WorkOS(process.env.WORKOS_API_KEY); const { user, organizationId, role } = await workos.userManagement.authenticateWithCode({ code: authorizationCode, clientId: process.env.WORKOS_CLIENT_ID, }); // Store session in your application req.session.userId = user.id; req.session.organizationId = organizationId; req.session.role = role; 
See the AuthKit guide for complete session management implementation.
Be sure to understand how to handle interim new users for managing users who sign up during your migration process.
After completing your migration to WorkOS, you can take advantage of additional features:
If you haven’t already, check out the AuthKit Quick Start guide to learn how to integrate WorkOS into your application.
For questions or assistance with your migration, contact support@workos.com.