Import from CSV
Upload a CSV and map columns onto contacts or companies.
The CSV importer maps uploaded columns onto contacts or companies. It needs no import secret, unlike the hosted sources, so it works on a fresh self-host.
Upload the file
Uploads are resumable, served from a tus endpoint at /api/imports/uploads, and pinned to a gzipped tar, a zip or a CSV. The upload id is <tenant>.<random>, and the tenant prefix is the whole isolation story: a request naming another tenant's upload answers the same 404 as a guessed one.
Columns
Contacts accept email, phone, name, external_id, locale, timezone and country. Companies accept name, external_id, domains, plan, monthly_spend, size, industry and website. A mapping to a field that does not exist answers a 422 naming the key, rather than a silent no-op column once the job runs.
Map and run
The mapping has two parts: columns maps a field to a header, and attributes maps a custom attribute key to a header. Attribute keys must already exist as definitions, and their values are coerced to the definition's display type.
curl -X POST http://127.0.0.1/api/imports/runs \
-H 'content-type: application/json' \
-H 'authorization: Bearer fk_<key>' \
-d '{"source":"csv","entity":"contact","upload_id":"<upload id>","mapping":{"columns":{"email":"Email Address","name":"Full Name"},"attributes":{"plan":"Plan"}}}'The job streams the file in batches of a hundred, so progress is persisted per batch and a long import never grows the run row past what one poll wants to read.
Row errors
A bad row does not fail the run. The line number and message go into the run's errors, capped at a hundred entries, and the failed count increments. A value that cannot coerce to a number or boolean is a line error, because writing it would store the wrong shape against the definition.