Import contacts
Send a list of contacts in one request with Import contacts. The import runs in the background as a bulk job: the reply gives you a job_id, and Get a bulk job shows its progress and result. To add one contact, use Create a contact.
The helpers read a Google Sheet or a file into rows you can import. To find and merge duplicate contacts, see Duplicates and merge.
#The Bulk job object
idstring- The job id, as a string of digits. It’s the
job_idan import returns. user_id, owner_idinteger- Who started the job, and the account it belongs to.
kindstringcontacts_import,automation_enrollment,clean(a list clean) orcontacts_bulk_edit(a big tag, untag, assign or opt-out).statusstringqueuedorprocessingwhile it runs, thencompleted,failedorcancelled.file_namestring- The job’s label, or
null. total, processedinteger- Rows sent, and rows worked through so far.
processedmoves in steps of a few hundred rows. inserts, updates, skipped, errorsinteger- New contacts, existing contacts updated, rows skipped, and rows that couldn’t be saved.
error_textstring- Why a
failedjob stopped, in words you can show.nullotherwise. result_jsonobject- What a finished import did, or
nullbefore it finishes.Child attributes
imported,inserts,updates,skipped,errors,total: the counts.reasonsays why rows failed whenerrorsis above 0.duplicates: rows that matched a contact you already have (or an earlier row) inadd_onlymode.invalid_phones: rows with no usable phone.landlines: always0; the import no longer looks up carriers (the line-type check flags landlines on the contacts instead).opted_out: numbers the file marked as opted out.opted_out_unrecognized: rows whoseopted_outvalue we couldn’t read.tag_applied,tags_applied_count,tags_failed: the tags you asked for, how many contacts got them, and any tag that wasn’t applied.enroll_sequence_idsandenroll_failed;automation_rule_ids;list_id,list_name,list_addedorlist_failed;assigned_countandassign_failed.scrub_matched,scrub_clean,scrub_cost_usd(dollars),scrub_aborted(why the scrub didn’t run or finish, withneed_centsandhave_centswhen your balance was too low).touched_count: contacts the import added or updated.line_type_sample: the landline sample, when one was judged.invalid_details: up to 50 skipped rows with a reason.skipped_rows: up to 20,000 skipped rows (skipped_rows_truncatedistruepast that).refused:mostly_landlineorscrub_settings_unreadablewhen the import stopped for that reason.
payload_jsonobject- The import body you sent, every row included. Only Get a bulk job without
summary=1returns it. created_at, started_at, updated_at, completed_atstring- ISO 8601.
started_atandcompleted_atarenulluntil then.
Fields other job kinds use
consent_attested_at, consent_attested_text, consent_attested_ip, consent_attested_user_agentstring- Consent evidence some job kinds keep.
nullon an import, whose evidence is kept elsewhere.
A team member who can’t view contacts, or who sees only their assigned contacts, gets every job without payload_json, skipped_rows, invalid_details and landline_details.
#Import contacts
Adds or updates many contacts in one request. Returns 202 with a job_id at once, and the rows are worked through in the background: poll Get a bulk job with summary=1 until status is completed, failed or cancelled. The body can be up to 50 MB; there is no row limit below that. Needs the edit_contacts permission, plus send_sms to enroll in drip campaigns and manage_campaigns to run automations.
Two steps can charge your balance. apply_scrub: true runs the TCPA scrub on every contact the import adds or updates, charged at the price GET /contacts/scrub-quote?count=N quotes. And unless you send validate_line_type: false, Ready checks the line type of each new contact after the import and charges for every number it gets an answer for, at the price GET /contacts/linetype-quote?count=N quotes. If your account scrubs every import (a compliance setting), new contacts are scrubbed and charged even without apply_scrub. If your balance can’t cover apply_scrub, the scrub is skipped and not charged; the contacts are still imported.
The same body sent again within 2 minutes, or while its job still runs, returns the first job’s 202 with an Idempotent-Replayed: true header. No second import is started.
Parameters
contactsobject[]Required- The rows to import, at least one. A row without a
phoneis skipped.Row fields
phone: the number. A trailing extension is dropped. Invalid area codes, restricted numbers and 555-01xx sample numbers are always skipped.first_name,last_name,email,company_name,website,address1,city,state,postal_code(orzip).tags: an array, or one string split on commas, semicolons or|. On an existing contact they’re added to its tags.custom_fields: an object of field name to value. On an existing contact, the keys you send replace its values for those keys.assigned_to: a team member’s name, email or id. Left unassigned if we can’t match it.opted_out: see Opt-outs in a file.
Property fields (real estate)
property_address,property_city,property_state,property_zip,property_type,last_sale_date, and the numbersmortgage_balance,equity_value,estimated_value,home_value,last_sale_price,bathrooms,lot_size,year_built,bedrooms,square_feet,years_owned. A number that doesn’t read as one fails that row. consent_attestedbooleanRequired- Send
trueto confirm the people you’re importing agreed to get texts from you. Without it nothing is imported.
More parameters
consent_attestation_textstring- The consent words you showed, kept with the import as evidence.
modestringDefaultadd_onlyadd_onlyadds new contacts and leaves matches alone.update_onlyupdates matches and skips new rows.upsertdoes both.dedupe_keysstring[]Default["phone"]- How a row matches a contact you have:
phone,email,address1,first_last, orcf:plus a custom field key. Up to 8; unknown keys are ignored. dedupe_match_allbooleanDefaultfalsetrue: a row matches only when every key matches. Otherwise any one is enough.apply_tagstring or string[]- Tags to add to every contact the import adds or updates.
Opted Outis refused and reported intags_failed. list_idinteger- A smart list to put the imported contacts on.
list_namestring- Makes a new list with this name for the imported contacts.
list_idwins if you send both. enroll_sequence_idsinteger[]- Drip campaigns to enroll every contact the import adds or updates, including existing contacts a row matched in
update_onlyorupsertmode. Needssend_sms. automation_rule_idsinteger[]- Automations to run on the imported contacts. Needs
manage_campaigns. assign_tointeger- A team member id. Imported contacts with no assignee are assigned to them.
apply_scrubbooleanDefaultfalse- Runs the paid TCPA scrub on every contact the import adds or updates. Charged.
validate_line_typebooleanDefaulttrue- Checks each new contact’s line type and flags landlines. Charged per answered number. Send
falseto skip it. validate_phonesbooleanDefaultfalsetruealso skips numbers of the wrong length, which are otherwise imported.file_namestring- A label for the job.
confirm_reimportboolean- Send
trueto run a file again afterIMPORT_STOPPED_PARTWAY.
contacts and consent_attested are all a first import needs. To also update contacts you already have, add "mode": "upsert".
Switches (apply_scrub, validate_phones, dedupe_match_all) take true, false, 1 or 0. validate_line_type takes only true or false.
Opt-outs in a file
A row whose opted_out says the number opted out (yes, true, 1, x, opted out, unsubscribed, dnc, stop and similar), or whose tags include Opted Out, is recorded as an opt-out, the same as a STOP reply. That happens in every mode, even when the row is otherwise skipped as a duplicate. A new contact with that number is added with texting off. A value can only add an opt-out, never clear one: no, blank and anything else leave the number as it was. A number already opted out on your account is imported with texting off too.
After the rows
Ready checks the line type of a sample of up to 30 new numbers, free. If fewer than half of them are mobile, the contacts stay added but the job ends failed with refused: "mostly_landline", and nothing is tagged, listed, enrolled, assigned or scrubbed. Then it runs the scrub, so a number it flags is locked before anything can text it, and applies tags and drips, the list, the assignee and automations, in that order. A Cancel stops the remaining rows; rows already saved stay.
Returns
job_idstring- The bulk job. Pass it to Get a bulk job.
status, total, asyncstring, integer, booleanqueued, the number of rows sent, andtrue.
Errors
| HTTP | Code | When |
|---|---|---|
| 400 | NO_CONTACTS | contacts is missing, empty or not a list. |
| 400 | import_consent_required | consent_attested isn’t true. |
| 400 | invalid_field | An option has the wrong type, such as an unknown mode, a switch sent as "false", or dedupe_keys that isn’t a list. |
| 403 | permission_denied | No edit_contacts, or the import asks for drips or automations you can’t use. Nothing was imported. |
| 409 | import_in_progress | The same body is being queued right now. Wait a moment and check your jobs. |
| 409 | IMPORT_STOPPED_PARTWAY | The same body started in the last 24 hours and stopped partway, so some rows may already be in. The body has that job_id. Send again with confirm_reimport: true to run it anyway. |
| 413 | PAYLOAD_TOO_LARGE | The body is over 50 MB. Split the rows into several imports. |
| 500 | IMPORT_QUEUE_FAILED | The job couldn’t be started. Nothing was imported; try again. |
| 503 | IMPORT_CLAIM_UNAVAILABLE | We couldn’t safely start it. Nothing was queued; try again in a minute. |
#List bulk jobs
Your account’s bulk jobs, newest first: imports, automation enrollments and list cleans. Any team member can read them.
Parameters
kindstring · query- Only this kind, for example
contacts_import. limitinteger · queryDefault50Max200recent_minutesinteger · query- Only jobs still running, plus those that changed in the last N minutes (7 days at most).
Returns a list of Bulk job objects in data, without payload_json and without skipped_rows in result_json.
Errors
| HTTP | Code | When |
|---|---|---|
| 400 | invalid_field | recent_minutes isn’t a whole number from 1 to 999999. |
#Get a bulk job
One job, with its progress and, once it finishes, its result. Poll this every few seconds while an import runs. Any team member can read it.
Parameters
idstring · pathRequired- The job id.
summaryinteger · query1leaves outpayload_jsonandskipped_rows, which can be several MB. Use it when you poll.
Returns a Bulk job object in data. The sample result is a finished import of two mobile numbers. The two 555-01xx numbers in the Import contacts sample are sample numbers, which an import always skips, so use your own numbers to get a result like this one.
Errors
| HTTP | Code | When |
|---|---|---|
| 400 | invalid_field | The id isn’t a whole number. |
| 404 | job_not_found | No job with this id on your account, or it was removed. |
#Download skipped rows
The rows an import skipped or couldn’t save, as a CSV file with a Skip Reason column, so you can fix them and import just those. Up to 20,000 rows. Needs the view_contacts permission.
Parameters
idstring · pathRequired- The job id.
Returns text/csv with the columns First Name, Last Name, Phone, Email, Company and Skip Reason. A job with no skipped rows returns the header line only.
Errors
| HTTP | Code | When |
|---|---|---|
| 403 | contacts_hidden | Your seat sees only its assigned contacts. |
| 404 | job_not_found | No job with this id on your account. |
#Cancel a bulk job
Stops a queued or processing job. It stops at its next checkpoint, so a few more rows may go in; rows already saved stay. Cancelling a job that already finished is not an error: you get 200 with note: "Job already finished". Needs the edit_contacts permission.
Parameters
idstring · pathRequired- The job id. Send no body.
Returns the Bulk job object in data, as summary=1 returns it.
Errors
| HTTP | Code | When |
|---|---|---|
| 400 | invalid_field | The id isn’t a whole number, or you sent a body. |
| 404 | job_not_found | No job with this id on your account. |
| 429 | rate_limited | Too many job changes in the last minute. Wait a minute. |
| 500 | cancel_not_saved | The cancel didn’t save. Try again. |
#Remove a bulk job
Removes a finished job from your list. The contacts it imported stay. Cancel a running job first. Needs the edit_contacts permission.
Parameters
idstring · pathRequired- The job id.
Returns { "success": true }.
Errors
| HTTP | Code | When |
|---|---|---|
| 400 | invalid_field | The id isn’t a whole number, or you sent a body. |
| 404 | job_not_found | No job with this id on your account, or already removed. |
| 409 | job_still_running | The job is still queued or processing. |
| 429 | rate_limited | Too many job changes in the last minute. Wait a minute. |
#Rename a bulk job
Changes the job’s label (file_name) and nothing else. Needs the edit_contacts permission.
Parameters
idstring · pathRequired- The job id.
namestringRequired- The new label. Trimmed, and cut to 200 characters.
Returns the Bulk job object in data, as summary=1 returns it.
Errors
| HTTP | Code | When |
|---|---|---|
| 400 | name_required | name is missing or blank. |
| 400 | invalid_field | The id isn’t a whole number, or name is a list or object. |
| 404 | job_not_found | No job with this id on your account. |
| 429 | rate_limited | Too many job changes in the last minute. Wait a minute. |
#Count numbers you already have
How many of these phone numbers already belong to a contact on your account, matched the way an import matches on phone. Changes nothing. The first 20,000 different numbers are counted; the rest are ignored. Needs the edit_contacts permission.
Parameters
phonesstring[]- The numbers to check. Values that aren’t phone numbers are dropped.
Returns
totalinteger- Different numbers checked.
existing, new_countinteger- How many you already have, and how many are new. These are at the top level, not in
data.
Errors
| HTTP | Code | When |
|---|---|---|
| 400 | invalid_field | phones isn’t a list. |
| 500 | dedupe_check_failed | We couldn’t check. Try again in a minute. |
#Read a Google Sheet
Reads a public Google Sheet and returns its rows as import rows. Nothing is imported: send import_payload as contacts to Import contacts. The sheet must be shared as “Anyone with the link can view”. Needs the edit_contacts permission.
Columns are matched by their headers to first_name, last_name, phone, email, company_name, city, state, postal_code and opted_out; a full name column fills empty first and last names. With no phone header, the first column that holds phone numbers is used. Up to 50,000 rows are read, and rows without a phone are dropped. The sheet can be up to 15 MB.
Parameters
sheet_urlstringRequired- The sheet’s link, or its spreadsheet id. A
gidin the link picks the tab; otherwise the first tab is read.
Returns
total_rowsinteger- Data rows read, before rows without a phone were dropped.
truncatedbooleantruewhen the sheet had more than 50,000 data rows and only the first 50,000 were read. Export the rest as a CSV and import it separately.import_payloadobject[]- The rows, ready to import.
skipped_columnsstring[]- The sheet’s column headers that are not imported. Only name, phone, email, company, city, state, ZIP and opt-out columns are read.
Errors
| HTTP | Code | When |
|---|---|---|
| 400 | sheet_url_required | No sheet_url. |
| 400 | sheet_url_invalid | It isn’t a Google Sheets link. |
| 400 | sheet_not_public | The sheet isn’t shared publicly. |
| 400 | sheet_no_header | The sheet has no header row and contacts. |
| 413 | sheet_too_large | Over 15 MB. Export it as CSV and split it. |
| 502 | sheet_fetch_failed | Google didn’t send the sheet, or couldn’t find it. Check the link, or try again in a minute. |
#Convert a file to CSV
Turns an Excel, PDF or tab or pipe separated file into CSV text. Nothing is imported: map the columns yourself and send the rows to Import contacts. A PDF needs a text layer; a scanned PDF is refused. Needs the edit_contacts permission.
Parameters
filefile · multipartRequired- A
.csv,.xlsx,.xls,.xlsm,.pdf,.tsvor.txtfile, up to 15 MB, sent asmultipart/form-data. The first sheet with data in a workbook is used.
Returns { "csv": "..." } in data.
Errors
| HTTP | Code | When |
|---|---|---|
| 400 | FILE_MISSING | No file, or an empty one. |
| 400 | upload_failed | The upload wasn’t valid multipart. |
| 400 | file_unreadable | We couldn’t read it. reason is UNSUPPORTED_TYPE, NO_TABLE, TSV_TITLE_ROW, SCANNED_PDF or NO_SHEET when we know why. |
| 413 | file_too_large | Over 15 MB. limit_mb says the limit. |
| 422 | file_empty | The file has no rows. |