Importing Contacts

Importing a CSV is how an existing list becomes contacts in Leed. It is also the only self-serve way to create accounts. The whole flow is two steps — choose a file, map its columns — with one honest checkbox in between.

Importing is not gated on any plan: Free imports the same file Enterprise does. What is capped is how many of the resulting contacts you can then see, and that cap is a quota rather than a feature gate — Contacts explains what that looks like.

Where the importer is

In the Engage workspace, select Contacts in the section picker, then the Import CSV button beside + at the top of the left panel. Or go straight to /engage/contacts/import — the importer is a real URL, not a modal, so it survives a reload and can be bookmarked.

It needs contact:write, which starts at the Content Publisher role; without it the button is not rendered at all. See Roles and permissions.

Step one: the file

Drag a CSV onto the dashed drop zone — it reads “Drag a CSV here, or click to choose” — or select it to open a file picker.

Only text/csv is accepted. Anything else is refused in place, with the drop zone’s own label replaced by “Only CSV filetypes are supported.” A spreadsheet saved as .xlsx is the usual culprit; export it as CSV and try again.

The contact importer's first step, with the heading and the dashed drop zone

There is no server-side size limit on this route. There is a practical one: the whole file is parsed in your browser and posted as JSON, so a list in the hundreds of thousands of rows is better split into several imports.

Step two: name and map

The file is parsed as soon as it lands, and the importer switches to the mapping step.

Contact group name is pre-filled with the file’s name minus its .csv extension. It becomes two things at once: the name of the import batch you can filter by later, and the name of the contact group Leed creates for you. Rename it to something you will recognize in three months.

First row is a header is ticked by default. Leave it ticked for a normal export. Unticking it treats the first row as data — and clears every column mapping, so you will be mapping by hand from that point.

Then map each column.

How Leed guesses the mapping

Leed tries three things, in order, and stops at the first that works for a given column.

  1. The header name. An exact, case-insensitive match against a field name, or against its spaced form. So firstName, firstname and First Name all map to firstName; postalCode and Postal Code both map to postalCode.
  2. A short alias list, for the headers real exports actually use.
  3. A LinkedIn substring rule — any header containing “linkedin” maps to linkedinUrl.

If none of that produced an email column, Leed falls back to reading the data: it samples the first 25 rows and picks the column whose non-empty values are at least 60% valid email addresses. That is what rescues a file headed Work E-mail, or one with no header row at all.

Header in your fileMapped to
e-mailemail
tel, phone numberphone
employeessize
web addressurl
companyname
purchasedpurchasedProducts
application, competitorscompetitiveProducts
revenue, revenuesrevenue
sic, sic code, sic codessicCodes
zip, zip codepostalCode
anything containing linkedinlinkedinUrl

The mapping table

Each column gets a dropdown, headed by that column’s header text (or Column N where the header cell is empty), above a preview of the first ten data rows and the line Showing first 10 of N rows.

A mapped column’s dropdown has a neutral border. An unmapped one is outlined in amber and set to Ignore — a visual reminder rather than a warning, because ignoring columns is normal and expected. Unmapped columns are simply not read.

The mapping step with a file loaded, an auto-mapped Email column, an unmapped column, and the disabled import button

What blocks the import

Two conditions block, and one warns:

MessageBlocksWhat to do
Only CSV filetypes are supported.Yes — the file is never readRe-export the file as CSV
The same field is mapped to multiple columns.YesSet one of the duplicates back to Ignore
Map one column to Email to continue.YesPick the column holding email addresses
N row(s) have an invalid email and will be skipped.NoNothing, unless N is larger than you expected — then check you mapped the right column
400 email mapping not detectedYes, on a direct API callInclude a mapping whose value is email
400 email mapping invalidYes, on a direct API callUse the column’s numeric index as the mapping key

The permission checkbox

Below the mapping table: “I have permission to email everyone on this list.” The Import N contacts button stays disabled until it is ticked and every blocking error is cleared.

What happens when you import

The work is split across three places, which is why imported contacts appear a minute or so later rather than instantly, and why a mapping mistake is caught in your browser rather than after the fact.

flowchart TD
  A["CSV parsed in your browser"] --> B["Header auto-map"]
  B --> C{"Email column found?"}
  C -->|no| D["Sample 25 rows,<br/>pick the column that is 60%+ email addresses"]
  C -->|yes| E["Blocking checks:<br/>no duplicate field, email mapped"]
  D --> E
  E --> F["POST /api/contactgroup/upload"]
  F --> G["Batch created, type: upload"]
  G --> H["Rows with an empty email cell dropped"]
  H --> I["Remaining rows queued,<br/>ten per message"]
  I --> J["Company names upserted<br/>into accounts"]
  J --> K["Contacts upserted on<br/>workspace + email address"]
  K --> L["Contacts added to the batch"]
  G --> M["Contact group created,<br/>named after the import"]

The batch is created before the rows are queued, so the import shows up in the batch list immediately. A contact group is created alongside it, named after your import with the description “Contact group created automatically from upload”— it is usable as an email audience the moment the rows land. See Contact groups and audiences.

Company columns and accounts

Map a column to the company name field and Leed creates an account for each distinct value in it, then links each contact to the right one. This is the only self-serve way accounts come into existence — there is no + button on the Accounts list, as Accounts explains.

Account names are matched character for character, so normalize them in the CSV before you import. “Acme Inc” and “Acme, Inc.” become two separate accounts with two separate committees.

Duplicates

The email address is the key, within your workspace.

An address you already hold updates the existing contact rather than creating a second record. The rule is precise and worth knowing before you re-import: the columns you mapped are written — including with a blank cell, which clears the stored value — and columns you did not map are left alone. A contact’s source and creation date are insert-only, so an imported row never re-stamps a form-fill contact as upload.

A row whose email cell is empty is dropped before anything is written, and never reaches the queue. A row whose email cell holds something that is not an address counts toward the “N row(s) have an invalid email” warning.

What you can map

FieldBelongs toStoredNotes
emailContactYesRequired — the import cannot proceed without it
firstNameContactYes
lastNameContactYes
titleContactYesDrives the inferred committee role and the persona match
phoneContactYes
linkedinUrlContactYes
countryContactYesWritten to the contact, not the account
timezoneContactYesAn invalid zone is discarded rather than stored
emailDomainContactDerivedAlways recomputed from the email address; mapping it has no effect
nameAccountYesCreates and links the account
industry, size, url, revenue, sicCodesAccountNoOffered by the dropdown, discarded on import
address, address2, city, state, postalCodeAccountNoOffered by the dropdown, discarded on import
purchasedProducts, competitiveProductsAccountNoOffered by the dropdown, discarded on import

Imported contacts start at the lowest buying-intent baseline of any source — an uploaded address is the weakest evidence of interest Leed has. That baseline, and everything that lifts it afterwards, is on Engage scores.

Calling the API directly

The importer posts to a single endpoint. headerMapping is keyed by column index as a string; values is the data rows, without the header row.

POST /api/contactgroup/upload
{
  "name": "Q3 conference list",
  "headerMapping": { "0": "email", "1": "firstName", "2": "lastName", "4": "name" },
  "values": [
    ["jordan@acme.com", "Jordan", "Rivera", "", "Acme Inc"],
    ["sam@globex.com", "Sam", "Okafor", "", "Globex"]
  ]
}

The response carries the new batchId and the auto-created contactGroup. Two failures are possible: 400 "email mapping not detected" when no mapping value is email, and 400 "email mapping invalid" when the mapping key is not a column number.

My file has no header row

Untick First row is a header. Two things happen: the first row is treated as data, and every column mapping is cleared — including any Leed had guessed — so you map all of them by hand. The dropdowns lose their header labels too, since there are none to show, and you match columns against the ten-row preview underneath.

The email-detection fallback still runs before you get here, so the email column is usually already found by content even in a headerless file. Check it against the preview before you continue: a column of addresses is easy to confirm at a glance, and getting it wrong means importing a list keyed on the wrong thing.

The simpler alternative, if you can, is to add a header row in your spreadsheet first. Email, First Name, Last Name, Title, Company all auto-map, and a file that maps itself is a file you cannot mis-map.

ESC