Skip to content
  • Manage users
  • Add and edit

Import users via CSV or JSON

You can import users in bulk, from CSV or JSON. If you are moving to Kinde from Auth0 or Firebase, see the dedicated migration guides (Auth0, Firebase) instead.

File requirements

Link to this section
  • CSV - User details and passwords need to be in UTF-8 encoded CSV format
  • NDJSON - User details and passwords need to be in NDJSON (Newline Delimited JSON) format, one user object per line
  • Firebase NDJSON - Firebase Authentication user objects in NDJSON format (one object per line), prepared from firebase auth:export
  • File size must be 49MB or less

If you’ve got large user sets (over 49MB) or are concerned about file size limits, you might consider importing in batches, or contact us for import support.

Importing from Firebase

Link to this section

If you are migrating from Firebase Authentication, at a high level you will:

  • Export your users with the Firebase CLI (firebase auth:export)
  • Convert the JSON export to NDJSON — one user object per line — with a .ndjson extension
  • Upload the file using the Firebase import type in the admin
  • Enter your Firebase project’s password hash parameters on the confirmation screen, so migrated users keep their existing password (Kinde supports Firebase’s modified scrypt hashes)

For the full step-by-step workflow, including exporting, NDJSON conversion, hash parameters, and the Firebase-to-Kinde field mapping, see Migrate to Kinde from Firebase Authentication.

Other ways to bring Firebase passwords across

Link to this section

The Firebase NDJSON import above is the simplest path, but you can also supply Firebase scrypt passwords through:

  • Custom CSV import — include the password columns described in Prepare CSV files, with hashing_method set to firebase-scrypt. Each row needs the user’s hashed_password and salt, plus the four project-level values (signer_key, salt_separator, rounds, and mem_cost) repeated on every row.
  • Kinde Management API — call Set user password with the Firebase password hash and parameters.

For example:

{
"hashed_password": "<base64-firebase-password-hash>",
"hashing_method": "firebase-scrypt",
"salt": "<base64-user-salt>",
"signer_key": "<base64-signer-key>",
"salt_separator": "Bw==",
"rounds": 8,
"mem_cost": 14,
"is_temporary_password": false
}

Firebase exports passwordHash and salt as base64-encoded strings — submit both as-is without decoding. Replace the other example values with your Firebase project hash parameters. Set is_temporary_password to false so the user can keep their existing password. PBKDF2-specific fields (iterations and variant) are rejected for Firebase scrypt, and the Firebase scrypt fields are rejected for any other hashing method.

For both paths, the first time an imported user signs in, Kinde verifies their password against the Firebase scrypt hash, then re-hashes it with bcrypt. Users keep their existing passwords and do not see a reset prompt when Custom CSV rows set password_verified to TRUE, or when the Management API call sets is_temporary_password to false as in the example above.

Before you import users

Link to this section

Cases in usernames

Link to this section

Kinde treats usernames as case-insensitive. In other words, we ignore case. We do this because it eliminates the possibility of auth issues and fraud when two usernames are identical in every aspect except the case of one of their letters.

We are happy to support users choosing an aesthetically pleasing username combination, like RosyRose or BuilderBob. We just don’t also support separate identities for rosYrosE and BUilderbob. Before importing users, we recommend checking that all usernames are unique in more than just case.

Prepare NDJSON data

Link to this section

Before importing, make sure that you set up data that you want user records to be linked to, e.g. roles, permissions, organizations. Organizations are linked based on the ‘external_organization_id’ field , the value of which needs to be added against the organization object with ‘external_id’ in your import file.

Using the NDJSON format you can import users with the following user data:

  • Permissions
  • Roles
  • Property values
  • Identities
  • Feature flags
  • Scopes
  • Profiles

NDJSON files are line separated JSON objects with each JSON object representing a user. The following schema can be used for importing NDJSON files, note this schema represents one line within the file.

{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "User",
"description": "A user object for importing",
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "A unique external identifier for the user (e.g., 'your_external_user_id_001')."
},
"password": {
"type": "object",
"description": "Password metadata including salt, hash, and algorithm used.",
"properties": {
"salt": {
"type": ["string", "null"],
"description": "The salt value used when hashing the password, if applicable."
},
"salt_format": {
"type": ["string", "null"],
"enum": ["hex", "string", null],
"description": "Format of the salt value. Can be 'hex', 'string', or null if not used."
},
"salt_position": {
"type": ["string", "null"],
"enum": ["prefix", "suffix", null],
"description": "Position of the salt relative to the password. Can be 'prefix', 'suffix', or null."
},
"hashed_password": {
"type": "string",
"description": "The resulting hashed password string."
},
"hashing_algorithm": {
"type": "string",
"enum": ["crypt", "bcrypt", "sha256", "md5", "wordpress"],
"description": "The algorithm used to hash the password."
}
}
},
"first_name": {
"type": "string",
"description": "The user's first name."
},
"last_name": {
"type": "string",
"description": "The user's last name."
},
"identities": {
"type": "array",
"description": "A list of identity records such as email, username, or social.",
"items": {
"type": "object",
"description": "An identity record associated with the user.",
"properties": {
"type": {
"type": "string",
"description": "The type of identity.",
"enum": [
"email",
"phone",
"username",
"oauth2:slack",
"oauth2:apple",
"oauth2:github",
"oauth2:facebook",
"oauth2:twitter",
"oauth2:twitch",
"oauth2:gitlab",
"oauth2:xero",
"oauth2:linkedin",
"oauth2:discord",
"oauth2:bitbucket",
"oauth2:stripe",
"oauth2:microsoft",
"oauth2:clever",
"oauth2:roblox"
]
},
"identity": {
"type": "string",
"description": "The actual identifier value (e.g., email address, username, or provider user ID)."
},
"is_verified": {
"type": "boolean",
"description": "Whether this identity has been verified."
},
"provider": {
"type": "string",
"description": "The name of the provider (e.g., 'google') for OAuth identities."
},
"profile": {
"type": "object",
"description": "Optional key/value pairs returned from the provider.",
"additionalProperties": true
}
}
}
},
"properties": {
"type": "array",
"description": "Custom metadata key-value pairs associated with the user.",
"items": {
"type": "object",
"properties": {
"key": {
"type": "string",
"description": "The name of the property."
},
"value": {
"type": "string",
"description": "The value of the property."
}
}
}
},
"feature_flags": {
"type": "array",
"description": "A list of feature flags for the user.",
"items": {
"type": "object",
"properties": {
"key": {
"type": "string",
"description": "The name of the feature flag."
},
"value": {
"type": "string",
"description": "The value of the flag."
}
}
}
},
"organizations": {
"type": "array",
"description": "Organizations this user is associated with, including roles, permissions and api scopes.",
"items": {
"type": "object",
"properties": {
"external_id": {
"type": "string",
"description": "An external identifier for the organization (e.g., 'your_external_org_id_001')."
},
"roles": {
"type": "array",
"description": "List of roles assigned to the user within the organization.",
"items": {
"type": "string"
}
},
"permissions": {
"type": "array",
"description": "List of specific permissions granted to the user within the organization.",
"items": {
"type": "string"
}
},
"scopes": {
"type": "array",
"description": "API scopes assigned to the user for this organization.",
"items": {
"type": "object",
"properties": {
"audience": {
"type": "string",
"format": "uri",
"description": "The audience or API this scope applies to (e.g., 'https://your-api.com')."
},
"scope": {
"type": "string",
"description": "The name of the scope (e.g., 'scope_1')."
}
}
}
}
}
}
}
}
}

Here’s an single-line example:

{"id":"your_external_user_id_001","password":{"salt":null,"salt_format":null,"salt_position":null,"hashed_password":"$2a$10$t8Jz3hJCCTFk/Acja7bw3OpamB3xuLPhpJlRHb31bXIjfzeTfn8rq","hashing_algorithm":"bcrypt"},"last_name":"One","first_name":"User","identities":[{"type":"username","identity":"userone"},{"type":"email","identity":"userone@example.com","is_verified":true},{"type":"oauth2:google","profile":{"custom_provider_fields":"custom key/values from google"},"identity":"123456","provider":"google","is_verified":true}],"properties":[{"key":"property_1","value":"false"}],"feature_flags":[{"key":"feature_flag_1","value":"true"}],"organizations":[{"external_id":"your_external_org_id_001","roles":["admin","member"],"permissions":["read","write"],"scopes":[{"audience":"https://your-api.com","scope":"scope_1"}]}]}

Example, but a bit easier to read.

{
"id": "your_external_user_id_001",
"password": {
"salt": null,
"salt_format": null,
"salt_position": null,
"hashed_password": "$2a$10$t8Jz3hJCCTFk/Acja7bw3OpamB3xuLPhpJlRHb31bXIjfzeTfn8rq",
"hashing_algorithm": "bcrypt"
},
"last_name": "One",
"first_name": "User",
"identities": [
{ "type": "username", "identity": "userone" },
{ "type": "email", "identity": "userone@example.com", "is_verified": true },
{
"type": "oauth2:google",
"profile": {
"custom_provider_fields": "custom key/values from google"
},
"identity": "123456",
"provider": "google",
"is_verified": true
}
],
"properties": [{ "key": "property_1", "value": "false" }],
"feature_flags": [{ "key": "feature_flag_1", "value": "true" }],
"organizations": [
{
"external_id": "your_external_org_id_001",
"roles": ["admin", "member"],
"permissions": ["read", "write"],
"scopes": [{ "audience": "https://your-api.com", "scope": "scope_1" }]
}
]
}

Prepare CSV files

Link to this section

When exporting data from another auth system or your own system via CSV, the file needs to be set up with specific headings and formats for the data you are importing. These are detailed below.

If you are migrating from Auth0, see the Prepare JSON data (for Auth0 imports) section.

  • email or phone - minimum required identity information
  • external_organization_id - assign users to organizations, optional unless you are importing roles and permissions

Other user data

Link to this section
  • first_name and last_name

  • id (also referred to as provided_id) - unique to the auth provider and helps us match records as they are imported.

  • username - if usernames are part of a user’s identity

  • phone - in the E.164 format [+][country code][number]. For example, +6155511555. Required for phone authentication.

  • phone_verified - phone number verification status: TRUE or FALSE

  • email - the user’s email address

  • email_verified - email verification status: TRUE or FALSE. TRUE only applies if you are also importing the user’s password. If they have not set a password and you set this to TRUE, they will be prompted to set one using a one-time code the first time they sign in. This verifies their identity.

  • role_key - the role key for the role a user will be assigned on import. If the user is to be assigned more than one role, use a comma separated list.

  • permission_key - the permissions key for the permission a user will be assigned (that is not included in their role). If the user is to be assigned more than one permission, use a comma separated list.

  • external_organization_id - your supplied ‘external_id’ for the organization that you want the user to be imported into. Optional unless you are importing roles and permissions with user data. If the user belongs to more than one organization, use a comma separated list wrapped in double quotes. If left empty the user will be assigned to the default organization, if the ‘Add users to this organization if no organization is specified’ policy is enabled.

Password data (optional)

Link to this section
  • hashed_password - the hashed password.
  • hashing_method - the algorithm used to hash the password. One of bcrypt, crypt, md5, sha256, wordpress, or firebase-scrypt.
  • salt - extra characters added to the password to make it stronger. See the table below for when this is required.
  • salt_position - position of the salt in the password string: prefix or suffix.
  • salt_format - format of the salt, e.g. hex or string.
  • signer_key - Firebase only. The base64-encoded signer key from your Firebase project’s password hash parameters.
  • salt_separator - Firebase only. The base64-encoded salt separator from your Firebase project’s password hash parameters.
  • rounds - Firebase only. The scrypt rounds value (1–16) from your Firebase project’s password hash parameters.
  • mem_cost - Firebase only. The scrypt memory cost value (1–20) from your Firebase project’s password hash parameters.
Hashing methodSaltSalt position
bcryptOptional
cryptOptional
md5OptionalRequired if salt included
sha256OptionalRequired if salt included
wordpressOptional
firebase-scryptRequired (base64, per user)

For firebase-scrypt passwords, every row must also include the signer_key, salt_separator, rounds, and mem_cost columns. These four values are the same for every user in a Firebase project. You can find them in the Firebase console under Authentication > Users, in the three-dots menu, as Password hash parameters.

You can use the Firebase-style column names (signerKey, saltSeparator, and memCost) instead of the snake_case names if that is how your export is labelled.

Example simple csv import

Link to this section
email,id,first_name,last_name,roles,permissions,external_organization_id
jen@kinde.com,0001,"Jen","Smith","role_1","permission_1","ext_org_id_1,ext_org_id_2
elmo@kinde.com,0002,"Elmo","Smith","role_1","permission_2","ext_org_id_1,ext_org_id_2

Users with multiple orgs and multiple roles

Link to this section

If you’re importing users who belong to multiple organizations and they have different roles in those organizations, you can set up the CSV to duplicate the user on a separate line for each organization they belong to, with the relevant roles to match. For example:

email,id,first_name,last_name,roles,permissions,external_organization_id
jen@kinde.com,0001,"Jen","Smith","role_1,role_2","permission_1,permission_2","ext_org_id_1
jen@kinde.com,0001,"Jen","Smith","role_3","permission_3,permission_4","ext_org_id_2,ext_org_id_3

Alternatively, you can import your users first, then import their roles and organizations in a separate file:

File 1
email,id,first_name,last_name
jen@kinde.com,0001,"Jen","Smith
File 2
id,roles,permissions,external_organization_id
0001,"role_1,role_2","permission_1,permission_2","ext_org_id_1
0001,"role_3,role_4","permission_3,permission_4","ext_org_id_2

How to import users

Link to this section
  1. In Kinde, go to Users, then select Import users.
  2. Select the option for your situation:
  3. Follow the on-screen prompts to import the data.
  4. If there are any errors with the import, you will be able to view them afterwards.
  5. Most import errors can be fixed by editing the CSV or NDJSON file and then re-importing into Kinde. Any records that have already been successfully imported, will be ignored.

Impact on end-users

Link to this section

Importing all your existing users and passwords should mean that your end users won’t notice anything when they next sign in. This is the optimal experience. However:

  • If a user changes their password after the user export and while the migration is in progress, they will be prompted to reset their password on the next sign in.
  • If you have set up a new authentication method as part of the user migration (for instance, going passwordless) your users will be prompted to use the new method on sign in.
  • If you add or remove roles or permissions, they may gain/lose access to parts of your system.

Weak passwords are not rejected on import

Link to this section

When you import passwords via CSV, Kinde does not check for password strength. However, if you do not also include a TRUE in the password_verified column of the CSV, Kinde will send a one-time password to the user the first time they try to sign in, in order to verify their identity.

In future, we may add the ability to check password strength and initiate a password change if it’s deemed to weak by standard password criteria.

Re-importing does not update user info

Link to this section

If you add a user via import and they start authenticating via Kinde, and then you import their records again with changes - for example, a name change or a new email - that information will not be updated in Kinde.

Similarly, if a user has changed the spelling of their name or has new permissions, and you import data from a CSV containing outdated information, the older data will NOT override their current record in Kinde.

We recommend managing updates to user information via the Kinde admin, or via API.

Communication to users

Link to this section

Kinde does not send any notifications or invitations to users when they are added to Kinde via import. The idea is that your users have a seamless experience that feels (almost) like it always has in your app.

Similarly, if you add users via API, Kinde does not send an email or notification to the user.

If you’ve made changes to their sign in experience — for example adding multi-factor authentication — then consider contacting your users to let them know their sign in experience will be changed.