CSV contact import
Bulk import contacts from a CSV file. Best way to onboard a customer's existing list or turn a trade show export into CRM contacts.
Now shipping
The self-serve CSV import is live. From the Contacts page click Import CSV (top bar or empty state). A 3-step wizard walks you through upload, column mapping, and a preview so you can see exactly what will be created, updated or skipped before anything is written.
The wizard, step by step
- Upload. Drop the file in or click the drop zone. CSV up to 20 MB. Also on this screen you set the source label, optionally pick tags to attach to every imported contact, and choose your dedup behaviour (skip duplicates, or update existing rows when we match).
- Map columns. We guess a mapping from your headers to the CRM fields. Everything is editable. A live preview shows the first ten rows with a green (create), amber (update), grey (skip) or red (error) badge each, plus running totals for the whole file. Change a mapping and the counts refresh.
- Import. A progress bar tracks rows processed. On completion you get counts of created, updated, skipped and errored rows, an "Undo import" toast active for 30 seconds, and a downloadable errors CSV if any rows failed.
How dedup works
For each row we look for an existing contact in your company matched by lowercased email OR last 9 digits of the phone number. That is the same rule receptionist auto-creation and the WooCommerce sync use, so imports do not fight with your other data sources.
- If Skip duplicates is on (default), matches are counted as skipped and left untouched.
- If Update existing is on, matched rows have name, phone, email, city, address, website and niche overwritten from the CSV. Notes are appended, not replaced.
CSV format
Column headers we recognise
Case-insensitive, punctuation and spaces are ignored, so Email, e-mail and email address all map to email.
email - primary dedupe key.
contact_name OR (first_name + last_name) - first + last are automatically combined.
phone - E.164 preferred (+441234567890). UK-style local numbers like 07700 900123 get normalised.
business_name - company name.
city, postcode, country, address.
tags - comma-separated list. Missing tags are created automatically.
notes - free text. Also appended with an import audit line.
source - overrides the default source label per row.
Example
email,first_name,last_name,phone,business_name,city,tags,source
alex@acme.com,Alex,Smith,+441234567890,Acme Ltd,Bath,"warm lead,trade show",trade show 2026
jane@example.co.uk,Jane,Doe,+447700900456,,Bristol,"cold outreach",cold list Q3
Rules that avoid rework
- UTF-8 encoding. Excel adds a BOM which the importer strips, but "CSV UTF-8" from the save dialog is cleanest.
- Fully blank rows are ignored, so trailing empty rows are fine.
- Escape commas inside fields with double quotes:
"warm lead,trade show".
- Empty cells are fine. Do not use the string NULL or N/A, leave the cell empty.
- 50,000 rows max per file. Split larger lists.
- Before you upload, open the CSV in a text editor and eyeball the top 5 rows. If phone numbers look like
4.47E+11, re-export with the phone column formatted as text.
What happens after import
- Every contact touched by the job (created or updated) gets a marker tag named
csv_import_<job_id> so you can filter the results.
- New contacts get their
source column set to whatever source label you chose on step 1 (default csv_import).
- A note is appended to each new contact: Imported via CSV on YYYY-MM-DD from filename.csv.
- The full job (counts, errors file, options used) is recorded in
contact_import_jobs for audit.
Undo
Right after an import completes a toast in the bottom right offers "Undo" for 30 seconds. Undo archives the newly created contacts (soft delete: they get status = 'archived'), it does not roll back updates to previously existing contacts. If you close the tab, the API still accepts undo for 30 minutes via the same job id.
Alternatives if CSV is not right for you
- WooCommerce sync: if your list is already in WooCommerce, use that. You get order history back-fill you cannot get from a plain CSV.
- Manual add: for a handful of contacts, the "+ Add contact" button on the Contacts page is faster than preparing a CSV.
Common problems
- Every row imports as a new contact even though I have duplicates
- You did not include an
email or phone column. Dedup needs at least one of the two.
- Phone numbers look like scientific notation ("4.47E+11")
- Excel converted them. Re-export with the phone column formatted as text, or prepend an apostrophe (
'+44...) in the source before export.
- Tags are not created
- You used semicolons instead of commas as separator. Either is accepted, but be consistent within a file.
- Import completes but I cannot see the contacts
- You imported into the wrong company scope. Confirm the header of the Contacts page shows the right company name and use the company switcher top right if not.
- Big file, wizard says "starting" for ages
- Files over 500 rows run in a deferred pass triggered by the first status poll. Progress ticks once processing begins. If it stalls, refresh and the poll picks the job back up.
What this unlocks
Onboarding a new customer's existing list takes minutes not days. Trade show lead-scan exports become CRM contacts by end of day. Old customer lists from previous CRMs can be migrated in bulk with dedupe against your live WooCommerce buyers. And you never need to write a bespoke import script for a one-off list again.