> ## Documentation Index
> Fetch the complete documentation index at: https://auth0.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Bulk Import User Data into Auth0 Database Connections

> Migrate users from an existing database or service to Auth0 using bulk imports for database connections.

export const AuthCodeGroup = ({children, dropdown}) => {
  const [processedChildren, setProcessedChildren] = useState(children);
  useEffect(() => {
    let unsubscribe = null;
    function init() {
      unsubscribe = window.autorun(() => {
        const processChildren = node => {
          if (typeof node === "string") {
            let processedNode = node;
            for (const [key, value] of window.rootStore.variableStore.values.entries()) {
              const escapedKey = key.replaceAll(/[.*+?^${}()|[\]\\]/g, (String.raw)`\$&`);
              processedNode = processedNode.replaceAll(new RegExp(escapedKey, "g"), value);
            }
            return processedNode;
          } else if (Array.isArray(node)) {
            return node.map(processChildren);
          } else if (node && node.props && node.props.children) {
            return {
              ...node,
              props: {
                ...node.props,
                children: processChildren(node.props.children)
              }
            };
          }
          return node;
        };
        setProcessedChildren(processChildren(children));
      });
    }
    if (window.rootStore) {
      init();
    } else {
      window.addEventListener("adu:storeReady", init);
    }
    return () => {
      window.removeEventListener("adu:storeReady", init);
      unsubscribe?.();
    };
  }, [children]);
  return <CodeGroup dropdown={dropdown}>{processedChildren}</CodeGroup>;
};

export const AuthCodeBlock = ({filename, icon, language, highlight, children}) => {
  const [displayText, setDisplayText] = useState(children);
  const [copyText, setCopyText] = useState(children);
  const wrapperRef = React.useRef(null);
  useEffect(() => {
    let unsubscribe = null;
    function init() {
      if (!window.autorun || !window.rootStore) {
        return;
      }
      unsubscribe = window.autorun(() => {
        let processedChildrenForDisplay = children;
        let processedChildrenForCopy = children;
        for (const [key, value] of window.rootStore.variableStore.values.entries()) {
          const escapedKey = key.replaceAll(/[.*+?^${}()|[\]\\]/g, (String.raw)`\$&`);
          let displayValue = value;
          if (key === "{yourClientSecret}" && value !== "{yourClientSecret}") {
            displayValue = value.substring(0, 3) + "*****MASKED*****";
          }
          processedChildrenForDisplay = processedChildrenForDisplay.replaceAll(new RegExp(escapedKey, "g"), displayValue);
          processedChildrenForCopy = processedChildrenForCopy.replaceAll(new RegExp(escapedKey, "g"), value);
        }
        setDisplayText(processedChildrenForDisplay);
        setCopyText(processedChildrenForCopy);
      });
    }
    if (window.rootStore) {
      init();
    } else {
      window.addEventListener("adu:storeReady", init);
    }
    return () => {
      window.removeEventListener("adu:storeReady", init);
      unsubscribe?.();
    };
  }, [children]);
  useEffect(() => {
    if (!wrapperRef.current) return;
    const originalWriteText = navigator.clipboard.writeText.bind(navigator.clipboard);
    let isOverriding = false;
    const handleClick = e => {
      const button = e.target.closest('[data-testid="copy-code-button"]');
      if (!button || !wrapperRef.current.contains(button)) return;
      isOverriding = true;
      navigator.clipboard.writeText = text => {
        if (isOverriding) {
          isOverriding = false;
          navigator.clipboard.writeText = originalWriteText;
          return originalWriteText(copyText);
        }
        return originalWriteText(text);
      };
      setTimeout(() => {
        if (isOverriding) {
          isOverriding = false;
          navigator.clipboard.writeText = originalWriteText;
        }
      }, 100);
    };
    const wrapper = wrapperRef.current;
    wrapper.addEventListener('click', handleClick, true);
    return () => {
      wrapper.removeEventListener('click', handleClick, true);
      if (navigator.clipboard.writeText !== originalWriteText) {
        navigator.clipboard.writeText = originalWriteText;
      }
    };
  }, [copyText]);
  return <div ref={wrapperRef}>
      <CodeBlock filename={filename} icon={icon} language={language} lines highlight={highlight}>
        {displayText}
      </CodeBlock>
    </div>;
};

You can bulk import existing user data into an Auth0 database connection. This can be useful for migrating users from an existing database or service to Auth0.

To bulk import user data, you first create a user data file in the JSON format Auth0 expects and then start a job to import that data using the Dashboard or the Management API.

## Prerequisites

<Tabs>
  <Tab title="Auth0 Dashboard">
    * You must have a [database connection](/docs/authenticate/database-connections) to import the user data into, which can be either:
      * A database connection using the Auth0 user store.
      * A [custom database connection](/docs/authenticate/database-connections/custom-db) using your external user store with import mode on.

    * You must have one of the following [tenant member roles](/docs/get-started/manage-dashboard-access/feature-access-by-role):
      * **Admin**
      * **Editor - Users**
  </Tab>

  <Tab title="Management API">
    * You must have a [database connection](/docs/authenticate/database-connections) to import the user data into, which can be either:
      * A database connection using the Auth0 user store.
      * A [custom database connection](/docs/authenticate/database-connections/custom-db) using your external user store with import mode on.

    * You must have or create a [Management API token](/docs/secure/tokens/access-tokens/management-api-access-tokens) for job endpoint requests with the necessary scopes for the [Create import users job endpoint](/docs/api/management/v2/jobs/post-users-imports).
  </Tab>
</Tabs>

## 1. Create the user data file

First, format your existing user data into JSON for import into Auth0 following the structure defined in the [user data JSON schema and property reference](/docs/manage-users/user-migration/bulk-user-import-database-schema-and-examples).

The following considerations may be helpful as you create the user data file:

* The file size limit for a bulk import is 500KB. If your data exceeds this limit, you need to divide your user data into multiple smaller files to upload across multiple jobs.

  As a guideline, approximately 1,000 users with fewer than 10 metadata fields per user is typically under the file size limit.

* If you're starting with an [Auth0 user data export](/docs/manage-users/user-migration/bulk-user-exports):

  * Bulk imports support JSON format, but user exports generated by Auth0 are in [NDJSON](https://github.com/ndjson/ndjson-spec) format. You can convert from NDJSON to JSON using tools like [`jq`](https://jqlang.org/).

  * Bulk imports automatically add the `auth0|` prefix to imported user IDs, but user exports generated by Auth0 already have the `auth0|` prefix on user IDs.

  To keep the same user IDs, remove the `auth0|` prefix from all imported user IDs. Otherwise, the user IDs will have the prefix twice (`auth0|auth0|<user_id>`).

* You can import user data with passwords hashed by a [supported algorithm](./bulk-user-import-schema#param-algorithm). Users with passwords hashed by unsupported algorithms must reset their password when they log in for the first time after the bulk import.

  If a user did not log in with the `custom_password_hash` you imported initially, you can update it by resubmitting the user data with a different value for `custom_password_hash` in a new job with upserts enabled.

## 2. Import the user data into your database connection

<Tabs>
  <Tab title="Auth0 Dashboard">
    To import user data using the Auth0 Dashboard:

    1. Go to [**Dashboard > User Management > Users**](https://manage.auth0.com/#/users).

    2. In the top right corner of the page, select **Import/Export Users** to go to the **Import/Export Users** page.

    3. Select **Import Users** to go to the **Import Users** page.

           <Frame>
             <img src="https://mintlify.s3.us-west-1.amazonaws.com/auth0/docs/images/cdy7uua7fh8z/KrknYhAUOaYBzzBrgHp4M/b2389f5926122a306eab10e54b917da6/import.png" alt="" />
           </Frame>

    4. Under **Users JSON file**, select **+ Choose File** and upload the user data JSON file.

    5. Under **Connection**, open the drop-down menu and select the database connection where you want to import the user data.

    6. Optionally, check **Upsert pre-existing users in connection**. When an imported user matches an existing user in the database connection, you can choose whether or not to upsert the data:

       * By default, upserts are disabled. Imports of users that match on email address, user ID, phone, or username fail.

       * With upserts enabled, imports of users matching on email address update all [upsertable attributes](/docs/manage-users/user-migration/bulk-user-import-database-schema-and-examples).

    7. Optionally, check **Send completion email to all tenant owners**. When enabled, this sends a completion email to all tenant administrators when the job succeeds or fails.

    8. To submit the job, select **Import Users**.
  </Tab>

  <Tab title="Management API">
    To import user data using the Management API, call the [Create Import Users Job endpoint](/docs/api/management/v2/jobs/post-users-imports) (`POST /jobs/users-imports`). View the endpoint documentation for details about the body parameters and response schemas.

    To use the examples below:

    * Replace the placeholder variables `MGMT_API_ACCESS_TOKEN`, `USERS_IMPORT_FILE.json`, `CONNECTION_ID`, and `EXTERNAL_ID` with your own values.

          <Tip>
            You can [view the database connection's ID in the Dashboard](/docs/authenticate/identity-providers/locate-the-connection-id) or get it with the Management API's [Get all connections endpoint](/docs/api/management/v2/connections/get-connections).
          </Tip>

    * Optionally, set `upsert`. When an imported user matches an existing user in the database connection, you can choose whether or not to upsert the data:

      * By default, upserts are disabled. Imports of users that match on email address, user ID, phone, or username fail.

      * With `upsert` enabled, imports of users matching on email address update all [upsertable attributes](/docs/manage-users/user-migration/bulk-user-import-database-schema-and-examples).

    * Optionally, set \`send\_completion\_email to send a completion email to all tenant administrators when the job succeeds or fails.

    <AuthCodeGroup>
      ```bash cURL theme={null}
      curl --request POST \
        --url 'https://{yourDomain}/api/v2/jobs/users-imports' \
        --header 'authorization: Bearer MGMT_API_ACCESS_TOKEN' \
        --form users=@USERS_IMPORT_FILE.json \
        --form connection_id=CONNECTION_ID \
        --form external_id=EXTERNAL_ID
      ```

      ```csharp C# theme={null}
      var client = new RestClient("https://{yourDomain}/api/v2/jobs/users-imports");
      var request = new RestRequest(Method.POST);
      request.AddHeader("authorization", "Bearer MGMT_API_ACCESS_TOKEN");
      request.AddHeader("content-type", "multipart/form-data; boundary=---011000010111000001101001");
      request.AddParameter("multipart/form-data; boundary=---011000010111000001101001", "-----011000010111000001101001\r
      Content-Disposition: form-data; name="users"; filename="USERS_IMPORT_FILE.json"\r
      Content-Type: text/json\r
      \r
      \r
      -----011000010111000001101001\r
      Content-Disposition: form-data; name="connection_id"\r
      \r
      CONNECTION_ID\r
      -----011000010111000001101001\r
      Content-Disposition: form-data; name="external_id"\r
      \r
      EXTERNAL_ID\r
      -----011000010111000001101001--\r
      ", ParameterType.RequestBody);
      IRestResponse response = client.Execute(request);
      ```

      ```go Go theme={null}
      package main

      import (
      	"fmt"
      	"strings"
      	"net/http"
      	"io/ioutil"
      )

      func main() {

      	url := "https://{yourDomain}/api/v2/jobs/users-imports"

      	payload := strings.NewReader("-----011000010111000001101001\r
      Content-Disposition: form-data; name="users"; filename="USERS_IMPORT_FILE.json"\r
      Content-Type: text/json\r
      \r
      \r
      -----011000010111000001101001\r
      Content-Disposition: form-data; name="connection_id"\r
      \r
      CONNECTION_ID\r
      -----011000010111000001101001\r
      Content-Disposition: form-data; name="external_id"\r
      \r
      EXTERNAL_ID\r
      -----011000010111000001101001--\r
      ")

      	req, _ := http.NewRequest("POST", url, payload)

      	req.Header.Add("authorization", "Bearer MGMT_API_ACCESS_TOKEN")
      	req.Header.Add("content-type", "multipart/form-data; boundary=---011000010111000001101001")

      	res, _ := http.DefaultClient.Do(req)

      	defer res.Body.Close()
      	body, _ := ioutil.ReadAll(res.Body)

      	fmt.Println(res)
      	fmt.Println(string(body))

      }
      ```

      ```java Java theme={null}
      HttpResponse<String> response = Unirest.post("https://{yourDomain}/api/v2/jobs/users-imports")
        .header("authorization", "Bearer MGMT_API_ACCESS_TOKEN")
        .header("content-type", "multipart/form-data; boundary=---011000010111000001101001")
        .body("-----011000010111000001101001\r
      Content-Disposition: form-data; name="users"; filename="USERS_IMPORT_FILE.json"\r
      Content-Type: text/json\r
      \r
      \r
      -----011000010111000001101001\r
      Content-Disposition: form-data; name="connection_id"\r
      \r
      CONNECTION_ID\r
      -----011000010111000001101001\r
      Content-Disposition: form-data; name="external_id"\r
      \r
      EXTERNAL_ID\r
      -----011000010111000001101001--\r
      ")
        .asString();
      ```

      ```javascript Node.JS theme={null}
      var axios = require("axios").default;

      var options = {
        method: 'POST',
        url: 'https://{yourDomain}/api/v2/jobs/users-imports',
        headers: {
          authorization: 'Bearer MGMT_API_ACCESS_TOKEN',
          'content-type': 'multipart/form-data; boundary=---011000010111000001101001'
        },
        data: '-----011000010111000001101001\r
      Content-Disposition: form-data; name="users"; filename="USERS_IMPORT_FILE.json"\r
      Content-Type: text/json\r
      \r
      \r
      -----011000010111000001101001\r
      Content-Disposition: form-data; name="connection_id"\r
      \r
      CONNECTION_ID\r
      -----011000010111000001101001\r
      Content-Disposition: form-data; name="external_id"\r
      \r
      EXTERNAL_ID\r
      -----011000010111000001101001--\r
      '
      };

      axios.request(options).then(function (response) {
        console.log(response.data);
      }).catch(function (error) {
        console.error(error);
      });
      ```

      ```php PHP theme={null}
      $curl = curl_init();

      curl_setopt_array($curl, [
        CURLOPT_URL => "https://{yourDomain}/api/v2/jobs/users-imports",
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_ENCODING => "",
        CURLOPT_MAXREDIRS => 10,
        CURLOPT_TIMEOUT => 30,
        CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
        CURLOPT_CUSTOMREQUEST => "POST",
        CURLOPT_POSTFIELDS => "-----011000010111000001101001\r
      Content-Disposition: form-data; name="users"; filename="USERS_IMPORT_FILE.json"\r
      Content-Type: text/json\r
      \r
      \r
      -----011000010111000001101001\r
      Content-Disposition: form-data; name="connection_id"\r
      \r
      CONNECTION_ID\r
      -----011000010111000001101001\r
      Content-Disposition: form-data; name="external_id"\r
      \r
      EXTERNAL_ID\r
      -----011000010111000001101001--\r
      ",
        CURLOPT_HTTPHEADER => [
          "authorization: Bearer MGMT_API_ACCESS_TOKEN",
          "content-type: multipart/form-data; boundary=---011000010111000001101001"
        ],
      ]);

      $response = curl_exec($curl);
      $err = curl_error($curl);

      curl_close($curl);

      if ($err) {
        echo "cURL Error #:" . $err;
      } else {
        echo $response;
      }
      ```

      ```python Python theme={null}
      import http.client

      conn = http.client.HTTPSConnection("{yourDomain}")

      # Read the users import file
      with open("USERS_IMPORT_FILE.json", "rb") as f:
          file_contents = f.read()

      payload = (
          "-----011000010111000001101001\r\n"
          "Content-Disposition: form-data; name=\"users\"; filename=\"USERS_IMPORT_FILE.json\"\r\n"
          "Content-Type: application/json\r\n"
          "\r\n"
          + file_contents.decode("utf-8") + "\r\n"
          "-----011000010111000001101001\r\n"
          "Content-Disposition: form-data; name=\"connection_id\"\r\n"
          "\r\n"
          "CONNECTION_ID\r\n"
          "-----011000010111000001101001\r\n"
          "Content-Disposition: form-data; name=\"external_id\"\r\n"
          "\r\n"
          "EXTERNAL_ID\r\n"
          "-----011000010111000001101001--\r\n"
      )

      headers = {
          'authorization': "Bearer MGMT_API_ACCESS_TOKEN",
          'content-type': "multipart/form-data; boundary=---011000010111000001101001"
          }

      conn.request("POST", "/api/v2/jobs/users-imports", payload, headers)

      res = conn.getresponse()
      data = res.read()

      print(data.decode("utf-8"))
      ```

      ```ruby Ruby theme={null}
      require 'uri'
      require 'net/http'
      require 'openssl'

      url = URI("https://{yourDomain}/api/v2/jobs/users-imports")

      http = Net::HTTP.new(url.host, url.port)
      http.use_ssl = true
      http.verify_mode = OpenSSL::SSL::VERIFY_NONE

      request = Net::HTTP::Post.new(url)
      request["authorization"] = 'Bearer MGMT_API_ACCESS_TOKEN'
      request["content-type"] = 'multipart/form-data; boundary=---011000010111000001101001'
      request.body = "-----011000010111000001101001\r
      Content-Disposition: form-data; name="users"; filename="USERS_IMPORT_FILE.json"\r
      Content-Type: text/json\r
      \r
      \r
      -----011000010111000001101001\r
      Content-Disposition: form-data; name="connection_id"\r
      \r
      CONNECTION_ID\r
      -----011000010111000001101001\r
      Content-Disposition: form-data; name="external_id"\r
      \r
      EXTERNAL_ID\r
      -----011000010111000001101001--\r
      "

      response = http.request(request)
      puts response.read_body
      ```
    </AuthCodeGroup>

    When you successfully create a job, the return value includes information about the job, including the job ID.
  </Tab>
</Tabs>

## 3. Check job status

<Tabs>
  <Tab title="Auth0 Dashboard">
    To check the status of a job:

    1. Go to [**Dashboard > User Management > Users**](https://manage.auth0.com/#/users).

    2. In the top right corner of the page, select **Import/Export Users** to go to the **Import/Export Users** page. Once you start at least one job, this page lists your submitted jobs.

    3. To view more information about a job, select **More info** next to any job.

    Job statuses include **Job creation failed**, **Job created, user import failed**, and **Job created, user import succeeded**.
  </Tab>

  <Tab title="Management API">
    To check the status of a job, call the [Get a job endpoint](/docs/api/management/v2/jobs/get-jobs-by-id) (`GET /jobs`) with the ID of the job. View the endpoint documentation for details about endpoint parameters and response schemas.

    The `status` of the returned job can be `pending`, `completed`, or `failed`. If the job completed, the response's `summary` object includes totals of successful, failed, inserted, and updated records.

    If a job fails, you can get the error details by calling the [Get job error details endpoint](https://auth0.com/docs/api/management/v2#!/Jobs/get_errors) (`GET /jobs/{id}/errors`) with the ID of the job. Each error object in the response includes an error code and a message explaining the error in more detail. The possible error codes are:

    * `ANY_OF_MISSING`
    * `ARRAY_LENGTH_LONG`
    * `ARRAY_LENGTH_SHORT`
    * `CONFLICT`
    * `CONFLICT_EMAIL`
    * `CONFLICT_USERNAME`
    * `CONNECTION_NOT_FOUND`
    * `DUPLICATED_USER`
    * `ENUM_MISMATCH`
    * `FORMAT`
    * `INVALID_TYPE`
    * `MAX_LENGTH`
    * `MAXIMUM`
    * `MFA_FACTORS_FAILED`
    * `MIN_LENGTH`
    * `MINIMUM`
    * `NOT_PASSED`
    * `OBJECT_REQUIRED`
    * `PATTERN`
  </Tab>
</Tabs>

Jobs fail if there is an error, but not if there is invalid user information (like an invalid email).

## Limits

* There is a limit of two concurrent import jobs per tenant.

* User import jobs time out after two (2) hours. If a job does not complete within this time frame, it is marked as failed.

* All job-related data is automatically deleted after 24 hours.

* Duplicated user entries in the import file cause an error. They do not cause an import followed by an upsert.

* Upserts do not merge `user_metadata` or `app_metadata`. The existing metadata is completely overwritten with the new metadata.

## Best practices for large-scale migrations

When importing large quantities of user data (requiring 10 or more jobs), we recommend the following strategies:

* **Use a job scheduler framework** (such as [Bull](https://github.com/OptimalBits/bull) or [Agenda](https://github.com/agenda/agenda)). This lets you:

  * Enforce the required concurrency limit.

  * Include a retry strategy to handle network connection drops and temporary failures.

  * Store job results and error details beyond the deletion window.

* **Don't interrupt imports on single user failures.**  Instead, implement a "finalizer" job that reviews all spawned jobs and collects failed records into a new file. Fix any errors in the collected failed records and import them as a new job.

* **Use caution when enabling `upsert` mode.** Imports with upsert are slower than standard imports and do not merge metadata. Use upsert mode when you need to update existing users and understand these limitations.
