Skip to content
Esc
navigateopen⌘Jpreview
Dashboard
On this page

Account Migration

Migrate your users from a legacy authentication provider to SuperTokens.

The following guide shows you how to move users from your current authentication solution to SuperTokens.


Overview

The process of migrating your accounts breaks down into two parts:

Creating new users on the fly

To ensure a smooth migration process, with no downtime, you need to be able to directly create new users from the legacy sign up flow. This is necessary since there is a time gap between when you export all your data for bulk import and when you go live with SuperTokens.

New users might get created in that interval through your legacy authentication provider. Hence, you also need to create them in SuperTokens to keep the data in sync.

Adding most of your users through a bulk import

After you have set in place the lazy migration process you can move on to adding most of your users. This happens through the bulk import API. The process is asynchronous and can work with large amounts of data.

Before you start

This guide assumes that you have already integrated SuperTokens with your existing stack. If you have not, please check the Quickstart Guide and explore all the supported authentication methods.

Bulk import requires Core 10.0.0 or later and persistent database storage; the in-memory database does not support these APIs. Before importing:

  • create and configure every target tenant, role, recipe, and third-party provider referenced by the import;
  • enable account linking before importing a user with multiple login methods, and test your linking policy with a representative export;
  • decide how each legacy identity maps to a tenant and login method, and reject ambiguous or duplicate mappings; and
  • take a restorable source export and define retry, reconciliation, rollback, and cutover procedures.

For email/password users, provide either a supported passwordHash with its hashingAlgorithm, or a plainTextPassword, as defined by the bulk-import request schema. Prefer compatible bcrypt, Argon2, or Firebase scrypt hashes over plain-text passwords. Treat exports, password hashes, MFA secrets, API keys, and access tokens as credentials: encrypt them in transit and at rest, restrict access, never put them in logs or user metadata, and securely delete temporary copies after reconciliation.

Steps

1. Update the legacy sign up flow

Modify the legacy sign up flow logic to also create new users in SuperTokens. You can do this through the Import User endpoint that allows you to directly create accounts. Call the endpoint from the authentication flow used by your legacy provider.

curl -X POST "<CORE_API_ENDPOINT>/appid-public/bulk-import/import" \
  -H "api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "externalUserId": "user_12345",
  "userMetadata": {
    "firstName": "Jane",
    "lastName": "Doe",
    "department": "Engineering"
  },
  "userRoles": [
    {
      "role": "admin",
      "tenantIds": [
        "public"
      ]
    }
  ],
  "totpDevices": [
    {
      "secretKey": "JBSWY3DPEHPK3PXP",
      "period": 30,
      "skew": 1,
      "deviceName": "Main Device"
    }
  ],
  "loginMethods": [
    {
      "isVerified": true,
      "isPrimary": true,
      "timeJoinedInMSSinceEpoch": 1672531199000,
      "recipeId": "emailpassword",
      "email": "jane.doe@example.com",
      "passwordHash": "$2b$10$Tix3Vpu93kiaZRLPPzD6QOIm62x0l5gRdvlyark5S.MLn/NY6t4gS",
      "hashingAlgorithm": "bcrypt"
    }
  ]
}'
const response = await fetch("<CORE_API_ENDPOINT>/appid-public/bulk-import/import", {
  method: "POST",
  headers: {
    "api-key": "YOUR_API_KEY",
    "Content-Type": "application/json"
  },
  body: JSON.stringify({
  "externalUserId": "user_12345",
  "userMetadata": {
    "firstName": "Jane",
    "lastName": "Doe",
    "department": "Engineering"
  },
  "userRoles": [
    {
      "role": "admin",
      "tenantIds": [
        "public"
      ]
    }
  ],
  "totpDevices": [
    {
      "secretKey": "JBSWY3DPEHPK3PXP",
      "period": 30,
      "skew": 1,
      "deviceName": "Main Device"
    }
  ],
  "loginMethods": [
    {
      "isVerified": true,
      "isPrimary": true,
      "timeJoinedInMSSinceEpoch": 1672531199000,
      "recipeId": "emailpassword",
      "email": "jane.doe@example.com",
      "passwordHash": "$2b$10$Tix3Vpu93kiaZRLPPzD6QOIm62x0l5gRdvlyark5S.MLn/NY6t4gS",
      "hashingAlgorithm": "bcrypt"
    }
  ]
})
});
package main

import (
	"net/http"
	"strings"
)

func main() {
	req, err := http.NewRequest("POST", "<CORE_API_ENDPOINT>/appid-public/bulk-import/import", strings.NewReader(`{
  "externalUserId": "user_12345",
  "userMetadata": {
    "firstName": "Jane",
    "lastName": "Doe",
    "department": "Engineering"
  },
  "userRoles": [
    {
      "role": "admin",
      "tenantIds": [
        "public"
      ]
    }
  ],
  "totpDevices": [
    {
      "secretKey": "JBSWY3DPEHPK3PXP",
      "period": 30,
      "skew": 1,
      "deviceName": "Main Device"
    }
  ],
  "loginMethods": [
    {
      "isVerified": true,
      "isPrimary": true,
      "timeJoinedInMSSinceEpoch": 1672531199000,
      "recipeId": "emailpassword",
      "email": "jane.doe@example.com",
      "passwordHash": "$2b$10$Tix3Vpu93kiaZRLPPzD6QOIm62x0l5gRdvlyark5S.MLn/NY6t4gS",
      "hashingAlgorithm": "bcrypt"
    }
  ]
}`))
	if err != nil {
		panic(err)
	}
	req.Header.Set("api-key", "YOUR_API_KEY")
	req.Header.Set("Content-Type", "application/json")
	response, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer response.Body.Close()
}
import requests

response = requests.post(
    "<CORE_API_ENDPOINT>/appid-public/bulk-import/import",
    headers={
        "api-key": "YOUR_API_KEY",
        "Content-Type": "application/json"
    },
    json={
  "externalUserId": "user_12345",
  "userMetadata": {
    "firstName": "Jane",
    "lastName": "Doe",
    "department": "Engineering"
  },
  "userRoles": [
    {
      "role": "admin",
      "tenantIds": [
        "public"
      ]
    }
  ],
  "totpDevices": [
    {
      "secretKey": "JBSWY3DPEHPK3PXP",
      "period": 30,
      "skew": 1,
      "deviceName": "Main Device"
    }
  ],
  "loginMethods": [
    {
      "isVerified": True,
      "isPrimary": True,
      "timeJoinedInMSSinceEpoch": 1672531199000,
      "recipeId": "emailpassword",
      "email": "jane.doe@example.com",
      "passwordHash": "$2b$10$Tix3Vpu93kiaZRLPPzD6QOIm62x0l5gRdvlyark5S.MLn/NY6t4gS",
      "hashingAlgorithm": "bcrypt"
    }
  ]
},
)
Auth0 Instructions

Create the Auth0 roles in SuperTokens before migrating users. The application endpoint must own an allowlisted mapping from Auth0 organization/connection/provider identifiers to SuperTokens tenants and providers. Do not let Action input select arbitrary tenant IDs or provider configuration.

1. Access the Auth0 Dashboard
2. From the navigation menu go to Actions > Library
3. Click Create Action > Create custom action
4. Specify a custom name for your action and then select Login/Post Login as the trigger
5. Add MIGRATION_ENDPOINT_URL and MIGRATION_ENDPOINT_TOKEN Action secrets
6. Paste the following code in the editor

MIGRATION_ENDPOINT_TOKEN must authorize only this migration endpoint. The endpoint must authenticate every request, allow only the expected Auth0 tenant/issuer, rate-limit by credential and legacy user ID, enforce request-size limits, and use externalUserId as an idempotency key. Keep the Core URL and Core API key only in your backend secret store. The backend validates and maps the identity, retrieves any credential export through restricted storage, and then calls Core.

exports.onExecutePostLogin = async (event, api) => {
  const migrationEndpoint = event.secrets.MIGRATION_ENDPOINT_URL;
  const migrationToken = event.secrets.MIGRATION_ENDPOINT_TOKEN;

  try {
    if (event.user.app_metadata?.migrated_to_supertokens) {
      return;
    }

    const response = await fetch(migrationEndpoint, {
      method: "POST",
      headers: {
        Authorization: `Bearer ${migrationToken}`,
        "Content-Type": "application/json; charset=utf-8",
      },
      body: JSON.stringify({
        externalUserId: event.user.user_id,
        auth0OrganizationId: event.organization?.id,
        identities: event.user.identities?.map(({ provider, connection, user_id }) => ({
          provider,
          connection,
          userId: user_id,
        })),
      }),
    });

    const result = await response.json();

    if (response.ok && result.status === "OK") {
      api.user.setAppMetadata("migrated_to_supertokens", true);
      api.user.setAppMetadata("supertokens_user_id", result.userId);
    } else {
      console.error("Migration endpoint rejected the request");
    }
  } catch (error) {
    console.error("Migration endpoint request failed");
  }
};

2. Export the accounts from your legacy provider

Export the users from your legacy authentication provider and adjust the data to match the request body schema used in the Add Users for Bulk Import endpoint.

Auth0 Instructions

1. Create a management API application in Auth0

1.1 Navigate to Auth0 Dashboard and the select Applications > APIs
1.2 Select Auth0 Management API
1.3 Go to Machine to Machine Applications tab
1.4 Authorize your application or create a new one
1.5 Grant only read:users and read:users_app_metadata
1.6 Save your Domain, Client ID, and Client Secret

2. Get the management API access token

You need a valid Management API Access Token to export users. Use the following cURL command to get the token:

curl --request POST \
  --url 'https://YOUR_DOMAIN.auth0.com/oauth/token' \
  --header 'content-type: application/json' \
  --data '{
    "client_id": "YOUR_CLIENT_ID",
    "client_secret": "YOUR_CLIENT_SECRET",
    "audience": "https://YOUR_DOMAIN.auth0.com/api/v2/",
    "grant_type": "client_credentials"
  }'

3. Create the export job

Use the POST /api/v2/jobs/users-exports endpoint to create a job that exports all users.

curl --request POST \
  --url 'https://YOUR_DOMAIN.auth0.com/api/v2/jobs/users-exports' \
  --header 'authorization: Bearer YOUR_MGMT_API_TOKEN' \
  --header 'content-type: application/json' \
  --data '{
    "format": "json",
    "fields": [
      {"name": "user_id"},
      {"name": "email"},
      {"name": "email_verified"},
      {"name": "name"},
      {"name": "nickname"},
      {"name": "picture"},
      {"name": "created_at"},
      {"name": "updated_at"},
      {"name": "identities"},
      {"name": "app_metadata"},
      {"name": "user_metadata"},
      {"name": "phone_number"},
      {"name": "phone_verified"}
    ]
  }'

4. Check the export job status

Check if the export job has finalized with this request:

curl --request GET \
  --url 'https://YOUR_DOMAIN.auth0.com/api/v2/jobs/job_abc123xyz' \
  --header 'authorization: Bearer YOUR_MGMT_API_TOKEN'

5. Download the export file

The previous request returns a location attribute in the response body if the export job has finalized. Use it do access your data.

umask 077
cd /secure-migration-work # An encrypted, access-restricted filesystem
curl --fail --location --proto '=https' -o auth0_users.json.gz "LOCATION_URL_FROM_RESPONSE"
gzip --test auth0_users.json.gz
sha256sum auth0_users.json.gz > auth0_users.json.gz.sha256
age --recipient "<MIGRATION_ARCHIVE_RECIPIENT>" --output auth0_users.json.gz.age auth0_users.json.gz

Keep the compressed download on that encrypted filesystem, move the encrypted archive and checksum to restricted migration storage, and verify that decryption succeeds. Auth0 exports NDJSON inside the gzip stream. Convert it without replacing the retained compressed archive:

age --decrypt --identity /run/secrets/migration-archive-key auth0_users.json.gz.age \
  | gzip -dc \
  | jq -s '.' > /secure-migration-work/auth0_users_array.json

Keep the compressed export, encrypted copy, and checksum unchanged through transformation, import, failed-row retries, and source-to-target reconciliation. Keep derived plaintext only on encrypted restricted storage and delete it after each run. Delete all source-archive copies only after final reconciliation and rollback retention requirements are met; use your storage system’s verified deletion/lifecycle mechanism rather than assuming rm securely erases every medium.

6. Transform the data to the SuperTokens format

Create the Auth0 roles in SuperTokens before migrating users. This example assigns them to the default public tenant.

const fs = require("fs");

const auth0Users = JSON.parse(fs.readFileSync("auth0_users_array.json", "utf8"));

const superTokensUsers = auth0Users
  .map((auth0User) => {
    if (auth0User.app_metadata?.migrated_to_supertokens) {
      console.log(`User ${auth0User.user_id} already migrated`);
      return;
    }

    const userPayload = {
      externalUserId: auth0User.user_id,
      userMetadata: {
        auth0_user_id: auth0User.user_id,
        name: auth0User.name,
        nickname: auth0User.nickname,
        picture: auth0User.picture,
        auth0_user_metadata: auth0User.user_metadata,
        auth0_app_metadata: auth0User.app_metadata,
      },
      userRoles: (auth0User.app_metadata?.roles || []).map((role) => ({ role, tenantIds: ["public"] })),
      loginMethods: [],
    };

    const ThirdPartyProviders = ["google-oauth2", "facebook", "github", "apple"];

    auth0User.identities.forEach((identity, index) => {
      if (ThirdPartyProviders.includes(identity.provider)) {
        userPayload.loginMethods.push({
          recipeId: "thirdparty",
          thirdPartyId: mapProvider(identity.provider),
          thirdPartyUserId: identity.user_id,
          email: identity.profileData?.email ?? auth0User.email,
          isVerified: identity.profileData?.email_verified ?? auth0User.email_verified ?? false,
          isPrimary: index === 0,
          timeJoinedInMSSinceEpoch: new Date(auth0User.created_at).getTime(),
        });
      } else if (identity.provider === "auth0" || identity.provider === "Username-Password-Authentication") {
        // Auth0 does not export password hashes through the ordinary user export
        // You will have to contact their support and request them
        userPayload.loginMethods.push({
          recipeId: "emailpassword",
          email: identity.profileData?.email ?? auth0User.email,
          // Request the password hash from Auth0 and then implement the function to retrieve the values
          passwordHash: getPasswordHash(identity.profileData?.email),
          hashingAlgorithm: "bcrypt",
          isVerified: identity.profileData?.email_verified ?? auth0User.email_verified ?? false,
          isPrimary: index === 0,
          timeJoinedInMSSinceEpoch: new Date(auth0User.created_at).getTime(),
        });
      } else if (identity.provider === "sms") {
        userPayload.loginMethods.push({
          recipeId: "passwordless",
          phoneNumber: identity.profileData?.phone_number || auth0User.phone_number,
          isVerified: identity.profileData?.phone_verified ?? auth0User.phone_verified ?? false,
          isPrimary: index === 0,
          timeJoinedInMSSinceEpoch: new Date(auth0User.created_at).getTime(),
        });
      } else if (identity.provider === "email") {
        userPayload.loginMethods.push({
          recipeId: "passwordless",
          email: identity.profileData?.email || auth0User.email,
          isVerified: identity.profileData?.email_verified ?? auth0User.email_verified ?? false,
          isPrimary: index === 0,
          timeJoinedInMSSinceEpoch: new Date(auth0User.created_at).getTime(),
        });
      } else {
        throw new Error(`Unknown provider: ${identity.provider}`);
      }
    });

    return userPayload;
  })
  .filter(Boolean);

fs.writeFileSync("supertokens_users.json", JSON.stringify({ users: superTokensUsers }, null, 2));

function mapProvider(auth0Provider) {
  const mapping = {
    "google-oauth2": "google",
    facebook: "facebook",
    github: "github",
    apple: "apple",
  };
  return mapping[auth0Provider] || auth0Provider;
}

console.log(`Transformed ${superTokensUsers.length} users`);

3. Perform the bulk migration process

3.1 Add the accounts to import

Using the data that you have generated in the previous step, call the Add Users for Bulk Import endpoint. This step stages the data that the background job imports later.

Keep in mind that the endpoint has a limit of 10000 users per request.

curl -X POST "<CORE_API_ENDPOINT>/appid-public/bulk-import/users" \
  -H "api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "users": [
    {
      "externalUserId": "user_2",
      "userMetadata": {
        "firstName": "John",
        "lastName": "Doe",
        "department": "Marketing"
      },
      "userRoles": [
        {
          "role": "editor",
          "tenantIds": [
            "public"
          ]
        }
      ],
      "loginMethods": [
        {
          "isVerified": true,
          "isPrimary": true,
          "timeJoinedInMSSinceEpoch": 1672617599000,
          "recipeId": "thirdparty",
          "email": "john.doe@gmail.com",
          "thirdPartyId": "google",
          "thirdPartyUserId": "google_987654321"
        }
      ]
    }
  ]
}'
const response = await fetch("<CORE_API_ENDPOINT>/appid-public/bulk-import/users", {
  method: "POST",
  headers: {
    "api-key": "YOUR_API_KEY",
    "Content-Type": "application/json"
  },
  body: JSON.stringify({
  "users": [
    {
      "externalUserId": "user_2",
      "userMetadata": {
        "firstName": "John",
        "lastName": "Doe",
        "department": "Marketing"
      },
      "userRoles": [
        {
          "role": "editor",
          "tenantIds": [
            "public"
          ]
        }
      ],
      "loginMethods": [
        {
          "isVerified": true,
          "isPrimary": true,
          "timeJoinedInMSSinceEpoch": 1672617599000,
          "recipeId": "thirdparty",
          "email": "john.doe@gmail.com",
          "thirdPartyId": "google",
          "thirdPartyUserId": "google_987654321"
        }
      ]
    }
  ]
})
});
package main

import (
	"net/http"
	"strings"
)

func main() {
	req, err := http.NewRequest("POST", "<CORE_API_ENDPOINT>/appid-public/bulk-import/users", strings.NewReader(`{
  "users": [
    {
      "externalUserId": "user_2",
      "userMetadata": {
        "firstName": "John",
        "lastName": "Doe",
        "department": "Marketing"
      },
      "userRoles": [
        {
          "role": "editor",
          "tenantIds": [
            "public"
          ]
        }
      ],
      "loginMethods": [
        {
          "isVerified": true,
          "isPrimary": true,
          "timeJoinedInMSSinceEpoch": 1672617599000,
          "recipeId": "thirdparty",
          "email": "john.doe@gmail.com",
          "thirdPartyId": "google",
          "thirdPartyUserId": "google_987654321"
        }
      ]
    }
  ]
}`))
	if err != nil {
		panic(err)
	}
	req.Header.Set("api-key", "YOUR_API_KEY")
	req.Header.Set("Content-Type", "application/json")
	response, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer response.Body.Close()
}
import requests

response = requests.post(
    "<CORE_API_ENDPOINT>/appid-public/bulk-import/users",
    headers={
        "api-key": "YOUR_API_KEY",
        "Content-Type": "application/json"
    },
    json={
  "users": [
    {
      "externalUserId": "user_2",
      "userMetadata": {
        "firstName": "John",
        "lastName": "Doe",
        "department": "Marketing"
      },
      "userRoles": [
        {
          "role": "editor",
          "tenantIds": [
            "public"
          ]
        }
      ],
      "loginMethods": [
        {
          "isVerified": True,
          "isPrimary": True,
          "timeJoinedInMSSinceEpoch": 1672617599000,
          "recipeId": "thirdparty",
          "email": "john.doe@gmail.com",
          "thirdPartyId": "google",
          "thirdPartyUserId": "google_987654321"
        }
      ]
    }
  ]
},
)

3.2 Monitor the progress of the job

To determine if the import flow has processed all the users, call the Count Staged Users API.

Before doing that, first understand the different states in which a staged user can be. During the import process, the user can have one of the following statuses:

  • NEW (not yet started): The import process has not yet picked up the user.
  • PROCESSING: The import process has selected the user for import.
  • FAILED: The import process has failed for that user.

If a user gets imported successfully it then gets removed from the staged list. Hence, no status exists for that state.

With this new information, get back to the count users endpoint. The request counts the users that await import. Pass a status filter as a query parameter to count only users in that state: status=NEW, status=PROCESSING, or status=FAILED.

curl -X GET "<CORE_API_ENDPOINT>/appid-public/bulk-import/users/count?status=PROCESSING" \
  -H "api-key: YOUR_API_KEY"
const response = await fetch("<CORE_API_ENDPOINT>/appid-public/bulk-import/users/count?status=PROCESSING", {
  method: "GET",
  headers: {
    "api-key": "YOUR_API_KEY"
  }
});
package main

import (
	"net/http"
)

func main() {
	req, err := http.NewRequest("GET", "<CORE_API_ENDPOINT>/appid-public/bulk-import/users/count?status=PROCESSING", nil)
	if err != nil {
		panic(err)
	}
	req.Header.Set("api-key", "YOUR_API_KEY")
	response, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer response.Body.Close()
}
import requests

response = requests.get(
    "<CORE_API_ENDPOINT>/appid-public/bulk-import/users/count?status=PROCESSING",
    headers={
        "api-key": "YOUR_API_KEY"
    },
)

Given that information, to check if your import is complete do the following:

  1. Call the count users API once without any filters. If the count is 0, then the import process is complete.
  2. If the count is not 0, then check if you still have rows that are getting processed (status=PROCESSING) or if there are rows that the import job has not yet picked up (status=NEW)
  3. If the only rows that remain are the ones with the FAILED status, then proceed to step 3.3. There you can see how to debug those issues.

3.3 Handle staged users that failed to import

Go through this step only if you have staged users that failed to import. This can happen for a number of reasons. Some common ones:

  • Email / phoneNumber already exists
  • externalUserId is being already used by other user
  • A primary user already exists for the email but with a different login method

If at the end of the previous step you have determined that you have staged users that failed to import, debug the issues with the Get Staged Users API. Filter the results with status=FAILED.

curl -X GET "<CORE_API_ENDPOINT>/appid-public/bulk-import/users?status=FAILED" \
  -H "api-key: YOUR_API_KEY"
const response = await fetch("<CORE_API_ENDPOINT>/appid-public/bulk-import/users?status=FAILED", {
  method: "GET",
  headers: {
    "api-key": "YOUR_API_KEY"
  }
});
package main

import (
	"net/http"
)

func main() {
	req, err := http.NewRequest("GET", "<CORE_API_ENDPOINT>/appid-public/bulk-import/users?status=FAILED", nil)
	if err != nil {
		panic(err)
	}
	req.Header.Set("api-key", "YOUR_API_KEY")
	response, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer response.Body.Close()
}
import requests

response = requests.get(
    "<CORE_API_ENDPOINT>/appid-public/bulk-import/users?status=FAILED",
    headers={
        "api-key": "YOUR_API_KEY"
    },
)

The response includes the import error messages for each specific user. Use them to determine what you need to correct in your import data. Record the failed staged-row IDs and remove those exact rows before retrying. Verify that every requested ID appears in deletedIds and that invalidIds is empty; otherwise, stop and reconcile the discrepancy.

curl -X POST "<CORE_API_ENDPOINT>/appid-{appId}/bulk-import/users/remove" \
  -H "api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "ids": [
    "fa7a0841-b533-4478-9253-0fde890c576"
  ]
}'
const response = await fetch("<CORE_API_ENDPOINT>/appid-{appId}/bulk-import/users/remove", {
  method: "POST",
  headers: {
    "api-key": "YOUR_API_KEY",
    "Content-Type": "application/json"
  },
  body: JSON.stringify({
  "ids": [
    "fa7a0841-b533-4478-9253-0fde890c576"
  ]
})
});
package main

import (
	"net/http"
	"strings"
)

func main() {
	req, err := http.NewRequest("POST", "<CORE_API_ENDPOINT>/appid-{appId}/bulk-import/users/remove", strings.NewReader(`{
  "ids": [
    "fa7a0841-b533-4478-9253-0fde890c576"
  ]
}`))
	if err != nil {
		panic(err)
	}
	req.Header.Set("api-key", "YOUR_API_KEY")
	req.Header.Set("Content-Type", "application/json")
	response, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer response.Body.Close()
}
import requests

response = requests.post(
    "<CORE_API_ENDPOINT>/appid-{appId}/bulk-import/users/remove",
    headers={
        "api-key": "YOUR_API_KEY",
        "Content-Type": "application/json"
    },
    json={
  "ids": [
    "fa7a0841-b533-4478-9253-0fde890c576"
  ]
},
)

After removal, fix the source records and repeat step 3.1 only for that corrected data. Re-run the status checks and reconcile every source identity to one successfully imported account. Never treat a zero count as sufficient if the source export, removed IDs, corrected retries, and final accounts do not reconcile.

See also

API reference

API schema and response details