Import and reconcile contacts
This guide walks the bulk contact-import lifecycle over the API: prepare a CSV, dry-run it with the preview endpoint, run it as a background job with a merge strategy, poll for progress, download the rows that were skipped, and cancel or roll back a job that went wrong. The same flow backs the dashboard’s Audience → Imports page — the API and the UI drive the same jobs. For request and response schemas, see the contacts API reference. This page covers the workflow; custom fields covers defining the fields a CSV can map into, and opt-out lists covers how consent is enforced after the import lands — linked rather than repeated below.1. When to import — and when not to
Reach for a bulk import when you are moving a finite roster into Orbit:- Platform migrations — the contact export from your old provider.
- CRM exports — a HubSpot or Salesforce list pulled to CSV.
- Seed lists — a one-off event sign-up sheet or a purchased list you have consent for (see step 7).
2. Prepare the CSV
Every row needs at least one address: aphone or an email. Rows with neither are skipped during the import. Beyond that, map columns to contact field keys:
Keep the file to ≤32 columns and ≤2KB per cell when you want the original row echoed back in the skipped-rows CSV — oversized rows import fine but their raw form is truncated in the report. Two sizes of import exist:
- Up to 10,000 rows — the synchronous
POST /api/v1/contacts/bulkendpoint answers in one request. - Up to 1,000,000 rows — the asynchronous
POST /api/v1/contacts/importsjob this guide covers.
3. Preview before committing
Always dry-run first.POST /api/v1/contacts/imports/preview accepts the same row shape as the real import (up to 10,000 rows for the preview) and writes nothing:
validation— how many rows carry a phone, how many carry an email, how many are invalid, and a sample of the specific errors.duplicates.existing_db_match_count— how many rows match a contact already in Orbit (by phone or email). These are exactly the rows your merge strategy (step 4) will act on.duplicates.in_csv_dup_count— duplicate addresses within the file itself, withdup_match_fieldnaming the field that collided and the first row it was seen at. Deduplicate locally before you import; the job handles them, but a clean file is cheaper to reason about.
rows_invalid or in_csv_dup_count is high, fix the export at the source and re-preview. Running the preview on a 2,000-row sample of a million-row file is a fair proxy — header mistakes and formatting drift show up in the first page.
4. Run the async import
Post the rows mapped to field keys, with a merge strategy and the original file name for auditing:
The endpoint returns
202 with the job_id — the job now runs in the background. A 503 here means the import queue is unavailable; fall back to the synchronous POST /api/v1/contacts/bulk for files under 10,000 rows, or retry later.
Only contacts the job created are ever counted as the job’s output — merge rows update existing records and are never owned by the job, which matters when you roll back (step 6).
5. Poll for progress and collect skipped rows
PollGET /api/v1/contacts/imports/{id} until status leaves pending/processing:
pending → processing → one of completed, failed, or cancelled. Poll every few seconds; there is no push channel for these jobs.
When the job finishes with failed_rows > 0 — or a synchronous bulk import reports skips — download the skipped-rows CSV, keyed by the job id. For async jobs:
row_index column, a human-readable reason column, and one column for every header the original upload contained:
6. Cancel and roll back
Two different controls — cancel stops a job that is still running; rollback deletes what a finished job created. Cancel stamps a request the worker checks between batches, so in-flight work stops early and the job flips tocancelled:
409 means the job already completed, failed, or was cancelled — or the id is wrong.
Rollback hard-deletes every contact the job created, cascading through list memberships, segments, scores, engagement profiles, and consent records in one transaction:
rollback_until timestamp says exactly until when). The window exists so an old import whose contacts have accumulated downstream data cannot be silently orphaned. A 409 means the import was already rolled back or the window expired. Rollback only removes what the job created; rows a merge import updated keep their pre-existing records.
The combination for a botched import is cancel → let it settle → rollback.
7. Verify the audience and pair it with consent
After a successful import:- Spot-check in the dashboard under Audience → Imports — pick the job, preview a handful of the imported contacts, and confirm field mapping landed where you expected (custom fields especially).
- Segment, don’t re-import. If the import needs slicing — “everyone from the Q1 file with
plan_tier = enterprise” — build it as a segment over the imported contacts. - Importing an address is not consent. Suppression and opt-out handling is enforced at send time, not at import time, so a row landing in Orbit does not mean it is reachable. Capture consent with a public consent form or import it into an opt-out list before the first campaign — see opt-out lists. US SMS sends additionally gate on carrier registration (10DLC).
8. Troubleshooting
failed_rowsclimbs during the job. Wait for completion, download the skipped-rows CSV, fix the flagged rows, and re-import them as a new job. Thereasoncolumn names the exact cause per row.- Preview shows
existing_db_match_counthigher than expected. The file overlaps contacts already in Orbit; decide the merge strategy deliberately —skipkeeps the existing record,mergeoverwrites its fields from the CSV. - Duplicates inside the file.
in_csv_dup_countcatches repeated phones or emails within the upload; deduplicate at the source. - Rollback returns 409. The 24-hour
rollback_untilwindow expired or the job was already rolled back. From here the contacts are ordinary data — delete them withPOST /api/v1/contacts/bulk-deleteinstead. - Cancel returns 409 but contacts kept arriving. The job had already finished between your last poll and the cancel call; check
statusand use rollback within the window. - Imported contacts don’t receive the campaign. Consent is gated at send time: check the suppression cohort in the campaign dry-run response and the opt-out lists the audience funnels through. An imported address without consent record stays unreachable for marketing sends.
See also
- Send a campaign end-to-end — audiences, dry-run, launch, and measurement after the import lands
- Segments — slice imported contacts without re-importing
- Custom fields — define the typed fields a CSV maps into
- Opt-out lists — consent and suppression enforcement at send time
- Collect consent with a public form — gather opt-in before the first send
- Contacts API reference — full request/response schemas for the import endpoints