Manually add and edit users
Manage users
You can import users in bulk, from CSV or JSON. If you are moving to Kinde from Auth0, see this guide instead.
If you’ve got large user sets (over 20MB) or are concerned about file size limits, you might consider importing in batches, or contact us for import support.
Note: Importing users from Entra ID (formerly Azure) Set up the MS Entra ID connection in Kinde before you import your users. Then when you import, Kinde will match users to the relevant connection based on their email address.
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.
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:
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" }] } ]}
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 informationexternal_organization_id
- assign users to organizations, optional unless you are importing roles and permissionsThere are special considerations if you are importing users who will share a phone number to authenticate via a mobile device. Contact support@kinde.com for advice on the best way to import users where the phone is the primary identity and numbers are shared.
The more data that you include for import, the easier we can set up your users in Kinde. Kinde will not duplicate users with existing email addresses.
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.
Note that this is NOT the same as the Organization code in Kinde, which is a Kinde-supplied ID. You can define your ID in the organization ‘external_id’ field.
hashed_password
- the user’s password encrypted using a hashing method or algorithm.hashing_method
- the name of the algorithm used to encrypt the user’s password. Currently crypt, bcrypt, sha256, md5, and wordpress are supported. Contact us if you need a different method.salt
- extra characters added to passwords to make them strongersalt_position
- position of salt in password string. E.g. prefix (before) or suffix (after).salt_format
- format of the salt, e.g. hex, string, etc.Please note if you are importing bcrypt hashes with the $2b variant, Kinde will substitute this for the $2a variant. These are interchangeable as long as you were not running OpenBSD at the time the hashes were generated.
Provide the hash in hex format. Import the salt using the salt
column. For the salt_format
, specify how the salt should be interpreted: e.g. hex for a hex-encoded string (68656c6c6f for hello). By default, the salt is treated as a plain string, and escape sequences (like \n or \v) are treated as literal characters.
Hashing method | Salt | Salt position |
---|---|---|
md5 | Optional | required if salt included |
bcrypt | ||
crypt | Optional | |
wordpress | Optional | |
sha256 | Optional | required if salt included |
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_2elmo@kinde.com,0002,"Elmo","Smith","role_1","permission_2","ext_org_id_1,ext_org_id_2
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_1jen@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 1email,id,first_name,last_name
jen@kinde.com,0001,"Jen","Smith
File 2id,roles,permissions,external_organization_id0001,"role_1,role_2","permission_1,permission_2","ext_org_id_10001,"role_3,role_4","permission_3,permission_4","ext_org_id_2
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:
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.
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.
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.