<!-- llms.txt: https://workos.com/llms.txt -->
<!-- feedback.md: https://workos.com/docs/feedback.md -->

> Agents: if this page is wrong, missing a step, or its code fails, report it instead of working around it. `POST https://api.workos.com/feedback` with `Content-Type: application/json` and body `{"message":"Agent report: what you tried, what happened, client and model","pageUrl":"https://workos.com/docs/migrate/other-services"}`. Ask the user once first. No auth. No secrets. Details: https://workos.com/docs/feedback.md

# Migrate from other services

## Introduction

The WorkOS AuthKit API allows you to migrate your existing user data from a variety of sources. In this guide, we'll walk through the steps to export, and then import users from your own data store.

## (1) Exporting data

While moving authentication related metadata to WorkOS, most applications will continue to store certain user information in their data store. This common subset of data will usually be the following:

| Field               | Description                                                                                                                    | Status   |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------ | -------- |
| Email               | The user's email address. Used for various authentication and verification purposes.                                           | Required |
| First Name          | The user's first, or given name.                                                                                               | Optional |
| Last Name           | The user's last, or family name.                                                                                               | Optional |
| Verification Status | The user's email verification status if they have gone through a verification flow. Assumed as "not verified" unless supplied. | Optional |
| Password            | The user's password hash, if they use password-based authentication.                                                           | Optional |

While preparing the migration, you'll want to ensure this information is programmatically available for use in the import step, this can mean:

- Exporting the relevant data to a file such as JSON or CSV.
- Allowing the data to be queried from the data store directly.

After the data is accessible, we can configure the import.

***

## (2) Importing Users into WorkOS

Now that the User data is available, we can import it into WorkOS.

> For migrations involving more than 200,000 users or organizations, contact [support@workos.com](mailto:support@workos.com) to coordinate a managed import.

### Before you begin: disable webhook delivery

If you have [webhook endpoints](https://workos.com/docs/reference/webhooks) configured, temporarily disable delivery for the duration of the bulk import to avoid overwhelming your webhook consumers with high volumes of `user.created`, `organization.created`, and `organization_membership.created` events.

You can disable a webhook endpoint using the [Update Webhook Endpoint API](https://workos.com/docs/reference/webhooks/update):

```bash
curl -X PATCH https://api.workos.com/webhook_endpoints/{id} \
  -H "Authorization: Bearer sk_example_123456789" \
  -H "Content-Type: application/json" \
  -d '{ "status": "disabled" }'
```

Re-enable the endpoint after the import is complete by setting `status` back to `"enabled"`.

### Using the WorkOS migrations CLI

The WorkOS CLI includes a migrations tool that can import users from a CSV file:

```bash
npx workos migrations import --csv users.csv
```

For a guided, interactive experience:

```bash
npx workos migrations wizard
```

The CLI handles batching and rate limiting automatically.

### Creating users

For each of your users, you can call the WorkOS [Create User API](https://workos.com/docs/reference/authkit/user/create). This will create a matching [User object](https://workos.com/docs/reference/authkit/user) within WorkOS.

A successful response will include a new WorkOS user ID, most apps will want to persist this WorkOS user ID alongside the application-local user object.

```json
{
  "object": "user",
  /* highlight-start */
  "id": "user_01E4ZCR3C56J083X43JQXF3JK5",
  /* highlight-end */
  "email": "marcelina.davis@gmail.com",
  "firstName": "Marcelina",
  "lastName": "Davis",
  "emailVerified": true,
  "createdAt": "2021-06-25T19:07:33.155Z",
  "updatedAt": "2021-06-25T19:07:33.155Z"
}
```

> **Email addresses are unique** to each WorkOS environment. If you have a subset of users already in WorkOS, you may need to handle constraint violation errors.

There are now several options on how to proceed, depending on your application's needs:

### Importing passwords

If your users currently use password-based authentication, you can import existing password hashes during the [users creation](https://workos.com/docs/reference/authkit/user/create) process, or later using the WorkOS [Update User API](https://workos.com/docs/reference/authkit/user/update).

WorkOS currently supports the following password hashing algorithms:

- `bcrypt`
- `scrypt`
- `firebase-scrypt`
- `ssha`
- `ssha256`
- `pbkdf2`
- `argon2`

For `scrypt` and `pbkdf2` passwords, use the PHC string format.
The hash and salt should be B64 encoded: trim the `=` characters that represent Base64 padding. Using a PHC-formatting library, like
Node's [`@phc/format`](https://www.npmjs.com/package/@phc/format), should handle this for you.

The following table shows how to map the `scrypt` and `pbkdf2` parameters to the PHC parameters.

#### scrypt

| `Scrypt` value    |     | PHC hash parameter |
| ----------------- | --- | ------------------ |
| `key length`      | →   | `kl`               |
| `cost`            | →   | `n`                |
| `rounds`          | →   | `r`                |
| `parallelization` | →   | `p`                |

A valid `scrypt` PHC formatted string looks like this:

```txt
$scrypt$v=1$n=16384,r=8,p=1,kl=64$Swhqd4iUYTtWfbCYIPeuMw$q7pfdBQMJujd5FX/qX+ozM2O6aNqP+mo1ZnHGH15XM2vlhroQfPA037UpbdfpH4H66OrSPjsUhfkAMuNoBiQvw
```

#### pbkdf2

| `pbkdf2` value |     | PHC hash parameter |
| -------------- | --- | ------------------ |
| `digest`       | →   | `d`                |
| `iterations`   | →   | `i`                |

For `pbkdf2` allowed values for digest are `sha1`, `sha256`, or `sha512`. The value for iterations is dependent on digest. For `sha256` there is a minimum of 600,000 iterations and a max of 1,000,000. For `sha512` there is a minimum of 210,000 and a max of 1,000,000. For `sha1` there is a minimum of 1,000 iterations and a max of 1,000,000, and the derived key must be between 20 and 36 bytes long.

A valid `pbkdf2` PHC formatted string looks like this:

```txt
$pbkdf2$i=600000,d=sha256$T2ptRFh6MXhDQVh2SWZuUGdpQXBUTg$xXiyTisD7390NijyCv5ICMhFW4eDuMlzypRoLGLyIvA
```

`sha1` iteration counts in the field are far below the [OWASP recommendation](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#pbkdf2) of 1,300,000, so `pbkdf2` hashes using the `sha1` digest are not stored as provided. They are re-hashed with bcrypt at import, which strengthens the stored hash without requiring your users to reset their passwords. Sign-in is unaffected: users continue to authenticate with their existing passwords.

#### argon2

| `argon2` value |     | PHC hash parameter |
| -------------- | --- | ------------------ |
| `variant`      | →   | algorithm id       |
| `version`      | →   | `v`                |
| `memory`       | →   | `m`                |
| `time`         | →   | `t`                |
| `parallelism`  | →   | `p`                |

The variant should be `argon2id`, but older supported variants include `argon2d` and `argon2i`. The version must be `19`. The following memory, time (iterations), and parallelism settings are based on [OWASP recommendations](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#argon2id). Memory is specified in KiB with a minimum of 4,096 KiB (4 MiB) and maximum of 262,144 KiB (256 MiB). For time, there is a minimum of 1 iteration and a maximum of 5 iterations, except for `argon2i` which has a minimum of 3 iterations. Parallelism ranges from 1 to 8 threads. If your requirements fall outside of these guidelines, please [contact support](mailto:support@workos.com).

A valid `argon2` PHC formatted string looks like this:

```txt
$argon2id$v=19$m=65536,t=3,p=4$c29tZXNhbHQ$RdescudvJCsgt3ub+b+dWRWJTmaaJObG
```

For `firebase-scrypt` passwords, refer to the [Firebase Migration guide](https://workos.com/docs/migrate/firebase) for an example of how to format the `password_hash`.

For `ssha` passwords, use the following algorithm:

1. Generate a `salt`: random bytes
2. Hash the user's password and the `salt` using the SHA1 algorithm
3. Base64 encode the hash followed by the salt
4. Prepend the string with `{SSHA}`

A high-level representation is: `{SSHA}base64(sha1(password + salt) + salt)`.

For `ssha256` passwords, use the same algorithm as `ssha` but with SHA256:

1. Generate a `salt`: random bytes
2. Hash the user's password and the `salt` using the SHA256 algorithm
3. Base64 encode the hash followed by the salt
4. Prepend the string with `{SSHA256}`

A high-level representation is: `{SSHA256}base64(sha256(password + salt) + salt)`.

By default, `ssha256` digests are verified as `sha256(password + salt)` — the salt appended to the password. If your legacy system placed the salt before the password instead (`sha256(salt + password)`), pass `password_salt_position: "prefix"` alongside `password_hash_type` when importing. The string format above is unchanged: the salt still follows the digest inside the base64 payload, and only the digest computation differs.

A mismatched salt position cannot be detected at import time — it surfaces later as failed sign-ins — so always confirm that a test account can sign in with its existing password after importing.

Once imported, users can continue to sign-in with their existing password, **without** having to go through a password reset flow.

### Triggering password resets

If you are unable to export passwords from your existing data store, whether for security reasons or other limitations, you can programmatically trigger a password reset flow using the WorkOS [Password Reset API](https://workos.com/docs/reference/authkit/password-reset).

This process can be initiated at any time, and doesn't need to happen during the user import process.

Some applications may want to remove password-based authentication when switching to WorkOS, in favor of another method like Magic Auth. If this is the case for your application, you can skip dealing with passwords entirely.

### Migrating social auth users

If you have users who previously signed in using social auth providers, such as [Google](https://workos.com/docs/integrations/google-oauth) or [Microsoft](https://workos.com/docs/integrations/microsoft-oauth), those users can continue to sign in with those providers after you've migrated to WorkOS.

Check out our [integrations](https://workos.com/docs/integrations) page for guidance on configuring the relevant provider's client credentials in WorkOS.

After your provider is configured in WorkOS, users can sign in with their provider credentials and will be automatically linked to a WorkOS user. WorkOS uses the **email address** from the social auth provider to determine this match.

> Some users may need to verify their email address through WorkOS if email verification is enabled in your WorkOS environment's authentication settings.

Email verification behavior varies depending on whether the provider is known to verify email addresses. For example, users signing in using Google OAuth and a `gmail.com` email domain will not need to perform the extra verification step.

***

## (3) Enterprise Connections (SSO)

If your application uses enterprise SAML or OIDC connections for SSO, you can migrate them to WorkOS. The migration is almost entirely self-serve: once WorkOS enables the Connections API migration capabilities for your environment, you create and update connections yourself with the Connections API or the [WorkOS CLI](https://github.com/workos/cli). Contact [support@workos.com](mailto:support@workos.com) to start the migration process — the capabilities are enabled per environment, and the endpoints return a `404` until then.

The best strategy depends on how many connections you have and whether you control the existing SSO infrastructure.

### Choosing a migration strategy

### (A) Fewer than 15 connections — Admin Portal approach

For smaller deployments, the simplest path is to recreate each connection in WorkOS and coordinate with each customer's IT team to update their Identity Provider configuration. You can use the [Admin Portal](https://workos.com/docs/admin-portal) to let customers self-service their SSO setup, reducing the coordination burden.

For each connection:

1. Create or identify the WorkOS organization matching the existing organization
2. Share the [Admin Portal](https://workos.com/docs/admin-portal) link with the customer's IT team so they can update their IdP configuration to point to WorkOS
3. Watch for the `connection.activated` webhook to roll out the customer to the WorkOS connection

### (B) 15 or more connections — Transparent migration

For larger deployments, coordinating with every customer's IT team is impractical. Instead, you can perform a **transparent migration** so that IT admins do not need to reconfigure anything on their end. The approach depends on whether you are migrating from an external identity service or from a homegrown SSO implementation.

#### Route 1: Migrating from an external service (proxy approach)

If you are migrating from an external identity provider (such as Auth0, or another third-party service), you can place a proxy in front of the service's custom domain to route IdP callback traffic to WorkOS.

This approach requires that you have a **custom domain** configured with your current provider (e.g., `auth.your-domain.com`) so that you control the domain routing.

##### Requirements

- You have contacted [support@workos.com](mailto:support@workos.com) to enable the Connections API migration capabilities — the connection create and update endpoints, the `external_id` filter, and the `callback_endpoint` field — for your environment
- You have a custom domain configured with your current identity provider
- Your SAML enterprise connections do not use SAML Request Signing with the provider's tenant global key pairs
- Your SAML enterprise connections do not use SAML Response Encryption with the provider's tenant global key pairs

##### Configuring the callback proxy

Once your connections exist in WorkOS, configure a proxy in front of your custom domain to route IdP callback traffic to WorkOS. The proxy intercepts callbacks at your custom domain's callback path and redirects them to each connection's WorkOS callback endpoint.

Create each connection in WorkOS with the [Create Connection API](https://workos.com/docs/reference/sso/connection/create) or the CLI's `workos connection create`, preserving the identifier you already use for it as the connection's `external_id` — see [Importing connections with the API and CLI](#importing-connections-with-the-api-and-cli) below for the full workflow. The proxy uses that identifier to determine where to send each callback: look up the connection with the Connections API filtered by `external_id`, then read its `callback_endpoint` — the immutable URL where WorkOS receives that connection's IdP response.

```bash title="Look up a connection by external ID to read its callback endpoint"
curl "https://api.workos.com/connections?external_id=your-existing-connection-id" \
  -H "Authorization: Bearer sk_example_123456789"
```

The response includes the connection's `callback_endpoint`. Redirect the IdP callback to that URL to complete authentication through WorkOS.

> The `external_id` filter and the `callback_endpoint` field are part of the Connections API migration capabilities. Contact [support@workos.com](mailto:support@workos.com) to enable them for your environment.

The proxy flow works as follows:

1. A user authenticates with their IdP, which posts the SAML response or OIDC callback to your custom domain
2. The proxy looks up the connection by `external_id` and redirects the callback to the connection's `callback_endpoint`
3. WorkOS attempts to find a matching connection:
   - If found, it processes the response and redirects to your application's WorkOS callback
   - If not found, it redirects back with a `fallback` query parameter, and the proxy forwards the original callback to your existing provider

This fallback mechanism ensures zero downtime — connections that haven't been migrated yet continue to work through your existing provider.

The proxy can be implemented using Cloudflare redirect and transform rules, a Cloudflare Worker, or any reverse proxy you control. Contact [support@workos.com](mailto:support@workos.com) for a reference implementation.

> We recommend performing a proof-of-concept in a staging environment before production to de-risk breaking changes in your setup.

#### Route 2: Migrating from a homegrown solution (callback modification)

If your application has its own SSO implementation where you directly handle IdP callbacks, you already control the callback endpoints. Instead of a proxy, you can modify your existing callback handlers to forward IdP responses to WorkOS using a feature flag mechanism.

##### Preparing connection data

Create your existing connections in WorkOS with the [Create Connection API](https://workos.com/docs/reference/sso/connection/create) or the WorkOS CLI, preserving each connection's identifier from your previous provider as its `external_id`. Storing the same identifier on the WorkOS connection lets your callback handler map an incoming IdP response to the correct WorkOS connection without any additional bookkeeping. You can look up a connection at any time with the Connections API filtered by `external_id`.

> The connection create and update endpoints and the `external_id` field are part of the Connections API migration capabilities. Contact [support@workos.com](mailto:support@workos.com) to enable them for your environment.

```bash title="Create a connection with the WorkOS CLI"
workos connection create --file connection.json
```

```json title="connection.json"
{
  "organization_id": "org_01EHWNCE74X7JSDV0X3SZ3KJNY",
  "name": "Acme Corp",
  "external_id": "your-existing-connection-id",
  "saml_options": {
    "idp_metadata_url": "https://idp.example.com/metadata.xml"
  }
}
```

Or call the endpoint directly:

```bash title="Create a connection with the Connections API"
curl -X POST https://api.workos.com/connections \
  -H "Authorization: Bearer sk_example_123456789" \
  -H "Content-Type: application/json" \
  -d @connection.json
```

Creating is idempotent on `external_id`, so a migration script can safely be re-run after a partial failure. To correct a connection's configuration afterwards, use the [Update Connection API](https://workos.com/docs/reference/sso/connection/patch) or `workos connection update`. See [Importing connections with the API and CLI](#importing-connections-with-the-api-and-cli) below for the full workflow, including SAML manual configuration, OIDC secrets, and attribute mappings.

##### How it works

1. Create your connections in WorkOS with the Connections API or CLI, setting `external_id` to your existing connection identifier
2. Modify your existing callback endpoint to check a [WorkOS Feature Flag](https://workos.com/docs/feature-flags) before processing the IdP response
3. For organizations flagged for WorkOS, forward the IdP response (SAMLResponse or authorization code) to WorkOS for processing
4. For organizations not yet migrated, continue processing through your existing flow

This gives you per-organization control over the migration without requiring IdP reconfiguration.

```typescript title="Callback modification with WorkOS Feature Flags"
import { WorkOS } from '@workos-inc/node';

const workos = new WorkOS(process.env.WORKOS_API_KEY);
const featureFlags = workos.featureFlags.createRuntimeClient();
await featureFlags.waitUntilReady({ timeoutMs: 5000 });

async function handleSSOCallback(req: Request): Promise<Response> {
  const organizationId = extractOrganizationFromCallback(req);

  const useWorkOS = featureFlags.isEnabled('workos-sso-enabled', {
    organizationId,
  });

  if (useWorkOS) {
    // Forward the IdP response to WorkOS for processing
    const { user } = await workos.userManagement.authenticateWithCode({
      code: req.query.code,
      clientId: process.env.WORKOS_CLIENT_ID!,
    });
    return handleWorkOSUser(user);
  }

  // Continue with existing SSO flow for non-migrated organizations
  return handleExistingSSOCallback(req);
}
```

### Gradual rollout with Feature Flags

Regardless of which route you use, the authorization URL piece works the same way. You can use [WorkOS Feature Flags](https://workos.com/docs/feature-flags) to control which organizations use WorkOS SSO versus your existing provider during the migration. Create a feature flag (e.g., `workos-sso-enabled`) and target it to specific organizations as you migrate them.

```typescript title="Route SSO based on feature flag"
import { WorkOS } from '@workos-inc/node';

const workos = new WorkOS(process.env.WORKOS_API_KEY);
const featureFlags = workos.featureFlags.createRuntimeClient();
await featureFlags.waitUntilReady({ timeoutMs: 5000 });

function getSSOAuthorizationUrl(organizationId: string): string {
  const useWorkOS = featureFlags.isEnabled('workos-sso-enabled', {
    organizationId,
  });

  if (useWorkOS) {
    return workos.userManagement.getAuthorizationUrl({
      provider: 'authkit',
      organizationId,
      clientId: process.env.WORKOS_CLIENT_ID!,
      redirectUri: process.env.WORKOS_REDIRECT_URI!,
    });
  }

  // Fall back to existing SSO provider for organizations not yet migrated
  return getExistingSSOAuthorizationUrl(organizationId);
}
```

This lets you gradually migrate organizations to WorkOS SSO, verify each one works correctly, and roll back individual organizations if needed — all without code deployments.

### Importing connections with the API and CLI

Whichever route you take, the import itself works the same way: export each connection's configuration from your existing provider, then create it in WorkOS with the [Create Connection API](https://workos.com/docs/reference/sso/connection/create) or the [WorkOS CLI](https://github.com/workos/cli).

#### Export connection data from your existing provider

For each connection, collect the organization it belongs to and its protocol configuration.

For **SAML** connections:

| Field                     | Description                                                              | Required                             |
| ------------------------- | ------------------------------------------------------------------------ | ------------------------------------ |
| IdP metadata URL          | The Identity Provider's metadata document URL                            | Either this or the manual IdP fields |
| IdP Entity ID             | The Identity Provider's Entity ID                                        | With manual IdP fields               |
| IdP SSO URL               | The Identity Provider's SSO URL                                          | With manual IdP fields               |
| IdP signing certificates  | X.509 certificates used to verify signed SAML responses                  | With manual IdP fields               |
| ACS URL                   | A custom Assertion Consumer Service URL, if your provider used one       | Optional                             |
| SP Entity ID              | A custom Service Provider Entity ID (audience), if your provider used one | Optional                            |
| Request signing key pair  | Customer-owned key pair used to sign SAML requests                        | Optional                             |
| Response encryption key pairs | Customer-owned key pairs used to decrypt encrypted SAML responses     | Optional                             |

For **OIDC** connections:

| Field               | Description                                              | Required |
| ------------------- | -------------------------------------------------------- | -------- |
| Discovery endpoint  | The OpenID Connect discovery URL                          | Yes      |
| Client ID           | The OIDC client ID registered with the IdP                | Yes      |
| Client secret       | The OIDC client secret (not needed for `private_key_jwt`) | Usually  |
| Redirect URI        | A custom OAuth callback URL, if your provider used one    | Optional |

Also record each connection's identifier in your existing provider. You'll preserve it as the WorkOS connection's `external_id`, which keeps imports idempotent and lets you correlate WorkOS connections back to your existing system. If your users' IdP attributes don't match the WorkOS defaults, export the attribute or claim mappings too.

#### Create or locate WorkOS organizations

Every connection belongs to an [organization](https://workos.com/docs/reference/organization). For each connection, find the matching WorkOS organization or create one with the [Create Organization API](https://workos.com/docs/reference/organization/create):

```bash
curl -X POST https://api.workos.com/organizations \
  -H "Authorization: Bearer sk_example_123456789" \
  -H "Content-Type: application/json" \
  -d '{ "name": "Acme Corp", "external_id": "acme-legacy-org-1" }'
```

Persist the returned organization ID — you'll need it to create the connection.

#### Create connections

Create each connection with the [Create Connection API](https://workos.com/docs/reference/sso/connection/create), or with the WorkOS CLI:

```bash
workos connection create --file connection.json
```

Or call the endpoint directly:

```bash
curl -X POST https://api.workos.com/connections \
  -H "Authorization: Bearer sk_example_123456789" \
  -H "Content-Type: application/json" \
  -d @connection.json
```

##### SAML connections

If your existing provider exposes an IdP metadata URL for the connection, provide it and WorkOS imports the IdP configuration from the metadata document:

```json title="connection.json — SAML via metadata URL"
{
  "organization_id": "org_01EHWNCE74X7JSDV0X3SZ3KJNY",
  "name": "Acme Corp SAML",
  "external_id": "acme-legacy-conn-42",
  "saml_options": {
    "idp_metadata_url": "https://idp.example.com/metadata.xml"
  }
}
```

Otherwise, provide the IdP fields manually. The metadata URL and the manual IdP fields are mutually exclusive:

```json title="connection.json — SAML via manual IdP fields"
{
  "organization_id": "org_01EHWNCE74X7JSDV0X3SZ3KJNY",
  "name": "Acme Corp SAML",
  "external_id": "acme-legacy-conn-42",
  "saml_options": {
    "idp_entity_id": "https://idp.example.com/entity",
    "idp_sso_url": "https://idp.example.com/sso",
    "idp_signing_certs": [
      "-----BEGIN CERTIFICATE-----\nMIIC...\n-----END CERTIFICATE-----"
    ]
  }
}
```

To avoid IdP-side reconfiguration, carry over the connection's existing service provider settings: pass the ACS URL and SP Entity ID the IdP is already configured with as `acs_url` and `sp_entity_id`, and import customer-owned key pairs with `sp_signing_key_pair` (request signing) and `sp_encryption_key_pairs` (response encryption). When omitted, WorkOS generates and manages these values.

##### OIDC connections

```json title="connection.json — OIDC"
{
  "organization_id": "org_01EHWNCE74X7JSDV0X3SZ3KJNY",
  "name": "Acme Corp OIDC",
  "external_id": "acme-legacy-conn-43",
  "oidc_options": {
    "discovery_endpoint": "https://idp.example.com/.well-known/openid-configuration",
    "client_id": "client_123",
    "client_secret": "secret_xyz"
  }
}
```

The client secret is write-only: it is stored encrypted and never returned by the API. For IdPs that authenticate with `private_key_jwt` instead of a client secret, set `token_authentication_method` to `private_key_jwt` and omit `client_secret`; WorkOS generates a signing key pair and returns its certificate in `oidc_options.jwt_signing_certs` for you to register at the IdP, or you can import an existing key pair with `jwt_signing_key_pair`.

##### Attribute mappings

If the IdP's attributes or claims don't match the defaults for the connection type, map them with `attribute_maps`:

```json
{
  "attribute_maps": {
    "standard_attributes": {
      "idp_id": "sub",
      "email": "mail",
      "first_name": "givenName",
      "last_name": "surname"
    },
    "custom_attributes": {
      "department": "dept_claim"
    }
  }
}
```

Custom attributes must already be defined in the WorkOS dashboard.

##### Idempotency with `external_id`

Set `external_id` to the connection's identifier in your existing provider. Creating is idempotent on it: if a connection with the same `external_id` already exists in the organization, the API returns the existing connection instead of creating a duplicate, so a migration script can safely be re-run after a partial failure. Create does not apply changed fields to an existing connection; use the Update Connection API below to correct its configuration. If the `external_id` is already used by a connection in a different organization, the API responds with an `external_id_already_in_use` error.

#### Update connections

Correct or evolve imported connections with the [Update Connection API](https://workos.com/docs/reference/sso/connection/patch), or with the CLI:

```bash
workos connection update conn_01E4ZCR3C56J083X43JQXF3JK5 \
  --data '{ "name": "Acme Corp (Prod)" }'
```

Or call the endpoint directly:

```bash
curl -X PATCH https://api.workos.com/connections/conn_01E4ZCR3C56J083X43JQXF3JK5 \
  -H "Authorization: Bearer sk_example_123456789" \
  -H "Content-Type: application/json" \
  -d '{ "name": "Acme Corp (Prod)" }'
```

Updates are partial: only the fields you provide are changed. Fields that accept `null` can be set to `null` to clear an override and revert to the WorkOS-generated default — for example, setting `saml_options.acs_url` to `null` reverts to the standard WorkOS ACS URL, and setting `external_id` to `null` stops tracking one.

You can look up a connection by its `external_id` at any time with the [List Connections API](https://workos.com/docs/reference/sso/connection/list):

```bash
curl "https://api.workos.com/connections?external_id=acme-legacy-conn-42" \
  -H "Authorization: Bearer sk_example_123456789"
```

### Managing SAML certificates

SAML connections involve up to three kinds of certificates, each with its own lifecycle. Customer-owned key pairs can only be imported when the connection is created — after that, WorkOS generates and holds every new key pair, and the API only ever returns public certificates, never private keys.

#### IdP signing certificates

The Identity Provider signs SAML responses; WorkOS verifies them against the connection's IdP signing certificates, so WorkOS never holds a private key for these. You import them at creation with `saml_options.idp_signing_certs` (or via the metadata URL), and can add more later — for example, ahead of an IdP-side rotation — with the [IdP signing certificate endpoints](https://workos.com/docs/reference/sso/saml-idp-signing-certificate):

```bash
curl -X POST https://api.workos.com/connections/conn_01E4ZCR3C56J083X43JQXF3JK5/saml_idp_signing_certs \
  -H "Authorization: Bearer sk_example_123456789" \
  -H "Content-Type: application/json" \
  -d '{ "value": "-----BEGIN CERTIFICATE-----MIIC...-----END CERTIFICATE-----" }'
```

Existing certificates keep working until they are deleted or expire, so add the new certificate before the IdP starts signing with it. The last remaining certificate cannot be deleted (`cannot_delete_last_certificate`), and a deleted certificate that is still published in the connection's IdP metadata may be restored by a later metadata refresh — remove it from the metadata document too.

#### SP signing certificate

The connection's request signing key pair signs SAML requests sent to the IdP. Import a customer-owned pair at creation with `saml_options.sp_signing_key_pair`; afterwards, the [SP signing certificate endpoints](https://workos.com/docs/reference/sso/saml-sp-signing-certificate) manage a WorkOS-generated key pair instead — creating with a `key`/`cert` body is rejected with `key_pair_not_accepted`.

A connection signs with one key pair at a time, so creating a second is rejected with `sp_signing_cert_already_exists`. To rotate, delete the active certificate, create its replacement, then register the new public certificate with the IdP:

```bash
curl -X DELETE https://api.workos.com/connections/conn_01E4ZCR3C56J083X43JQXF3JK5/saml_sp_signing_cert/saml_party_trust_01E4ZCR3C56J083X43JQXF3JK5 \
  -H "Authorization: Bearer sk_example_123456789"

curl -X POST https://api.workos.com/connections/conn_01E4ZCR3C56J083X43JQXF3JK5/saml_sp_signing_cert \
  -H "Authorization: Bearer sk_example_123456789"
```

#### SP encryption certificates

The connection's encryption key pairs decrypt encrypted SAML responses. Import customer-owned pairs at creation with `saml_options.sp_encryption_key_pairs`; afterwards, the [SP encryption certificate endpoints](https://workos.com/docs/reference/sso/saml-sp-encryption-certificate) generate WorkOS-held key pairs — as with signing, a `key`/`cert` body is rejected with `key_pair_not_accepted`.

Unlike signing, several encryption key pairs can be active at once: every active private key is tried when decrypting, so rotations overlap. Creating appends a new key pair rather than replacing the old one:

```bash
curl -X POST https://api.workos.com/connections/conn_01E4ZCR3C56J083X43JQXF3JK5/saml_sp_encryption_certs \
  -H "Authorization: Bearer sk_example_123456789"
```

To rotate: create the new certificate, register it with the IdP, then delete the old one once the IdP has stopped encrypting with it. At most three key pairs can be active at once — creating beyond the cap is rejected with `too_many_sp_encryption_certs`.

### Validate migrated connections

Before routing production traffic, test a sign-in against each migrated connection, or at least one connection per IdP type. For connections that need IdP-side changes, share an [Admin Portal](https://workos.com/docs/admin-portal) link so the customer's IT team can finish the setup themselves.

***

## (4) Handling interim new users

Many applications allow users to sign up at any time. If your app offers this feature, then you should consider the timing of your migration. If any users sign up after you've completed importing users into WorkOS, but before you've switched to WorkOS for authentication, then those users will have been omitted from the migration process.

There are two main strategies to handle this:

### (A) Disable signups during migration

The simplest solution is to schedule an appropriate time for the migration and disable signup while in progress. This may be done using temporary code added to your application and controlled by a feature flagging system.

After the migration is complete, your application should be updated to perform authentication using WorkOS, and the signup flag block disabled. This helps to ensure the export/import process captures all active users.

### (B) Use a dual-write strategy

For applications that want to avoid disabling signups, a "dual-write" strategy can be used.

![Diagram demonstrating the dual-write process.](https://images.workoscdn.com/images/656fbbac-adb1-4f99-b416-056d80e408ac.png?auto=format\&fit=clip\&q=80)\[border=false]

When a new user signs-up, in addition to creating a user record in the existing user store, the application should also create a matching record in WorkOS using the [Create User API](https://workos.com/docs/reference/authkit/user/create). As time passes, WorkOS will stay consistent with future new users, but a migration will still need to be performed for the historical set of users.

You will need to perform the same export and import process into WorkOS, but keeping in mind that some users will already exist in WorkOS as a result from the "dual-write".

While this minimizes forms of downtime for your application, there are other complications. For example, if a user updates their email or authentication method, you will need to perform the same update in WorkOS, at least until the migration process is complete.

### Which to choose?

Your timeline for completing the migration, along with your user's tolerances for disruption, will affect which strategy makes more sense for your application.

Disabling signups, or even sign-in entirely, and doing a "big-bang" migration by moving all users at the same time, could be reasonable for a smaller application. However, larger applications that are on the critical path for their customers may need a more careful path in order to provide consistent access.

## Wrapping-up

User management migration complexity can vary, so it is important to consider how existing application constraints will transfer to WorkOS. If you have any questions, reach out to [support@workos.com](mailto:support@workos.com) or via your team's WorkOS Slack channel for more help planning your migration.
