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

# Contacts CSV import

> Bulk-import contacts from a CSV file — format, limits, and troubleshooting.

# 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

| Column  | Required | Notes                                                          |
| ------- | -------- | -------------------------------------------------------------- |
| `name`  | Yes      | Display name. Aliases: `display_name`, `full_name`.            |
| `phone` | Yes      | Phone number. Aliases: `phone_e164`, `phone_number`, `mobile`. |

## Optional columns

| Column  | Notes                                                        |
| ------- | ------------------------------------------------------------ |
| `tags`  | Semicolon-separated list, e.g. `vip;newsletter;2026-winter`. |
| `email` | Contact email (stored on the contact record).                |

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).

| Raw input           | Normalised                        |
| ------------------- | --------------------------------- |
| `+393289214993`     | `+393289214993`                   |
| `+1 (555) 123-4567` | `+15551234567`                    |
| `393289214993`      | `+393289214993`                   |
| `555-123-4567`      | ❌ missing country code — rejected |

Tip: if your spreadsheet strips the leading `+` or adds scientific notation,
format the phone column as **Text** before exporting.

## Example CSV

```csv theme={null}
name,phone,tags,email
Alice Rossi,+393289214993,vip;newsletter,alice@example.com
Bob Meyer,+4915112345678,partners,bob@example.com
Carla Díaz,+34911234567,vip,
Derek Okafor,+2348011234567,,
```

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.
