CSV import
Two entities can be imported from a CSV: leads and contacts. They do not share an implementation and they do not behave the same way, so they are documented separately below.
Before anything else, the two constraints that catch people out:
- Neither importer is reachable from the web app or the phone. Both are built and tested on the backend, and no screen in either client calls them. Importing today means calling the API yourself. The Import button on the leads list is not wired up.
- Files are capped at 5 MB, measured against the bytes actually read rather than the
Content-Lengththe client claims. There is no chunking, no background queue you can poll, and no larger-file path. Split a bigger export before uploading.
Both endpoints require an admin, or a profile with explicit sales access. An ordinary member gets a 403.
Contacts: preview, then commit
The better of the two, because it lets you see what a file will do before it does it.
POST /api/contacts/import/preview/
POST /api/contacts/import/commit/
Content-Type: multipart/form-data
file=<contacts.csv>
The form field is file. preview parses and validates every row and writes nothing; commit
re-runs the same validation and writes inside a single transaction.
Headers
| Headers | |
|---|---|
| Required | first_name, last_name |
| Optional | email, phone, organization, title, department, do_not_call, linkedin_url, address_line, city, state, postcode, country, description, account_name, assigned_emails, team_names, tags |
A missing required header fails the file as a whole with a header_error rather than failing row by
row. An unrecognised header is reported too, so a typo does not silently drop a column.
Errors are per row
{ "row": 7, "field": "email", "message": "Enter a valid email address." }
row is 1-based and matches the CSV line number, with the header counted as row 0, so you can open
the file and go straight to it. Every reference and duplicate lookup is scoped to your organisation,
so a crafted CSV cannot attach a contact to another tenant's account.
The file must be UTF-8. If it cannot be decoded you get one clear header_error saying so rather
than a wall of per-row noise.
Leads: one-shot upload
Older and blunter than the contacts path.
POST /api/leads/upload/
Content-Type: multipart/form-data
leads_file=<leads.csv>
Note the field name is leads_file, not file.
There is no preview step. The endpoint validates, splits the rows into valid and invalid, hands both to a background job, and returns immediately:
{ "error": false, "message": "Leads created Successfully" }
That response is an acknowledgement, not a result. It does not tell you how many rows were created, which ones failed, or why, and there is no task id and nothing to poll. If you need to know what happened, list the leads afterwards and compare.
Accounts
There is no account import. No endpoint, no client path, nothing.
What this page used to say
It documented POST /api/contacts/upload/ and POST /api/accounts/upload/, neither of which
exists, fuzzy account matching and a review queue, cf_-prefixed custom-field columns, a Celery
queue for files over 5 MB returning a task_id to poll at GET /api/imports/<task_id>/, and advice
about splitting files over 100 MB when the hard cap is 5 MB. None of that is real. It is recorded
here rather than quietly deleted, because anyone who built against that page deserves to know why
their integration never worked.
See also
- Custom fields: how org-defined fields work on the API.
- Errors: the standard error response shape.