A valid request URL is required to generate request examples{
"import": {
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"filename": "<string>",
"columns": [
"<string>"
],
"status": "pending",
"total": 123,
"pending": 123,
"created": 123,
"matched": 123,
"conflicts": 123,
"failed": 123,
"created_at": "2023-11-07T05:31:56Z",
"finished_at": "2023-11-07T05:31:56Z"
}
}{
"error": {
"code": "forbidden",
"message": "your role does not allow this action"
}
}{
"error": {
"code": "forbidden",
"message": "your role does not allow this action"
}
}{
"error": {
"code": "forbidden",
"message": "your role does not allow this action"
}
}{
"error": {
"code": "forbidden",
"message": "your role does not allow this action"
}
}{
"error": {
"code": "forbidden",
"message": "your role does not allow this action"
}
}Import contacts from a CSV
Requires customers:write. Answers 202: the file is accepted and durable, and none of it has been applied yet — a worker applies the lines in batches, and the report fills in as it goes.
Send the file as text/csv, or as JSON with the contents in a csv field.
What the header decides
email and external_id are the identifiers, and the file needs at least one of the two columns. Without either, every line would fail for the same reason, so the file is refused instead of imported into a report of identical failures.
name, customer, customer_role and the first-touch columns — utm_source, utm_medium, utm_campaign, utm_term, utm_content, referrer, landing_page — are read when present. Everything else is ignored, because a real export carries a dozen columns this has no place for.
A password has nowhere to go: UserKit never signs a contact in, so password_hash — also spelled passwordhash, password_digest, encrypted_password, hashed_password — and the algorithm columns beside it (password_hasher, password_algorithm, hash_algorithm) are refused by name with unimportable_column, rather than ignored: a column dropped in silence is a migration somebody believes carried the credential, discovered at the first sign-in that does not work. A plaintext password column is refused whatever it holds. Your own authentication keeps the credential.
What a line does
A line naming somebody who does not exist here creates them, with everything the line carried.
A line naming somebody who already exists matches them and leaves their profile alone. An export from your previous provider is not evidence about a contact this environment already has — their name and address are where their own security notices go, and a stale row is not a reason to redirect them.
A line whose email belongs to one contact and whose external_id belongs to another is reported as a conflict, and nothing is written for it. Identifying values never re-point, and there is no un-merge — so an import that resolved collisions on its own would produce wrong merges in bulk, at the exact moment somebody is trusting it with their whole audience. The report names both contacts, which is what POST /v1/organization/contacts/{id}/merge takes.
A line that cannot be read is reported and the import keeps going. Stopping at line 4,312 is useless to somebody migrating; ignoring it silently is worse, because they finish believing everybody arrived.
Two columns are refused by name
No email is sent
Not one message, of any kind. An import is the fastest way to mail ten thousand addresses that have never heard from you, and one of those ends a sending domain’s reputation. The report is where the outcome lives instead.
A valid request URL is required to generate request examples{
"import": {
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"filename": "<string>",
"columns": [
"<string>"
],
"status": "pending",
"total": 123,
"pending": 123,
"created": 123,
"matched": 123,
"conflicts": 123,
"failed": 123,
"created_at": "2023-11-07T05:31:56Z",
"finished_at": "2023-11-07T05:31:56Z"
}
}{
"error": {
"code": "forbidden",
"message": "your role does not allow this action"
}
}{
"error": {
"code": "forbidden",
"message": "your role does not allow this action"
}
}{
"error": {
"code": "forbidden",
"message": "your role does not allow this action"
}
}{
"error": {
"code": "forbidden",
"message": "your role does not allow this action"
}
}{
"error": {
"code": "forbidden",
"message": "your role does not allow this action"
}
}Authorizations
A staff session token, uk_st_…. Minted by sign-up, sign-in or the two-factor exchange. Only a 401 means it is spent; not_a_member (403) is about the organization named in X-Organization-Id and leaves the token good for the others.
Headers
The organization the caller is acting on — the org_… code that appears in the panel URL. It identifies; the membership JOIN is what authorizes, so a forged code reads nothing: the answer is not_a_member (403), which does not mean the session is over. Absent, the session's default organization answers, or — if that membership was revoked while the session was open — any other one the caller still holds.
Query Parameters
Which environment the contacts land in. Required here, unlike every read on this surface: a read of the wrong environment is a wasted click, and ten thousand contacts written into the wrong one cannot be un-written.
live, test What to call the file in the report. Only read with a text/csv body; the JSON body carries its own filename.
Body
The body is of type string.
"email,name,external_id,customer\nana@example.com,Ana,usr_1,Acme Inc\nbia@example.com,Bia,usr_2,Acme Inc\n"
Response
Accepted. Nothing has been applied yet — poll the import to watch it.
One CSV import. Progress is derived from its lines on every read rather than accumulated as the worker goes — the tallies are a query, never a counter, because a counter maintained by a job the queue may retry double-counts.
Show child attributes
Show child attributes