Import contacts from a CSV
const url = 'https://api.threadkite.app/v1/contacts/imports?columns=name%2Cemail%2Cphone%2Ccompany_name%2C%2Cconsent%3Aemail';const form = new FormData();form.append('file', 'file');
const options = {method: 'POST', headers: {Authorization: 'Bearer <token>'}};
options.body = form;
try { const response = await fetch(url, options); const data = await response.json(); console.log(data);} catch (error) { console.error(error);}curl --request POST \ --url 'https://api.threadkite.app/v1/contacts/imports?columns=name%2Cemail%2Cphone%2Ccompany_name%2C%2Cconsent%3Aemail' \ --header 'Authorization: Bearer <token>' \ --header 'Content-Type: multipart/form-data' \ --form file=@fileChecks the file and its columns as the preview does (a file that cannot be imported is refused at once), stores it and starts the import, which runs in the background: follow it with GET /v1/contacts/imports/{id}. The import adds a contact for every new person and fills the blanks of the contacts you already have (nothing is overwritten), finds or adds the companies they work for and links them (their primary company when they have none), and records consent, as given in an import, where a consent column says yes (an opt-out already recorded stands). Rows with problems are skipped and reported. Says contact.created, contact.updated, company.created and contact.consent_changed as usual. Importing the same file again changes nothing. The file is deleted once the import has run.
Authorizations
Section titled “Authorizations”Parameters
Section titled “Parameters”Query Parameters
Section titled “Query Parameters”What each column holds, in the order of the columns, separated by commas: name, first_name, last_name, email, phone, company_name, company_number, company_domain, job_title, role, field:<contact field key> or consent:<email|sms|whatsapp|phone> (yes records an opt-in), or nothing to leave a column out. Left out: guessed from the headers (the preview says what it guessed)
Example
name,email,phone,company_name,,consent:emailWhat each column holds, in the order of the columns, separated by commas: name, first_name, last_name, email, phone, company_name, company_number, company_domain, job_title, role, field:<contact field key> or consent:<email|sms|whatsapp|phone> (yes records an opt-in), or nothing to leave a column out. Left out: guessed from the headers (the preview says what it guessed)
Request Bodyrequired
Section titled “Request Bodyrequired”object
CSV, up to 5 MB and 10,000 rows, with a header row
Responses
Section titled “Responses”object
Pass to GET /v1/contacts/imports/{id} to follow it
queued (waiting to start), running, done (see result) or failed (see error)
Data rows in the file
Rows gone through so far
object
Data rows in the file
New contacts
Contacts you already had, matched by email address then phone number, with blanks filled, a company linked or consent recorded (nothing is overwritten)
Contacts you already had that the file adds nothing to
Rows matching a contact in an inbox you are not in: left as they are, and not shown
New companies
Companies you already had, matched by company number, then domain, then name
Rows that cannot be imported (see errors)
Rows that were skipped and why (the first 100), without the values they quote: “This is not an email address”
object
The line in the file (the header is 1)
Why it failed, when it did
Example
{ "status": "queued", "rows": 10000, "rowsDone": 2400, "result": { "rows": 120, "contactsCreated": 80, "contactsUpdated": 25, "contactsUnchanged": 10, "contactsSkipped": 0, "companiesCreated": 12, "companiesMatched": 30, "rowsWithErrors": 5, "errors": [ { "line": 7, "messages": [ "No email address or phone number" ] } ] }}object
Stable machine-readable reason
With code invalid: every problem with the request, by field (message is the first)
object
Dotted path inside the item, e.g. attributes.price.amountMinor
Example
{ "statusCode": 403, "code": "forbidden", "message": "You do not have permission to do that.", "errors": [ { "message": "Enter the make" } ]}Missing or invalid API key
object
Stable machine-readable reason
With code invalid: every problem with the request, by field (message is the first)
object
Dotted path inside the item, e.g. attributes.price.amountMinor
Example
{ "statusCode": 403, "code": "forbidden", "message": "You do not have permission to do that.", "errors": [ { "message": "Enter the make" } ]}The organisation is on the Free plan (code upgrade_required)
object
Stable machine-readable reason
The feature that needs Pro
Where to upgrade or pay, in the web app
Example
{ "statusCode": 402, "code": "upgrade_required", "message": "Connections are part of Pro. Upgrade to connect texts, calls and more.", "feature": "extraSeats", "upgradePath": "/settings/billing"}The key does not have the scope (or the person the permission)
object
Stable machine-readable reason
With code invalid: every problem with the request, by field (message is the first)
object
Dotted path inside the item, e.g. attributes.price.amountMinor
Example
{ "statusCode": 403, "code": "forbidden", "message": "You do not have permission to do that.", "errors": [ { "message": "Enter the make" } ]}object
Stable machine-readable reason
With code invalid: every problem with the request, by field (message is the first)
object
Dotted path inside the item, e.g. attributes.price.amountMinor
Example
{ "statusCode": 403, "code": "forbidden", "message": "You do not have permission to do that.", "errors": [ { "message": "Enter the make" } ]}Rate limit reached
object
Stable machine-readable reason
Seconds until another request will be accepted
Example
{ "statusCode": 429, "code": "rate_limited", "message": "Too many requests. Each key can make 120 requests a minute; try again in 12 seconds.", "retryAfter": 12}