Importing Members
Need to add many members at once? The CSV import lets you bulk-create or update members from a spreadsheet, including their contact info, labels, notification preferences, and form data.
CSV Import (UI)
- Go to Members
- Click the Import button
- Select your CSV file
- Choose a role for any newly created members
- Review the import preview
- Click Confirm to import
Import progress and history
After the import starts, you can close the dialog or browser tab. The import continues in the background. Open Members → Import/Export → Import history to see its status, totals, and per-row outcomes. Row numbers match your CSV, including the header row. Only one background Member import can run in a workspace at a time; wait for it to finish before starting another.
A lost progress connection does not mean the import failed. Check its history before retrying. Conflicting rows remain unchanged until you review and resolve them. Keep your original file if you need to revisit conflict details.
CSV Format
Your file must be a standard CSV with headers in the first row. At minimum, each row needs one of: email, phone, or external_id. CRM exports that use display names are accepted too: UserEmail, E-mail, and Email Address map to email; Telephone, Mobile, and MobilePhone map to phone; ExternalId maps to external_id. The export's own key columns are accepted as well: id, member_id, and memberid map to id; uuid, member_uuid, and memberuuid map to uuid (see The export id and uuid columns). Do not include both an alias and the canonical name (for example email and UserEmail) — the import rejects that as a duplicate column.
email,phone,name,date_of_birth,external_id,description,labels,notify_email,notify_sms,notify_voice
alice@example.com,+1-555-123-4567,Alice Johnson,1980-06-15,user_123,Support specialist,"vip,premium",1,1,0
bob@example.com,+1-555-123-4568,Bob Smith,1991-11-03,user_124,,standard,1,0,1
Column Reference
| Column | Required | Format |
|---|---|---|
email | One of email, phone, or external_id | Email address, stored lower-cased. The import does not check the format. |
phone | One of email, phone, or external_id | E.164 format (e.g. +15551234567) |
home_phone | No | An additional number, stored on the member labelled "home". Never used to match a member. Alias: Home Phone. |
work_phone | No | An additional number, stored on the member labelled "work". Never used to match a member. Alias: Work Phone. |
external_id | One of email, phone, or external_id | Your system's user ID |
id | No | This workspace's member id, as written by the export. Matched first, and honoured only when the row corroborates it with a matching email, phone, or external_id; an id that names no member of this workspace fails the row (see The export id and uuid columns). Never written back. Aliases: member_id, memberid. |
uuid | No | This workspace's member UUID, as written by the export. Same rules as id. Aliases: member_uuid, memberuuid. |
name | No | Free text |
date_of_birth | No | Date in YYYY-MM-DD format |
description | No | Free text |
labels | No | Comma-separated slugs; quote the cell if it contains commas (e.g. "vip,premium"). Missing labels are created during the import. |
notify_email | No | true/false or 1/0 |
notify_sms | No | true/false or 1/0 |
notify_voice | No | true/false or 1/0 |
form.{type}.{field} | No | Dot-notation for form data (see below) |
field.{namespace}.{key} | No | Dot-notation for member field data (see below) |
salesforce_id | No | Salesforce Contact/Lead record ID — links the member to that record (see below) |
Including Form Data
To populate form records during import, add columns using dot notation: form.{data_type_slug}.{field_slug}.
email,name,form.patient.first_name,form.patient.medical_id
alice@example.com,Alice Johnson,Alice,MED123456
bob@example.com,Bob Smith,Bob,MED123457
The data type and field slugs must match what's configured in your workspace under Knowledge → Forms.
Including Member Fields
To write to a member's key-value field store, add columns using field.{namespace}.{key} notation. The namespace groups related fields (e.g. voice, ehr, fhir) and the key identifies a specific field within that namespace.
email,name,field.voice.language,field.ehr.mrn
alice@example.com,Alice Johnson,es,MED123456
bob@example.com,Bob Smith,fr,MED123457
Member fields imported this way are user-managed (not tied to any integration). If a field with the same namespace and key already exists on a member, it is updated; otherwise a new field is created.
Linking to Salesforce records
If your workspace has a connected Salesforce integration, add a salesforce_id column to link each imported member directly to a Salesforce Contact or Lead. This is the same link the Sync → Reconciliation tab creates manually — once linked, features like Chat-Triggered Tasks can write conversation activity back to the right Salesforce record automatically, with no separate reconciliation step.
email,phone,name,salesforce_id
lead@example.com,+15551234567,Jordan Lee,00QQq00000kAybVMAS
Notes:
- The column is only accepted when an enabled Salesforce connection exists in the workspace; otherwise the import is rejected with a clear error.
- Values must be a valid 15- or 18-character Salesforce record ID. A malformed value fails validation up front rather than creating a link that never resolves.
- Re-importing the same
salesforce_idfor an already-linked member is a no-op. A row whosesalesforce_idconflicts with the member's existing link fails that row (the rest of the import continues) rather than silently re-pointing the member. - Linking works even while the connection is in a "needs re-authorization" state — the link is a local record and does not call Salesforce.
- Each link created by an import is recorded in the audit log, the same as links created from the Sync tab.
Limits
- Maximum 10,000 rows per import
- Maximum 10 MiB file size
Import from a Manager chat
If your patient or client list comes from another system, such as a Jane
patient export, you don't need to reformat it first. Drag the CSV or Excel
(.xlsx) file into a chat with the Manager and say "import this". The Manager:
- Reads the column headers and proposes how each one maps to a member field. You confirm or correct the mapping, and it is saved for next time.
- Uses the source system's patient ID as the external ID, so importing a newer export updates the same members instead of creating duplicates.
- Formats phone numbers and birth dates, and reuses your existing labels for tags and patient types.
- Shows a preview with the counts of new, updated, unchanged, and conflicting rows, and imports only after you say yes.
- Reports the result, listing any rows that failed or were skipped by row number.
When several rows share a phone number or email address, such as a family on one parent's number, only the first row keeps it. The later rows import without it, with that notification channel turned off, and the Manager tells you which rows these were. A later row left with no phone or email of its own can't be created, and the Manager reports it.
Only members with the members admin permission can import this way. Imports made from a chat don't appear in the Import/Export tab's history; the Manager's report in the chat is the record of what changed.
Bulk Import via API
For background imports that survive a disconnected client, send the same file
and query parameters to POST /api/v2/w/{workspace_uuid}/members/import/runs.
This endpoint streams Server-Sent Events with import_run, import_item, and
done message types. Run events carry status and cumulative counts; item events
carry a zero-based data-row index, status, reason code, and optional Member ID.
The first data row has index 0 (CSV row 2). These events contain no contact
values. A done event ends progress delivery; read the run's status to distinguish
completion, completion with errors, and failure.
Use GET /api/v2/w/{workspace_uuid}/members/import/runs for recent runs and
GET /api/v2/w/{workspace_uuid}/members/import/runs/{run_id}/items for row outcomes.
These endpoints require members:admin. The preview endpoint also accepts the
same resolutions form field as the import endpoint so corrected contact values
can be checked again before retrying.
The existing request-scoped NDJSON endpoint remains available for current API and SDK consumers. Its work is tied to the streaming request:
curl -X POST "https://api.gravityrail.com/api/v2/w/{workspace_uuid}/members/import?new_member_role_id=5" \
-H "Authorization: Bearer YOUR_API_KEY" \
-F "file=@members.csv"
The API streams progress as NDJSON so you can track large imports:
{"type":"progress","current":50,"total":200,"success":50,"failed":0,"conflicted":0,"created":30,"updated":20,"unchanged":0}
{"type":"conflict","row":61,"message":"identity_mismatch_on_phone_match","conflict":{"code":"identity_mismatch_on_phone_match","fields":["name","date_of_birth"],"match":{"match_type":"phone","member_id":123,"member_uuid":"..."}},"conflicted":1}
{"type":"complete","success":197,"failed":2,"conflicted":1,"total":200,"created":100,"updated":97,"unchanged":0,"errors":["Row 15: ValueError"],"failedRows":[15,89],"conflictRows":[61]}
By default the API is identity-safe: a phone-only match with different identity
fields is skipped and reported under conflicted (never silently
overwritten). To preview the classification without writing anything, call
POST /api/v2/w/{workspace_uuid}/members/import/preview?new_member_role_id=5
with the CSV as the file form field, which returns a JSON summary plus
per-row actions. To deliberately overwrite conflicting identities, add
identity_conflict_policy=overwrite_identity_fields as a query parameter
on the import call.
To find the new_member_role_id, list your roles at GET /api/v2/w/{workspace_uuid}/member-roles or check Members → Roles in the admin.
For creating individual members with associated data records in one call, see the Importing Members developer guide which covers the /create-signup endpoint.
Duplicates and Update-vs-Create
The import matches existing members by the export's id/uuid, then external ID, email, or phone number (in that priority order):
- New member — If no match is found, a new member is created with the role you selected. Imported members are people, so the Agent role is refused here: picking it fails every created row. Choose a human role or a custom role.
- Existing member — If a match is found, the member's fields are updated with the values from the CSV (blank cells are skipped, not cleared)
- Role preserved — Existing members keep their current role; the selected role only applies to new members
Shared phone numbers and emails
Families and care teams often share contact details — a parent's phone and email copied onto each child's record, one case manager's number on many clients. A repeated phone number or email never stops the import. What happens to the repeat depends on whether the two rows are the same person:
- The first holder keeps it as the primary contact. In the file, that is the
first row with the value. If a member already in the workspace has it as
their primary contact and the row has a different
external_id, that member keeps it. - If the rows are the same person — one person entered twice, which happens — the later row updates the member the first row created. It does not make a second member, and the number stays that member's primary.
- If they are different people, the later row is still imported: the value is
saved on their member as an additional contact, and is not used to match or
identify them. A row whose only phone and email are held by someone else
becomes a member who can be contacted through those details but cannot sign in
— so if the role you chose for new members requires signing in (for example
User or Manager), that row fails with
AccountlessMemberRoleErrorinstead of being created. - When it is not clear, the row comes back as a conflict
(
identity_review_in_file) rather than being guessed at. Nothing is written for it until someone answers — same person or a different person — and that answer currently comes through the API, in the row'sidentityfield alongside its optionalresolutionfield; answering a different person imports it as above. - Only the primary contact is used automatically — for texts, calls, emails and recognising who is calling or texting in. An additional contact is kept for your team's reference.
- The preview and results tell you which rows repeat a value (the phone or email field, never the value itself).
For CLI imports, put this JSON in the file passed to --resolutions-file.
API callers send the same JSON in the resolutions form field. This example
answers the repeated phone on CSV row 3 after you confirm the rows are the same person:
[{"row":3,"identity":{"phone":"same_person"}}]
Use different_person when they are different people, or the email key for
a repeated email. Put identity alongside the optional resolution field.
The import UI does not prompt for these identity answers yet.
When a member has no primary phone or email after their row is applied, a
number or email from the row that nobody else uses becomes their primary
contact automatically — the phone column is preferred over home_phone or
work_phone. API callers can turn this off with
auto_promote_unique_contacts=false.
Automatic identity decisions are saved with the preview and reused during import. Start within one hour; an expired plan requires another preview. Database matches are still checked when each row imports.
The export id and uuid columns
A member export leads with this workspace's member id, and you can add uuid
to it under Export. When a row carries one of those columns the import
matches on that key first, and — because Member.id is numbered per
workspace, so another workspace's export collides with yours — the key is only
honoured when the row also corroborates it: the row must carry at least one
of email, phone, or external_id, at least one of those must equal the
member's own value, and none of them may disagree with it.
That makes the export → edit → re-import round trip exact, and makes a key the row cannot corroborate loud instead of silent: such a row is refused — never matched by email/phone instead, and never created as a new member.
| What the row looks like | Result |
|---|---|
id/uuid names a member of this workspace, and a carried contact value matches that member | Updates that member |
id/uuid names no member of this workspace | Row fails (ExplicitMemberKeyNotFoundError) — no fallback, no new member |
id and uuid name two different members | Row fails (ExplicitMemberKeyConflictError) |
id/uuid is not a valid member id or UUID | Row fails (ExplicitMemberKeyInvalidError) |
the row carries no email, phone, or external_id at all | Row fails (ExplicitMemberKeyConflictError) — a bare key is not evidence |
the row's email, phone, or external_id disagrees with the member the key names | Row fails (ExplicitMemberKeyConflictError) |
Editing name, date_of_birth, or another non-identity field is fine, and you
can fill in a previously empty email, phone, or external_id provided
that value belongs to no other member — the row still has to carry one
identity value the keyed member already holds. Changing one of those values to
something the member does not already have fails the row; edit the member record
instead.
Member.id is numbered per workspace, so an export from one workspace does
not identify the same people in another — importing it there fails rather than
overwriting unrelated members. Always export from the workspace you intend to
update.
The whole file is rejected up front, before any row is written, when nothing in it identifies your members. There are two cases, and the message tells you which:
- No
id/uuidvalue names a member of this workspace — the column is not ours. That is what you will see when the CSV is a CRM extract that happens to carry its ownIDcolumn: rename or remove that column (or use a different header) to import the file by email/phone instead. - The values do name members, but no row's
email/phone/external_idagrees with the member its key names — the file is an export of this workspace, but the contact cells no longer match. Correct the disagreeing cells, or clear the key cell on a row to import that row by contact instead. Removing theidcolumn here would send those rows to email/phone matching, where they match nobody and create duplicates.
One thing anchors the whole file on its own: a row whose uuid names a member of
this workspace. A UUID cannot collide across workspaces the way Member.id can,
so uuid needs no corroboration. Such a file keeps per-row handling, and any row
that still cannot corroborate its key fails individually.
Email matching is case-insensitive
CSV email matching ignores case, so Alice@Example.com updates the member
stored as alice@example.com instead of creating a duplicate. If two members
hold addresses that differ only by case, the row fails
(AmbiguousMemberEmailError) rather than arbitrarily picking one — re-import
the export with its id column to say which member you mean.
Within those rules you can safely re-import the same file after making edits: rows the import can identify unambiguously are updated, and rows with no key and no match create new members.
Identity-conflict protection (preview first)
A phone number can be shared (households, caregivers, a shared office line), so a match by phone alone is treated as weak evidence. When the Preview step runs, any row that matches an existing member only by phone but carries a different name or date of birth is flagged as an identity conflict rather than silently overwriting that person's details. (When both the row and that member have an external ID and the two differ, the row is a different person: it is imported as its own member with the number saved as an additional contact — see Shared phone numbers and emails.)
For each conflict you'll see the existing member's identity next to the incoming row, and you choose how to proceed:
- Skip conflicts (default) — safe rows import normally; conflicting rows are left untouched and listed so you can fix them
- Overwrite identities — a clearly labelled, destructive action that replaces the existing identity details for every conflicting row (use only when you're sure the rows refer to the same people). Each overwrite is recorded in the audit log.
Matches by external ID or email are treated as strong identity matches and are updated as usual — the phone-only conflict guard does not apply to them.
Operator and protected members
Workspace Manager / Platform Support members (and any role with
workspace admin permission), plus members marked import-protected, are
never treated as an import match. A row whose email or phone collides with one
of those members is reported as conflicted with reason operator_role or
import_protected and nothing is written to that member — including via
the signup / EventRule upsert APIs. Patient (User / External) rows continue to
match and update as before. Admins can flag a non-operator member as
import-protected from the member record (isImportProtected).
Using External IDs
If your members originate from another system (CRM, EHR, etc.), populate the external_id column with your system's user ID. This makes it easy to:
- Look up members by your system's ID via the API
- Keep members in sync across repeated imports
- Track which members correspond to records in your other systems
Validation Errors and Troubleshooting
Common Errors
| Error | Cause | Fix |
|---|---|---|
| "Invalid phone format" | Phone not in E.164 format | Use full international format: +15551234567 |
| "No identifier provided" | Row has no email, phone, or external_id | Add at least one identifier column |
| "Unknown form field" | form.x.y references a non-existent data type or field | Check your data type and field slugs in Knowledge → Forms |
| "Invalid boolean value" / "Invalid enum value. Expected one of: …" | A form.* cell does not match the field's type or its allowed options | Correct the cell to a value the field accepts. The row is counted failed and neither the member nor its form record is written for it, so fix the row and re-import. |
ExplicitMemberKeyNotFoundError | The row's id/uuid names no member of this workspace — usually an export taken from a different workspace | Export from the workspace you are importing into, or correct/clear the id/uuid cell. The row is not matched by email/phone and no member is created. |
ExplicitMemberKeyConflictError | The row's id and uuid name different members; or the row carries no email/phone/external_id to corroborate the key; or one it carries disagrees with the member the key names | Re-export the row, or use the id/uuid the target workspace's export wrote. A bare key is refused on purpose — see The export id and uuid columns |
ExplicitMemberKeyInvalidError | The id/uuid cell is not a positive integer / 36-character UUID | Clear the cell, or use the value the export wrote |
identity_review_in_file | Two rows carry the same phone or email, and it was not clear whether they are the same person. Nothing is written for the row | Answer it through the API, in that row's identity field alongside its optional resolution — same person (the row updates the member the earlier row made) or a different person (it is imported with the number as an additional contact). The import UI does not prompt for this yet |
AccountlessMemberRoleError | Every phone and email on the row belongs to another member, so the member is created without a sign-in, but the role you chose for new members (for example User or Manager) needs one | Pick a role for imported members that does not require signing in, or give the row its own email or phone |
AmbiguousMemberEmailError | Two members in this workspace hold addresses that differ only by case, so the row's email is not unique | Re-import the export including its id column so the row says which member it means |
| "Column 'id': no value in this file names a Member in this workspace" | Nothing in the CSV's id/uuid column names a member of this workspace — usually a file that is not a Gravity Rail export | Rename or remove that column if the file is not a Gravity Rail export, or export the members from the workspace you intend to update |
| "Column 'id': the members these keys name do not match the email, phone, or external ID on their rows" | The keys do name members of this workspace, but the rows' contact cells no longer agree with them | Correct the disagreeing cells so each row corroborates its key, or clear the key cell to import that row by contact instead — removing the column would create duplicates |
Tips
- Check your encoding — Save CSV files as UTF-8 to avoid garbled characters
- Trim whitespace — Leading/trailing spaces in emails or phone numbers cause validation failures
- Preview first — The import UI shows a preview before committing; review it for unexpected data in the wrong columns
- Export then edit — If updating existing members, export your current member list first (
GET /api/v2/w/{workspace_uuid}/members/export), edit the CSV, then re-import
Best Practices
- Start small — Test with 5-10 rows before importing thousands
- Use external IDs — They make re-imports and API lookups much easier
- Use ISO dates —
date_of_birthmust beYYYY-MM-DD - Normalize phone numbers — Always use E.164 format (
+followed by country code and number, no spaces or dashes) - Use labels directly — The import creates labels that do not yet exist in the workspace
- Set up forms first — If including form data columns, make sure the data types and fields exist in your workspace
- Keep a backup — Save your original CSV before editing, in case you need to start over
For developers
- Importing Members guide — bulk-load Members programmatically via CSV or API
- Members API — create and update Members one at a time