# Importing users

> Move users from Auth0, Firebase, Cognito, Supabase, Clerk, Keycloak, Okta or your own database, with or without their passwords.

Source: https://9in.io/docs/importing-users/

You can bring existing users to 9in without asking them to sign up again, and usually without asking for a new password.

| Your users are in | Do this |
|---|---|
| Auth0, Firebase, Cognito, Supabase, Clerk, Keycloak or Okta | Export them, import the file in the console. Missing passwords come over with lazy migration. |
| Your own database | Import a JSON or CSV file or use the Management API, or keep the database and connect it as an [adapter](https://9in.io/docs/own-database/) with *Import users on first login*. |
| SSOfy | See [Coming from SSOfy](https://9in.io/docs/coming-from-ssofy/). |

## Import in the console

**Import Users → Import a file**. Pick the source, drop the export, and 9in converts and checks it **in your browser**, showing what will be imported, what is skipped and why, before anything is uploaded. Then:

- **Keep user ids:** the old ids become 9in user ids, so the `sub` your apps already store doesn't change (`auth0|60425dc4` stays `auth0|60425dc4`).
- **Existing users:** skip them, or update them with the file's data.

Social identities (Google, GitHub…) and original sign-up dates come along.

## Passwords

| Format | Typical source | |
|---|---|---|
| bcrypt (`$2a$`, `$2b$`, `$2y$`) | Auth0 (hash export via support), Supabase, Clerk, most frameworks | Imported as is |
| Argon2 (`argon2id`, `argon2i`, `argon2d`) | Keycloak, modern apps | Imported as is |
| PBKDF2 (SHA-1, SHA-256, SHA-512) | Keycloak, Django | Imported as is |
| Firebase scrypt | Firebase (with your project's hash parameters) | Imported as is |

Imported hashes are checked as they are and upgraded to argon2id the next time each user signs in.

## Lazy migration

Some providers don't export passwords (Cognito, Okta, Auth0 without the hash export). With **lazy migration** (**Import Users → Lazy migration**), when someone signs in and 9in has no password for them, 9in checks the password once with the old provider, then keeps it:

| Source | Needs |
|---|---|
| Auth0 | Tenant domain, a client ID and secret with the password grant, the database connection name |
| Amazon Cognito | Region and an app client with `ALLOW_USER_PASSWORD_AUTH` |
| Firebase | The Web API key |
| Supabase | The project URL and anon key |
| Any OIDC provider with the password grant (Okta, Keycloak, Ping…) | Issuer, client ID and secret |

Users who never come back can reset their password or sign in with a one-time code. Turn lazy migration off when you're done: no more calls to the old provider.

## Import with the API

`POST /api/v1/users/import` takes up to 500 users per request (50 if they carry plain-text passwords), with the `management:users:write` scope:

```http
POST https://acme.9in.io/api/v1/users/import
Authorization: Bearer <token>
Content-Type: application/json

{
  "on_conflict": "skip",
  "source": "legacy",
  "users": [
    {
      "id": "42",
      "email": "jane@acme.com",
      "emailVerified": true,
      "name": "Jane Doe",
      "passwordHash": "$2b$10$…",
      "createdAt": 1680343200000,
      "metadata": { "plan": "pro", "fields": { "company": "Acme" } },
      "identities": [{ "connection": "social:google", "subject": "1098…" }]
    }
  ]
}
```

```json
{ "results": [{ "index": 0, "ok": true, "status": "created", "id": "42" }] }
```

Each record succeeds or fails on its own; results come back in order with `created`, `updated`, `skipped` (with a `reason`) or `failed` (with an `error` and `details`). Records match existing users by id, email, phone, username or social identity. Every imported user produces a `user.imported` entry in the activity log.

Identity connections are `social:<provider>`, `enterprise:<connection id>` or `migration:<source>`. Ids may contain letters, digits and `| _ . : @ + -` (up to 128 characters).

## Checklist

1. Import into the **sandbox** first (`https://acme-sandbox.9in.io`), try signing in as a few users, then import into production.
2. If ids change (you didn't keep them), map them in your app using the email or a value you put in `metadata`.
3. Turn on the [sign-in methods](https://9in.io/docs/sign-in-methods/) and [social providers](https://9in.io/docs/social-login/) your users had.
