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

# Batch Patient Import

> Create or update patients from a CSV file with typed external IDs and row-level results

# Batch Patient Import

Create or update as many as 500 patients from one CSV file on the [import page](https://app.getsolum.com/patients/batch). Each data row is processed independently, so one failed row does not undo successful rows.

## Quick start

1. Download the current CSV template from the import page.
2. Add one patient per row. Keep the camelCase column names exactly as shown.
3. Upload the file and correct any client-side validation errors.
4. Submit the import.
5. Review the separate patient and insurance totals. Every CSV row remains available in the result table, including rows with no insurance information.
6. Download the insurance-results CSV if you need to reconcile unresolved, incomplete, or failed coverages outside Solum.

## Identifying a patient

Each row must contain either:

* `phoneNumber`; or
* all three of `firstName`, `lastName`, and `dateOfBirth`.

You may also provide a typed external ID using the optional pair `externalIdType` and `externalId`.

```csv theme={null}
firstName,lastName,dateOfBirth,externalIdType,externalId
Jamie,Rivera,1990-01-15,EHR Patient ID,EXT001
```

Both external-ID cells may be blank. If either is present, both are required.

* `externalIdType` accepts the UUID or case-insensitive name of an external-ID type configured for your company.
* `externalId` is trimmed and must contain 1–500 characters.
* A row supports one typed external ID.
* If the same resolved type and value appears more than once in the file, every duplicate row fails. Other rows continue.

## How existing patients are matched

Batch import uses the same matching rules as the patient upsert API, in this order:

1. Exact typed external ID
2. Exact first name, last name, and date of birth
3. Normalized phone number plus fuzzy name
4. Normalized email plus fuzzy name

When a patient matches, supplied non-null fields update that patient using the same normalization, phone-lock, payor, audit-history, webhook, and side-effect rules as `POST /v1/patients/upsert`. If no patient matches, a new patient is created.

## CSV columns

Column order does not matter. Unknown columns are ignored.

### Patient fields

| Column                  | Format                                                         | Example                |
| ----------------------- | -------------------------------------------------------------- | ---------------------- |
| `firstName`             | Text                                                           | `Jamie`                |
| `lastName`              | Text                                                           | `Rivera`               |
| `middleName`            | Text                                                           | `Alex`                 |
| `phoneNumber`           | 10 digits, 11 digits beginning with 1, or common US formatting | `(555) 123-4567`       |
| `additionalPhoneNumber` | Phone number                                                   | `5559876543`           |
| `dateOfBirth`           | `YYYY-MM-DD`, `YYYYMMDD`, or `MM/DD/YYYY`                      | `1990-01-15`           |
| `email`                 | Email address                                                  | `jamie@example.com`    |
| `gender`                | `male`, `female`, or `other`                                   | `female`               |
| `address`               | Street address                                                 | `123 Main St`          |
| `address2`              | Address line 2                                                 | `Apt 4B`               |
| `city`                  | City                                                           | `New York`             |
| `state`                 | Two-letter US code or full state name                          | `NY`                   |
| `zip`                   | ZIP code                                                       | `10001`                |
| `externalIdType`        | Company external-ID type UUID or name                          | `EHR Patient ID`       |
| `externalId`            | Identifier paired with `externalIdType`                        | `EXT001`               |
| `comments`              | Notes                                                          | `New patient referral` |

### Workflow and tags

| Column          | Format                                      | Example            |
| --------------- | ------------------------------------------- | ------------------ |
| `workflowStage` | Existing stage UUID or title                | `Intake`           |
| `assignedUser`  | Existing user ID or email                   | `user@example.com` |
| `tags`          | Comma-separated existing tag UUIDs or names | `New Patient, VIP` |

Values that cannot be resolved are reported in `dropped_fields` for that row.

### Insurance

| Column               | Description                      | Example                  |
| -------------------- | -------------------------------- | ------------------------ |
| `primaryInsurance`   | Existing insurance name or alias | `Aetna`                  |
| `primaryMemberId`    | Primary member ID                | `MEM123456`              |
| `secondaryInsurance` | Existing insurance name or alias | `Blue Cross Blue Shield` |
| `secondaryMemberId`  | Secondary member ID              | `MEM789012`              |

Insurance names are lookup-only and flow through the same resolver used by
`POST /v1/patients/upsert`. Solum can select only an active insurance belonging
to your company with a nonblank trading-partner service ID. It tries canonical
names, configured aliases, state-aware variants, and then a bounded
AI-assisted match.

An unresolved insurance does **not** fail the patient row. The patient and any
other valid coverage can still succeed, and the unresolved coverage is
reported with a stable reason. A member ID with a blank insurance-name cell is
reported as `incomplete`; a row where all four insurance cells are blank is
`not_provided`.

### Referral

| Column           | Format                          | Example                          |
| ---------------- | ------------------------------- | -------------------------------- |
| `physicianName`  | Text                            | `Dr. Jane Smith`                 |
| `physicianNpi`   | 10 digits                       | `1234567890`                     |
| `physicianPhone` | Phone number                    | `5551112222`                     |
| `physicianFax`   | Fax number                      | `5553334444`                     |
| `physicianGroup` | Practice or group               | `Smith Medical Group`            |
| `physicianEmail` | Email address                   | `drsmith@example.com`            |
| `icdCodes`       | Comma-separated diagnosis codes | `M54.5, M79.3`                   |
| `serviceTypes`   | Comma-separated service types   | `physical_therapy, consultation` |

## Partial-success results

A valid top-level request returns HTTP `200` even if individual rows fail. Authentication errors, malformed JSON, an empty item list, and more than 500 items remain request-level errors.

The response keeps the original patient aggregates and adds separate insurance
aggregates plus one ordered result for every patient row. Each row contains one
ordered `payor_results` item per supplied primary or secondary coverage.

```json theme={null}
{
  "created_count": 1,
  "updated_count": 1,
  "failed_count": 1,
  "insurance_created_count": 1,
  "insurance_updated_count": 0,
  "insurance_unchanged_count": 0,
  "insurance_unresolved_count": 1,
  "insurance_incomplete_count": 0,
  "insurance_failed_count": 0,
  "insurance_not_processed_count": 1,
  "insurance_not_provided_count": 0,
  "patient_ids": ["new-patient-uuid"],
  "updated_patient_ids": ["existing-patient-uuid"],
  "results": [
    {
      "row": 1,
      "patient_id": "new-patient-uuid",
      "created": true,
      "matched": false,
      "match_reason": null,
      "dropped_fields": [],
      "insurance_status": "complete",
      "payor_results": [
        {
          "payor_responsibility": "primary",
          "submitted_insurance_name": "Humana Gold Plus",
          "resolved_insurance_id": "configured-insurance-uuid",
          "resolved_insurance_name": "Humana",
          "trading_partner_service_id": "61101",
          "payor_id": "payor-uuid",
          "coverage_chain_id": "coverage-chain-uuid",
          "outcome": "created",
          "resolution_method": "alias",
          "reason_code": null,
          "message": null
        }
      ],
      "error": null
    },
    {
      "row": 2,
      "patient_id": "existing-patient-uuid",
      "created": false,
      "matched": true,
      "match_reason": "external_id",
      "dropped_fields": ["phone_number", "payors"],
      "insurance_status": "needs_review",
      "payor_results": [
        {
          "payor_responsibility": "secondary",
          "submitted_insurance_name": "Unknown Regional Plan",
          "resolved_insurance_id": null,
          "resolved_insurance_name": null,
          "trading_partner_service_id": null,
          "payor_id": null,
          "coverage_chain_id": null,
          "outcome": "unresolved",
          "resolution_method": null,
          "reason_code": "no_configured_match",
          "message": "No configured insurance matched this name."
        }
      ],
      "error": null
    },
    {
      "row": 3,
      "patient_id": null,
      "created": false,
      "matched": false,
      "match_reason": null,
      "dropped_fields": [],
      "insurance_status": "not_processed",
      "payor_results": [
        {
          "payor_responsibility": "primary",
          "submitted_insurance_name": "Cigna",
          "resolved_insurance_id": null,
          "resolved_insurance_name": null,
          "trading_partner_service_id": null,
          "payor_id": null,
          "coverage_chain_id": null,
          "outcome": "not_processed",
          "resolution_method": null,
          "reason_code": "patient_row_failed",
          "message": "Insurance was not processed because the patient row failed."
        }
      ],
      "error": {
        "type": "validation_error",
        "code": "invalid_parameter",
        "message": "external_id_type was not found for this company",
        "param": "external_id"
      }
    }
  ]
}
```

`row` is the one-based data-row number and does not count the header. Patient
outcomes are represented by `error`, `created`, and `matched`; insurance
outcomes come from `insurance_status` and `payor_results`.

### Insurance statuses and outcomes

Row-level `insurance_status` values are:

| Status          | Meaning                                                                   |
| --------------- | ------------------------------------------------------------------------- |
| `complete`      | Every supplied coverage was created, updated, or unchanged.               |
| `partial`       | At least one coverage succeeded and at least one needs review.            |
| `needs_review`  | Every supplied coverage is unresolved, incomplete, or could not be saved. |
| `not_processed` | Insurance input was present, but the patient row failed first.            |
| `not_provided`  | All primary and secondary insurance-name and member-ID cells were blank.  |

Each `payor_results[].outcome` is one of `created`, `updated`, `unchanged`,
`unresolved`, `incomplete`, `failed`, or `not_processed`. Expected resolution
abstentions use these stable `reason_code` values:

* `insurance_name_missing`
* `no_configured_match`
* `configured_insurance_inactive`
* `insurance_not_vob_ready`
* `ambiguous_match`
* `match_confidence_too_low`
* `resolution_unavailable`
* `persistence_failed`
* `patient_row_failed`

The completion page shows all rows, supports All, Needs Review, Created,
Updated, Unchanged, and No Insurance filters, and paginates after filtering.
Needs Review does not include informational `not_provided` rows. The downloadable
insurance report contains patient and configured-coverage identifiers and safe
outcome details, but never patient names or insurance member IDs.

## Troubleshooting

| Problem                                     | Fix                                                                                                                                  |
| ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| Missing phone and demographics              | Add `phoneNumber`, or add `firstName`, `lastName`, and `dateOfBirth`.                                                                |
| External-ID cells must be provided together | Fill both `externalIdType` and `externalId`, or leave both blank.                                                                    |
| External-ID type is unavailable             | Use a type UUID or name configured under your company's external-ID settings.                                                        |
| Duplicate typed external ID                 | Remove or change every repeated `(externalIdType, externalId)` pair.                                                                 |
| Invalid date                                | Use `YYYY-MM-DD`, `YYYYMMDD`, or `MM/DD/YYYY`.                                                                                       |
| Invalid NPI                                 | Use exactly 10 digits.                                                                                                               |
| A field appears in `dropped_fields`         | The row succeeded, but that field could not be normalized, resolved, or safely updated. Correct it and update the patient if needed. |
| Insurance is `incomplete`                   | Add the missing insurance name for the corresponding member ID.                                                                      |
| Insurance is `unresolved`                   | Review `reason_code`, then correct the name or update the company's insurance configuration.                                         |
| Insurance `failed` to save                  | The patient and other successful coverages remain saved. Retry that coverage after reviewing the result.                             |
| A row failed after other rows succeeded     | Fix and re-import only the failed row; successful earlier rows are not rolled back.                                                  |
