Skip to main content

Contacts CSV import

Bulk-import contacts into a workspace from a CSV file. The import runs in the background: the file is uploaded to object storage, a worker parses and ingests it in chunks, and the UI polls progress in real time.

Overview

  1. From the Contacts page, click Import CSV.
  2. Pick one or more .csv files from disk (select multiple files to queue them sequentially).
  3. Each file is uploaded, queued for processing, and you’ll see Uploading → Queued → Processing → Complete ✓ with a per-file progress bar and running counters.
  4. Any failed rows are summarised with a View errors button — review them, fix in your spreadsheet, and re-upload the corrected file.

Required columns

Optional columns

Column headers are matched case-insensitively. Columns you don’t provide are ignored — extra columns are safely skipped.

Phone format

All phone numbers must normalise to E.164 — an international format that starts with +, the country code, and digits only (no spaces, dashes, or parentheses). Tip: if your spreadsheet strips the leading + or adds scientific notation, format the phone column as Text before exporting.

Example CSV

Download this example as a starting point and replace the rows with your own contacts.

Limits

  • File size: 10 MB per file.
  • Row count: recommended max 50,000 rows per file. Split larger lists across multiple files and upload them in one go — they’ll process sequentially.
  • Encoding: UTF-8. Export from Excel as “CSV UTF-8 (Comma delimited)” on macOS, or “CSV (Comma delimited) (*.csv)” with UTF-8 encoding on Windows.
  • Line endings: \n or \r\n both supported.
  • Quoting: standard CSV quoting — wrap a field in "…" to include commas or newlines, escape a quote inside as "".

Troubleshooting

“missing name” / “missing phone” The row is missing a required value. Make sure the header row uses name and phone (or one of the listed aliases) and every data row has both fields populated. “invalid phone format” The phone number could not be normalised to E.164. Add a country code with a leading + (e.g. +39…, +1…) or remove non-digit characters that break parsing. “duplicate phone number (contact already exists)” Another contact in the workspace already has this phone number. Imports don’t overwrite existing contacts — delete or update the existing record first if you need to replace it. Import stuck on “Queued” The background worker is temporarily behind on jobs. The UI will catch up automatically when the worker picks up the job — leave the page open, or check back later; the job state is persisted server-side. “upload failed: 413” Your file exceeds the 10 MB cap. Split it into smaller files and upload them together.

How it works

Under the hood:
  1. POST /api/contacts/import accepts multipart form data, validates the file, and uploads it to the contact-imports object-storage bucket at {workspaceId}/{importId}/{filename}.
  2. A row is inserted in public.contact_import_jobs with status pending, and a contacts.import.process outbox job is queued.
  3. The worker claims the job, downloads the file, parses rows, normalises phones, and inserts contacts in chunks of 100 — updating processed_rows, successful_rows, failed_rows, and error_report after each chunk.
  4. GET /api/contacts/import/:id returns the current state for the UI to poll.
Duplicate phone numbers are reported as failed rows (the existing contact is not modified) — this keeps re-uploads safe and idempotent.