> For the complete documentation index, see [llms.txt](https://docs.joinflo.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.joinflo.com/flo-apis/user-api/members-csv-import.md).

# Members CSV Import

### Endpoints <a href="#endpoints" id="endpoints"></a>

| Method | Path                                                 | Purpose                                                                                                                                                                                |
| ------ | ---------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `POST` | `/members/import-csv?product={Product}`              | Creates new members and updates existing members from CSV input. Returns created and updated counts, or a detailed error response. Rejects all rows if any single row contains errors. |
| `GET`  | `/members/example-csv/{product}/{numRows}`           | Downloads a sample CSV pre-populated with example rows appropriate for your org's office locations, SSO configuration, etc. `numRows` is capped at 1000.                               |
| `GET`  | `/members/import-instructions.pdf?product={Product}` | Downloads a PDF reference guide for the import flow. Recruit/University variants omit the Performance-only columns.                                                                    |

Both write paths accept `multipart/form-data` with the CSV provided in the `file` part. The `product` query parameter is required on `import-csv` and is one of the values defined by the `Product` enum in the OpenAPI spec (e.g. `PERFORM`, `RECRUIT`).

#### Authorization <a href="#authorization" id="authorization"></a>

* `product=PERFORM` requires the **Perform Admin** role.
* All other products require the **Admin** role.

### File-level requirements <a href="#file-level-requirements" id="file-level-requirements"></a>

| Requirement   | Value                                                                                                                                                                                                  |
| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Encoding      | UTF-8. An optional UTF-8 BOM is accepted and stripped.                                                                                                                                                 |
| Max file size | 10 MB                                                                                                                                                                                                  |
| Line endings  | LF or CRLF                                                                                                                                                                                             |
| Header row    | Required. Must include at least the three required columns (see [Header rules](http://localhost:63342/markdownPreview/908541821/markdown-preview-index-26vd40t631gtlv10sm4r3ve57r.html#header-rules)). |

There is no enforced row-count cap, but very large files should respect the 10 MB size limit.

### Header rules <a href="#header-rules" id="header-rules"></a>

Column matching is **case-insensitive** and **order-independent**. Header rules are otherwise strict:

* **Required headers must be present.** `First Name`, `Last Name`, and `Email` must all appear in the header row. Missing any of them rejects the file with `MISSING_REQUIRED_COLUMNS`.
* **Unknown headers reject the file.** Any header not in the recognized column list below causes a `UNEXPECTED_COLUMNS` file error. There is no "ignore extras" behavior — clean up your column names before submitting.
* **Which columns you include matters for updates.** When updating an existing member, the *presence* of a column header (independent of whether its cells are blank) controls whether that field is preserved or cleared. See [Update semantics](http://localhost:63342/markdownPreview/908541821/markdown-preview-index-26vd40t631gtlv10sm4r3ve57r.html#update-semantics) below.

#### Recognized headers (case-insensitive) <a href="#recognized-headers-case-insensitive" id="recognized-headers-case-insensitive"></a>

```
Employee ID, First Name, Last Name, Email, Job Title, Level,
Office City, Office State (US Only), Office Country (Non-US Only),
Department, Practice Area, Law School, Graduation Year,
Effective Class Year, Start Date, Role, SSO ID, Use MFA,
Bio Link, Work Arrangement,
New Email, FLSA Status, Manager Email, Mentor Emails
```

### Column reference <a href="#column-reference" id="column-reference"></a>

All required-ness, type, and constraint information below reflects the **actual validator behavior**. A few items here differ from the inline `description` text on the endpoint — when in doubt, this document is the source of truth.

| Column                           | Type           | Required | Constraints                                                                                                                                                                                                                                     | Example                       |
| -------------------------------- | -------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------- |
| **First Name**                   | string         | Yes      | Must not contain: backslash, carriage return, newline, less-than sign, greater-than sign, pipe, double quote, or backtick.                                                                                                                      | `Jane`                        |
| **Last Name**                    | string         | Yes      | Same disallowed-character set as First Name.                                                                                                                                                                                                    | `Smith`                       |
| **Email**                        | string         | Yes      | Must be a well-formed email address. Must be unique across rows in the file (case-insensitive).                                                                                                                                                 | `jane.smith@firm.com`         |
| **Employee ID**                  | string         | No       | Treated as an opaque identifier. When provided, must be unique across rows in the file (case-insensitive). Not required to be numeric.                                                                                                          | `E-1042`                      |
| **Job Title**                    | string         | No       | None.                                                                                                                                                                                                                                           | `Senior Associate`            |
| **Level**                        | string         | No       | Must match one of the org's configured member levels (case-insensitive). **Only persisted when `product=PERFORM`** — silently ignored for other products.                                                                                       | `Senior`                      |
| **Office City**                  | string         | No       | When provided, exactly one of Office State or Office Country must also be provided. See [Office location rules](http://localhost:63342/markdownPreview/908541821/markdown-preview-index-26vd40t631gtlv10sm4r3ve57r.html#office-location-rules). | `New York`                    |
| **Office State (US Only)**       | string         | No       | Mutually exclusive with Office Country. Must accompany Office City when set.                                                                                                                                                                    | `NY`                          |
| **Office Country (Non-US Only)** | string         | No       | Mutually exclusive with Office State. Must accompany Office City when set.                                                                                                                                                                      | `United Kingdom`              |
| **Department**                   | string         | No       | When provided, must match one of the org's configured departments.                                                                                                                                                                              | `Litigation`                  |
| **Practice Area**                | string         | No       | When provided, must match one of the org's configured practice areas.                                                                                                                                                                           | `Corporate`                   |
| **Law School**                   | string         | No       | When provided, must match one of the configured law schools. See [Education writes](http://localhost:63342/markdownPreview/908541821/markdown-preview-index-26vd40t631gtlv10sm4r3ve57r.html#education-writes) for upsert behavior.              | `Columbia Law School`         |
| **Graduation Year**              | string         | No       | 4 digits, must be within 200 years of the current year. **Only persisted when `product=PERFORM`** — silently ignored for other products.                                                                                                        | `2018`                        |
| **Effective Class Year**         | string         | No       | 4 digits, must be within 200 years of the current year. **Only persisted when `product=PERFORM`** — silently ignored for other products.                                                                                                        | `2020`                        |
| **Start Date**                   | date           | No       | Format `yyyy-MM-dd`.                                                                                                                                                                                                                            | `2025-06-03`                  |
| **Role**                         | enum           | No       | `ADMIN` or `MEMBER` (case-sensitive). If omitted or blank on a new member, defaults to `MEMBER`.                                                                                                                                                | `MEMBER`                      |
| **SSO ID**                       | string         | No       | Required only when your org uses SSO and is not configured to use email-as-SSO-ID. Otherwise ignored. Specific validation depends on your org's SSO configuration.                                                                              | `jsmith`                      |
| **Use MFA**                      | boolean string | No       | One of (case-insensitive): `true`, `t`, `1`, `yes`, `y`, `false`, `f`, `0`, `no`, `n`.                                                                                                                                                          | `true`                        |
| **Bio Link**                     | URL            | No       | Must be a syntactically valid URL.                                                                                                                                                                                                              | `https://firm.com/bio/jsmith` |
| **Work Arrangement**             | enum           | No       | One of: `REMOTE`, `HYBRID`, `IN_PERSON` (case-sensitive).                                                                                                                                                                                       | `HYBRID`                      |

#### Field handling rules that apply to every column <a href="#field-handling-rules-that-apply-to-every-column" id="field-handling-rules-that-apply-to-every-column"></a>

* **Leading and trailing whitespace is trimmed** from every value before validation.
* A field whose content is whitespace-only is treated as **missing** (`null`).
* Embedded commas, double quotes, and newlines in a value must be CSV-escaped per RFC 4180 (wrap the value in double quotes; double up internal `"`).

### Office location rules <a href="#office-location-rules" id="office-location-rules"></a>

The Office City / Office State / Office Country triad is validated as a unit. All four combinations below are valid:

| City | State | Country | Valid?              |
| ---- | ----- | ------- | ------------------- |
| —    | —     | —       | Yes (no office set) |
| set  | set   | —       | Yes (US office)     |
| set  | —     | set     | Yes (non-US office) |
| —    | —     | —       | Yes                 |

Anything else produces a `ROW_VALUE_CONFLICT` or `EMPTY_REQUIRED_VALUE` row error:

* Office State or Office Country without Office City → conflict on the offending field.
* Office State **and** Office Country both set on the same row → conflict on both.
* Office City set with neither State nor Country → missing-required on both State and Country.

Beyond format-correctness, the resulting office location must match one of your org's configured offices (`INVALID_LIST_SELECTION` if it does not).

### Discovering valid list values <a href="#discovering-valid-list-values" id="discovering-valid-list-values"></a>

Five CSV columns require values that match data configured for your org or maintained globally by Flo. Use the endpoints below to retrieve the current set of valid values before building your CSV. All listing endpoints require admin authentication using the same scheme as `/members/import-csv`.

| CSV column                        | Endpoint                                                                   | Scope                                                 | Returns                                                                                                                                                                                                                                                                                                                                                              |
| --------------------------------- | -------------------------------------------------------------------------- | ----------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Law School**                    | `GET /lawSchoolsList.csv`                                                  | Global (same for every org)                           | CSV with the full list of recognized law schools. **A snapshot is also embedded below at** [**Law School values**](http://localhost:63342/markdownPreview/908541821/markdown-preview-index-26vd40t631gtlv10sm4r3ve57r.html#law-school-values) **for convenience.**                                                                                                   |
| **Practice Area**                 | `GET /practiceAreas/current` (JSON), or `GET /practiceAreasList.csv` (CSV) | Global static list + your org's custom list, combined | Every practice area that will pass validation for your org, each tagged with its type (static vs. org-custom). The static list alone is available at `GET /practiceAreasStaticList`; your org's custom list alone is available at `GET /firmPracticeAreasList`.                                                                                                      |
| **Department**                    | `GET /departments` (JSON), or `GET /departments.csv` (CSV)                 | Per-org                                               | Your org's configured departments.                                                                                                                                                                                                                                                                                                                                   |
| **Office City / State / Country** | `GET /offices` (JSON)                                                      | Per-org                                               | Your org's configured office locations, each as a (city, state, country) triple. Populate the three Office columns in your CSV to exactly match one of these triples — see [Office location rules](http://localhost:63342/markdownPreview/908541821/markdown-preview-index-26vd40t631gtlv10sm4r3ve57r.html#office-location-rules) for the format rules within a row. |
| **Level**                         | `GET /org-member-levels` (JSON)                                            | Per-org (Performance only)                            | Your org's configured member levels. Returned only when your org has the member-levels feature enabled. Non-Performance imports ignore the `Level` column regardless of the list.                                                                                                                                                                                    |

#### Law School values <a href="#law-school-values" id="law-school-values"></a>

**Snapshot as of 2026-06-02.** \~330 recognized law schools, sorted case-insensitively. Re-fetch from `GET /lawSchoolsList.csv` for the latest set — additions happen periodically.

<details>

<summary>Click to expand: full law school list (sorted case-insensitively)</summary>

```
```

</details>

#### Notes on using these endpoints <a href="#notes-on-using-these-endpoints" id="notes-on-using-these-endpoints"></a>

* **Lists can change between imports.** New law schools are added over time; orgs can add/edit their own departments, offices, practice areas, and member levels. Re-fetch before each large import rather than caching long-term.
* **Comparison in member-import validation is case-insensitive but otherwise exact.** Trailing whitespace in your CSV is trimmed before comparison; internal whitespace is not normalized — `"New York"` (two spaces) will not match `"New York"`.
* **Practice-area availability is feature-flag gated.** Your org's custom practice areas only count as valid values when the `custom-practice-areas` feature is enabled for your org. `GET /practiceAreas/current` returns the actual merged set that will pass validation, so always prefer it over the static-only endpoint for pre-import checks.
* **`/officesStaticList` exists but is not the right list for imports.** It returns a global suggestion list used by the office-creation UI; member-import validation runs against your org's *configured* offices from `GET /offices`.

#### Fixed-enum columns (no endpoint needed) <a href="#fixed-enum-columns-no-endpoint-needed" id="fixed-enum-columns-no-endpoint-needed"></a>

The following columns accept a small fixed set of values that doesn't vary per org. The accepted values are listed directly in the [Column reference](http://localhost:63342/markdownPreview/908541821/markdown-preview-index-26vd40t631gtlv10sm4r3ve57r.html#column-reference) and reproduced here for convenience:

| Column               | Accepted values                                                                  |
| -------------------- | -------------------------------------------------------------------------------- |
| **Role**             | `ADMIN`, `MEMBER` (case-sensitive). Defaults to `MEMBER` on new rows when blank. |
| **Work Arrangement** | `REMOTE`, `HYBRID`, `IN_PERSON` (case-sensitive).                                |
| **Use MFA**          | `true`, `t`, `1`, `yes`, `y`, `false`, `f`, `0`, `no`, `n` (case-insensitive).   |

### Cross-row rules <a href="#cross-row-rules" id="cross-row-rules"></a>

* **Duplicate Email across rows** → `DUPLICATE_VALUE` on every involved row (case-insensitive comparison).
* **Duplicate Employee ID across rows** → `DUPLICATE_VALUE` on every involved row (case-insensitive comparison). Empty Employee IDs are not considered duplicates of each other.

### Recommended workflow <a href="#recommended-workflow" id="recommended-workflow"></a>

There is no separate "validate" endpoint — `/members/import-csv` performs validation and the write in a single transaction:

1. Build the CSV. To get a template tailored to your org (with valid office locations, SSO id format, etc.), `GET /members/example-csv/{product}/{numRows}`.
2. `POST /members/import-csv?product={Product}` with the file.
3. On `200 OK`, parse `createdCount` and `updatedCount` from the response body.
4. On `400 Bad Request`, parse the validation result body to determine whether the failure was file-level (`fileError`) or row-level (`rowColumnErrors`). Fix and retry; the call is rejected as a whole, so partial state is never written.

### Update semantics <a href="#update-semantics" id="update-semantics"></a>

Members are matched by **Email** (case-insensitive). A row with an Email that matches an existing member in your org becomes an **update**; otherwise it becomes a **create**.

#### Updates: present-column rule <a href="#updates-present-column-rule" id="updates-present-column-rule"></a>

For an existing member, the *presence of a column header* in your file controls whether that field gets touched:

| Situation                                     | Effect on the existing field                        |
| --------------------------------------------- | --------------------------------------------------- |
| Column header **absent** from the CSV         | Existing value is **preserved**.                    |
| Column header **present**, cell **non-blank** | Existing value is **replaced** with the cell value. |
| Column header **present**, cell **blank**     | Existing value is **cleared**.                      |

This lets you do precise partial updates: include only the columns you want to change.

**Exceptions and special cases:**

* **First Name, Last Name, Email** are always required regardless. Their headers must be present and their cells non-blank on every row.
* **Office City / Office State / Office Country** are treated as a single unit. If none of the three column headers are present, the existing office location is preserved. If any one of them is present, all three are taken from the CSV (with the usual triad rules).
* **Level, Graduation Year, Effective Class Year** are removed from the present-columns set on non-Performance imports — even if you include those headers, the columns will be treated as absent for update purposes (preserved, not cleared).
* **SSO ID** and **Use MFA** are also context-merged before SSO/MFA validation runs — if only one of the two columns is present, the absent column's value is taken from the existing user record before validation, so you can update one without restating the other.

#### Education writes (Law School, Graduation Year) <a href="#education-writes-law-school-graduation-year" id="education-writes-law-school-graduation-year"></a>

Education-background writes on existing members are **create-only**: if the member already has any education record, additional ones are skipped. Use the dedicated profile-edit flows to modify or replace existing education records — the CSV will not overwrite them.

#### Creates <a href="#creates" id="creates"></a>

For new members, every required field on the row must pass validation. The default Role is `MEMBER` if Role is blank or absent. Welcome-email side effects fire per row.

### Updating a member after creation <a href="#updating-a-member-after-creation" id="updating-a-member-after-creation"></a>

There is no separate "update" endpoint for individual members on the REST API — updates are performed by submitting CSV rows whose **Email** matches an existing member. The same `POST /members/import-csv` endpoint handles both create and update; the [Update semantics](http://localhost:63342/markdownPreview/908541821/markdown-preview-index-26vd40t631gtlv10sm4r3ve57r.html#update-semantics) section above describes the underlying rules. This section shows how to apply them in practice.

#### The general recipe <a href="#the-general-recipe" id="the-general-recipe"></a>

1. Build a CSV containing **only the columns whose values you want to change**, plus the three always-required columns (`First Name`, `Last Name`, `Email`).
2. Include one row per member to update. Match by **Email** (case-insensitive).
3. `POST /members/import-csv?product={Product}` with the file.
4. On `200 OK`, `updatedCount` reflects how many existing members were touched. Any row whose email did not match an existing member is created instead and counts toward `createdCount`.

Remember: a column **header** present with a **blank** cell is interpreted as "clear this field," while an absent header is "preserve this field." This is the single most common source of accidental data loss — be deliberate about which columns you include in your update file.

#### Recipe 1: change one or two fields, leave the rest untouched <a href="#recipe-1-change-one-or-two-fields-leave-the-rest-untouched" id="recipe-1-change-one-or-two-fields-leave-the-rest-untouched"></a>

To update only the job title and work arrangement for an existing member, include exactly those columns alongside the required identifiers:

```csv
First Name,Last Name,Email,Job Title,Work Arrangement
Jane,Smith,jane.smith@firm.com,Senior Associate,HYBRID
```

Everything else — office location, department, practice area, law school, employee ID, levels, dates, role, SSO settings — is preserved because those columns are absent from the file.

#### Recipe 2: clear a field <a href="#recipe-2-clear-a-field" id="recipe-2-clear-a-field"></a>

Include the column header but leave the cell empty. To remove a member's bio link:

```csv
First Name,Last Name,Email,Bio Link
Jane,Smith,jane.smith@firm.com,
```

The bio link is set to empty on the existing record. All other fields are preserved.

#### Recipe 3: move a member to a new office <a href="#recipe-3-move-a-member-to-a-new-office" id="recipe-3-move-a-member-to-a-new-office"></a>

The office triad (`Office City`, `Office State (US Only)`, `Office Country (Non-US Only)`) is treated as a unit. Include all three columns to be explicit about the new location:

```csv
First Name,Last Name,Email,Office City,Office State (US Only),Office Country (Non-US Only)
Jane,Smith,jane.smith@firm.com,London,,United Kingdom
```

Omit all three columns to preserve the existing office.

#### Recipe 4: promote a member to admin (or demote) <a href="#recipe-4-promote-a-member-to-admin-or-demote" id="recipe-4-promote-a-member-to-admin-or-demote"></a>

Include the `Role` column. The role applies to the product the upload is targeted at (`?product=…`).

```csv
First Name,Last Name,Email,Role
Jane,Smith,jane.smith@firm.com,ADMIN
```

#### Recipe 5: grant an existing member access to an additional product <a href="#recipe-5-grant-an-existing-member-access-to-an-additional-product" id="recipe-5-grant-an-existing-member-access-to-an-additional-product"></a>

Submit the CSV against the **new** product. The import treats a row whose email matches an existing member but who doesn't yet have the target product role as a "grant product access" operation. A welcome email is sent for the newly-granted product.

```bash
curl -X POST 'https://<host>/api/v2/members/import-csv?product=PERFORM' \
  -b 'FLOAUTH=<your session token>' \
  -F 'file=@grant-perform.csv'
```

```csv
First Name,Last Name,Email
Jane,Smith,jane.smith@firm.com
```

#### Things to be careful about <a href="#things-to-be-careful-about" id="things-to-be-careful-about"></a>

* Match is by the Email column. If your records use a member's old email and the email on file has changed, you'll create a new member instead of updating the old one. To change a member's email, add the New Email column: on a row whose Email matches an existing member, New Email moves that member to the new address. This column only takes effect when the bulk email-update feature is enabled for your org — contact your Flo representative to have it enabled. The editEmployerMemberInfo GraphQL path described below is the per-member alternative.
* **Education records are create-only.** Setting `Law School` and `Graduation Year` on a re-upload will not overwrite existing education records. To change a member's law school after creation, contact Flo support.
* **Performance-only columns are silently ignored on non-Performance uploads.** `Level`, `Graduation Year`, and `Effective Class Year` are dropped at parse time when the request specifies a non-Performance product. To update those, target `product=PERFORM`.
* **Welcome emails fire when a member is granted a new product role.** If your re-upload is granting access to a previously-untouched product, expect notification side-effects (Recipe 5). If it's purely a field-update on a product the member already has, no email fires.
* **There is no separate dry-run mode.** Plan your column selection before submitting — the change is committed when the request succeeds. If your integration is risk-averse, send a single-row file first for a representative member, verify the result with a `memberById` query (GraphQL) or by visual inspection, then submit the rest in batch.

#### Alternative: GraphQL mutations for per-member updates <a href="#alternative-graphql-mutations-for-per-member-updates" id="alternative-graphql-mutations-for-per-member-updates"></a>

For integrations that need finer-grained control (single-member updates, partial updates without constructing a CSV, post-create email changes, deletions) the v2 GraphQL endpoint at `POST /graphql` exposes the following mutations:

| Mutation                           | What it does                                                                                                                                                                                      |
| ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `editEmployerMemberInfo`           | Update first/last name, employment (office, department, practice area, level, start date, effective class year), employee ID, bio link, law school, graduation year, work arrangement, job title. |
| `editEmployerMemberAuthInfo`       | Update product roles (ADMIN ↔ MEMBER), SSO/MFA settings.                                                                                                                                          |
| `editUniversityMemberInfo`         | University-side counterpart: update first/last name.                                                                                                                                              |
| `addEmployerMemberProductUserRole` | Grant an existing member access to an additional product (per-member equivalent of CSV Recipe 5).                                                                                                 |
| `revokeProductUserAccess`          | Remove a member's access to a product.                                                                                                                                                            |
| `deleteMember`                     | Delete a member entirely.                                                                                                                                                                         |

Each mutation takes a member `id` (the GraphQL `ID` returned at creation, or queryable via the `memberById` / `membersByProduct` queries). Authentication is the same as the CSV endpoint. Full request/response shapes are in the GraphQL schema at `member.schema.graphql`; the OpenAPI spec covers the REST surface area only. Reach out to your Flo contact if you need a GraphQL onboarding guide.

### Error response shape <a href="#error-response-shape" id="error-response-shape"></a>

`/members/import-csv` returns `400 Bad Request` with this JSON body on any validation failure:

```json
{
  "fileError": null,
  "rowColumnErrors": [
    {
      "rowNumber": 2,
      "columnName": "Email",
      "value": "jane@firm.com",
      "type": "DUPLICATE_VALUE",
      "message": "Duplicate value: jane@firm.com found in rows [2, 5]"
    }
  ]
}
```

Exactly one of `fileError` or `rowColumnErrors` is populated.

#### File-level errors (`fileError.type`) <a href="#file-level-errors-fileerrortype" id="file-level-errors-fileerrortype"></a>

| Type                       | Meaning                                                                       |
| -------------------------- | ----------------------------------------------------------------------------- |
| `EMPTY_FILE`               | The file is missing or empty.                                                 |
| `FILE_SIZE_EXCEEDED`       | The file is larger than 10 MB.                                                |
| `MISSING_REQUIRED_COLUMNS` | The header row is missing First Name, Last Name, or Email.                    |
| `UNEXPECTED_COLUMNS`       | The header row contains a column not in the recognized list.                  |
| `INVALID_FILE_FORMAT`      | Parse failure (malformed CSV, encoding issue, or an unexpected parser error). |

#### Row-level error types (`rowColumnErrors[].type`) <a href="#row-level-error-types-rowcolumnerrorstype" id="row-level-error-types-rowcolumnerrorstype"></a>

| Type                     | Triggers                                                                                                                  |
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------- |
| `EMPTY_REQUIRED_VALUE`   | A required field is missing, or an Office triad rule requires a value that wasn't provided.                               |
| `INVALID_EMAIL`          | Email is not a well-formed address.                                                                                       |
| `INVALID_FORMAT`         | First/Last Name contains a disallowed special character, **or** Use MFA isn't one of the accepted boolean strings.        |
| `INVALID_URL`            | Bio Link is not a syntactically valid URL.                                                                                |
| `INVALID_YEAR`           | Graduation Year / Effective Class Year is not 4 digits or is more than 200 years from the current year.                   |
| `INVALID_DATE_FORMAT`    | Start Date isn't in `yyyy-MM-dd`.                                                                                         |
| `DUPLICATE_VALUE`        | Email or Employee ID appears on two or more rows. The error message lists the colliding row numbers.                      |
| `INVALID_LIST_SELECTION` | A list field (Office Location, Practice Area, Law School, Department, or Level) doesn't match a known value for your org. |
| `INVALID_SSO_ID`         | SSO ID failed your org's SSO validation rules.                                                                            |
| `ROW_VALUE_CONFLICT`     | The Office triad on this row is internally inconsistent.                                                                  |
| `EXISTING_USER_CONFLICT` | A user conflict reported by the create path (e.g. an existing user in a different org).                                   |
| `UNKNOWN`                | An unmapped exception from the create/update path. Inspect the `message` field.                                           |

### Success response <a href="#success-response" id="success-response"></a>

`200 OK` with:

```json
{ "createdCount": 5, "updatedCount": 3, "absentMembers": [ { "email": "sam.lee@firm.com", "firstName": "Sam", "lastName": "Lee" } ] }
```

createdCount is the number of new members created. updatedCount is the number of existing members updated. absentMembers lists every non-admin member who already has access to this product but did not appear in the uploaded CSV — an import never removes anyone, so use this list to decide who to pass to DELETE /members/bulk-remove if you want the upload to be authoritative. It is an empty array when the CSV covered everyone.

### Sample CSV <a href="#sample-csv" id="sample-csv"></a>

A minimal valid file using only the required columns:

```csv
First Name,Last Name,Email
Jane,Smith,jane.smith@firm.com
John,Doe,john.doe@firm.com
```

A fuller file targeting `product=PERFORM`:

```csv
Employee ID,First Name,Last Name,Email,Job Title,Level,Office City,Office State (US Only),Office Country (Non-US Only),Department,Practice Area,Law School,Graduation Year,Effective Class Year,Start Date,Role,SSO ID,Use MFA,Bio Link,Work Arrangement
E-1042,Jane,Smith,jane.smith@firm.com,Senior Associate,Senior,New York,NY,,Litigation,Corporate,Columbia Law School,2018,2020,2024-06-03,MEMBER,jsmith,true,https://firm.com/bio/jsmith,HYBRID
E-1043,John,Doe,john.doe@firm.com,Partner,Partner,,,United Kingdom,Corporate,M&A,Yale Law School,2010,2012,2014-09-01,ADMIN,jdoe,true,,IN_PERSON
```

The second row demonstrates a non-US office (Office Country set, Office State empty) and an empty Bio Link.

### Example: curl <a href="#example-curl" id="example-curl"></a>

Download a 5-row template for the Recruit product:

```bash
curl -X GET 'https://<host>/api/v2/members/example-csv/RECRUIT/5' \
  -b 'FLOAUTH=<your session token>' \
  -o template.csv
```

Import:

```bash
curl -X POST 'https://<host>/api/v2/members/import-csv?product=PERFORM' \
  -b 'FLOAUTH=<your session token>' \
  -F 'file=@members.csv'
```

### Common pitfalls <a href="#common-pitfalls" id="common-pitfalls"></a>

| Symptom                                                                | Likely cause                                                                                                                                                                                                              |
| ---------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `UNEXPECTED_COLUMNS` rejection                                         | A header was spelled differently, has a typo, or includes a column from a different system. The match is case-insensitive but otherwise exact — `Hire Date` will be rejected because `Start Date` is the recognized name. |
| `MISSING_REQUIRED_COLUMNS` rejection                                   | One of First Name / Last Name / Email is not in the header row.                                                                                                                                                           |
| Existing member's field unexpectedly **cleared** to blank              | Your CSV included that column header with a blank cell. To preserve a field on an update, omit its column header from the file entirely.                                                                                  |
| Existing member's field unexpectedly **unchanged**                     | The reverse: you set a non-blank value but the column header was missing from your file.                                                                                                                                  |
| All Office fields flagged                                              | The Office triad rule was violated — see [Office location rules](http://localhost:63342/markdownPreview/908541821/markdown-preview-index-26vd40t631gtlv10sm4r3ve57r.html#office-location-rules).                          |
| Level / Graduation Year not persisted                                  | `product` was not `PERFORM`. These three fields are silently ignored on non-Performance imports.                                                                                                                          |
| Law School / Graduation Year not applied to an existing member         | Education writes are create-only — once a member has any education record, the CSV will not overwrite it.                                                                                                                 |
| `DUPLICATE_VALUE` on Employee ID across rows you thought were distinct | Comparison is case-insensitive — `E-1042` and `e-1042` collide.                                                                                                                                                           |
| `INVALID_LIST_SELECTION` on a value that exists in your UI             | The list match is case-insensitive but otherwise exact and trims surrounding whitespace. Embedded extra whitespace (`"New York"`) will not match `"New York"`.                                                            |
| `INVALID_FORMAT` on a Name field                                       | First or Last Name contained one of the disallowed characters: `\`, `<`, `>`, \`                                                                                                                                          |
| Members in the file aren't reflected as updates                        | Match is by Email (case-insensitive). If the CSV email differs from what's on file in any way other than case (extra dots, plus addressing, alias), the row becomes a create attempt, not an update.                      |

<br>
