readyDocs Open the appOpen the app

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_id an import returns.
user_id, owner_idinteger
Who started the job, and the account it belongs to.
kindstring
contacts_import, automation_enrollment, clean (a list clean) or contacts_bulk_edit (a big tag, untag, assign or opt-out).
statusstring
queued or processing while it runs, then completed, failed or cancelled.
file_namestring
The job’s label, or null.
total, processedinteger
Rows sent, and rows worked through so far. processed moves 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 failed job stopped, in words you can show. null otherwise.
result_jsonobject
What a finished import did, or null before it finishes.
Child attributes
  • imported, inserts, updates, skipped, errors, total: the counts. reason says why rows failed when errors is above 0.
  • duplicates: rows that matched a contact you already have (or an earlier row) in add_only mode. invalid_phones: rows with no usable phone. landlines: always 0; 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 whose opted_out value 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_ids and enroll_failed; automation_rule_ids; list_id, list_name, list_added or list_failed; assigned_count and assign_failed.
  • scrub_matched, scrub_clean, scrub_cost_usd (dollars), scrub_aborted (why the scrub didn’t run or finish, with need_cents and have_cents when 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_truncated is true past that).
  • refused: mostly_landline or scrub_settings_unreadable when the import stopped for that reason.
payload_jsonobject
The import body you sent, every row included. Only Get a bulk job without summary=1 returns it.
created_at, started_at, updated_at, completed_atstring
ISO 8601. started_at and completed_at are null until 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. null on 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 phone is 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 (or zip).
  • 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.
On an update, only the fields a row fills are written. A row never changes an existing contact’s phone number.
Property fields (real estate)

property_address, property_city, property_state, property_zip, property_type, last_sale_date, and the numbers mortgage_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.

More parameters
modestringDefault add_only
add_only adds new contacts and leaves matches alone. update_only updates matches and skips new rows. upsert does both.
dedupe_keysstring[]Default ["phone"]
How a row matches a contact you have: phone, email, address1, first_last, or cf: plus a custom field key. Up to 8; unknown keys are ignored.
dedupe_match_allbooleanDefault false
true: 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 Out is refused and reported in tags_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_id wins 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_only or upsert mode. Needs send_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_scrubbooleanDefault false
Runs the paid TCPA scrub on every contact the import adds or updates. Charged.
validate_line_typebooleanDefault true
Checks each new contact’s line type and flags landlines. Charged per answered number. Send false to skip it.
validate_phonesbooleanDefault false
true also skips numbers of the wrong length, which are otherwise imported.
file_namestring
A label for the job.
confirm_reimportboolean
Send true to run a file again after IMPORT_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, boolean
queued, the number of rows sent, and true.
Errors
HTTPCodeWhen
400NO_CONTACTScontacts is missing, empty or not a list.
400import_consent_requiredconsent_attested isn’t true.
400invalid_fieldAn option has the wrong type, such as an unknown mode, a switch sent as "false", or dedupe_keys that isn’t a list.
403permission_deniedNo edit_contacts, or the import asks for drips or automations you can’t use. Nothing was imported.
409import_in_progressThe same body is being queued right now. Wait a moment and check your jobs.
409IMPORT_STOPPED_PARTWAYThe 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.
413PAYLOAD_TOO_LARGEThe body is over 50 MB. Split the rows into several imports.
500IMPORT_QUEUE_FAILEDThe job couldn’t be started. Nothing was imported; try again.
503IMPORT_CLAIM_UNAVAILABLEWe 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 · queryDefault 50Max 200
recent_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
HTTPCodeWhen
400invalid_fieldrecent_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 · query
1 leaves out payload_json and skipped_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
HTTPCodeWhen
400invalid_fieldThe id isn’t a whole number.
404job_not_foundNo 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
HTTPCodeWhen
403contacts_hiddenYour seat sees only its assigned contacts.
404job_not_foundNo 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
HTTPCodeWhen
400invalid_fieldThe id isn’t a whole number, or you sent a body.
404job_not_foundNo job with this id on your account.
429rate_limitedToo many job changes in the last minute. Wait a minute.
500cancel_not_savedThe 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
HTTPCodeWhen
400invalid_fieldThe id isn’t a whole number, or you sent a body.
404job_not_foundNo job with this id on your account, or already removed.
409job_still_runningThe job is still queued or processing.
429rate_limitedToo 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
HTTPCodeWhen
400name_requiredname is missing or blank.
400invalid_fieldThe id isn’t a whole number, or name is a list or object.
404job_not_foundNo job with this id on your account.
429rate_limitedToo 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
HTTPCodeWhen
400invalid_fieldphones isn’t a list.
500dedupe_check_failedWe 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 gid in the link picks the tab; otherwise the first tab is read.
Returns
total_rowsinteger
Data rows read, before rows without a phone were dropped.
truncatedboolean
true when 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
HTTPCodeWhen
400sheet_url_requiredNo sheet_url.
400sheet_url_invalidIt isn’t a Google Sheets link.
400sheet_not_publicThe sheet isn’t shared publicly.
400sheet_no_headerThe sheet has no header row and contacts.
413sheet_too_largeOver 15 MB. Export it as CSV and split it.
502sheet_fetch_failedGoogle 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, .tsv or .txt file, up to 15 MB, sent as multipart/form-data. The first sheet with data in a workbook is used.

Returns { "csv": "..." } in data.

Errors
HTTPCodeWhen
400FILE_MISSINGNo file, or an empty one.
400upload_failedThe upload wasn’t valid multipart.
400file_unreadableWe couldn’t read it. reason is UNSUPPORTED_TYPE, NO_TABLE, TSV_TITLE_ROW, SCANNED_PDF or NO_SHEET when we know why.
413file_too_largeOver 15 MB. limit_mb says the limit.
422file_emptyThe file has no rows.