curl --request POST \
--url https://api.lemlist.com/api/contacts \
--header 'Authorization: Basic <encoded-value>' \
--header 'Content-Type: application/json' \
--data '
{
"email": "jane.smith@example.com",
"firstName": "Jane",
"lastName": "Smith",
"linkedinUrl": "https://linkedin.com/in/janesmith",
"jobTitle": "Product Manager",
"companyDomain": "acme.com"
}
'import requests
url = "https://api.lemlist.com/api/contacts"
payload = {
"email": "jane.smith@example.com",
"firstName": "Jane",
"lastName": "Smith",
"linkedinUrl": "https://linkedin.com/in/janesmith",
"jobTitle": "Product Manager",
"companyDomain": "acme.com"
}
headers = {
"Authorization": "Basic <encoded-value>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Basic <encoded-value>', 'Content-Type': 'application/json'},
body: JSON.stringify({
email: 'jane.smith@example.com',
firstName: 'Jane',
lastName: 'Smith',
linkedinUrl: 'https://linkedin.com/in/janesmith',
jobTitle: 'Product Manager',
companyDomain: 'acme.com'
})
};
fetch('https://api.lemlist.com/api/contacts', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));require 'uri'
require 'net/http'
url = URI("https://api.lemlist.com/api/contacts")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Basic <encoded-value>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"email\": \"jane.smith@example.com\",\n \"firstName\": \"Jane\",\n \"lastName\": \"Smith\",\n \"linkedinUrl\": \"https://linkedin.com/in/janesmith\",\n \"jobTitle\": \"Product Manager\",\n \"companyDomain\": \"acme.com\"\n}"
response = http.request(request)
puts response.read_body<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.lemlist.com/api/contacts",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'email' => 'jane.smith@example.com',
'firstName' => 'Jane',
'lastName' => 'Smith',
'linkedinUrl' => 'https://linkedin.com/in/janesmith',
'jobTitle' => 'Product Manager',
'companyDomain' => 'acme.com'
]),
CURLOPT_HTTPHEADER => [
"Authorization: Basic <encoded-value>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}{
"success": true,
"data": {
"_id": "ctc_xW8Ou6C03Csv8vatp",
"teamId": "tea_8QvkOiBfPdb2ZRhHi",
"emails": [
{
"value": "jane.smith@example.com"
}
],
"fields": {
"firstName": "Jane",
"lastName": "Smith",
"jobTitle": "Product Manager"
},
"ownerId": "usr_ahfFktBBHUIxbVG5P",
"source": "api",
"createdAt": "2025-10-28T00:40:37.917Z",
"createdBy": "usr_ahfFktBBHUIxbVG5P",
"unsubscribed": false,
"created": false,
"updated": true,
"companyLinked": false
}
}{
"success": true,
"data": {
"_id": "ctc_xW8Ou6C03Csv8vatp",
"teamId": "tea_8QvkOiBfPdb2ZRhHi",
"emails": [
{
"value": "jane.smith@example.com"
}
],
"linkedinUrl": "https://www.linkedin.com/in/janesmith",
"fields": {
"firstName": "Jane",
"lastName": "Smith",
"jobTitle": "Product Manager"
},
"ownerId": "usr_ahfFktBBHUIxbVG5P",
"source": "api",
"createdAt": "2025-10-28T00:40:37.917Z",
"createdBy": "usr_ahfFktBBHUIxbVG5P",
"unsubscribed": false,
"created": true,
"companyLinked": true,
"companyId": "cpn_gG7PsmZFpEAnpMCHO"
}
}{
"success": false,
"error": {
"code": "MISSING_IDENTIFIER",
"message": "At least one identifier is required: email, linkedinUrl or contactId"
}
}"The authentication you supplied is incorrect"{
"success": false,
"error": {
"code": "CONTACT_NOT_FOUND_BY_ID",
"message": "No contact found with the provided contactId"
}
}{
"success": false,
"error": {
"code": "CONTACT_IDENTIFIER_CONFLICT",
"message": "An identifier (email, linkedinUrl or salesnavUrl) already belongs to another contact"
}
}Add and update contact
Creates a new contact or updates an existing one based on email or LinkedIn URL.
curl --request POST \
--url https://api.lemlist.com/api/contacts \
--header 'Authorization: Basic <encoded-value>' \
--header 'Content-Type: application/json' \
--data '
{
"email": "jane.smith@example.com",
"firstName": "Jane",
"lastName": "Smith",
"linkedinUrl": "https://linkedin.com/in/janesmith",
"jobTitle": "Product Manager",
"companyDomain": "acme.com"
}
'import requests
url = "https://api.lemlist.com/api/contacts"
payload = {
"email": "jane.smith@example.com",
"firstName": "Jane",
"lastName": "Smith",
"linkedinUrl": "https://linkedin.com/in/janesmith",
"jobTitle": "Product Manager",
"companyDomain": "acme.com"
}
headers = {
"Authorization": "Basic <encoded-value>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Basic <encoded-value>', 'Content-Type': 'application/json'},
body: JSON.stringify({
email: 'jane.smith@example.com',
firstName: 'Jane',
lastName: 'Smith',
linkedinUrl: 'https://linkedin.com/in/janesmith',
jobTitle: 'Product Manager',
companyDomain: 'acme.com'
})
};
fetch('https://api.lemlist.com/api/contacts', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));require 'uri'
require 'net/http'
url = URI("https://api.lemlist.com/api/contacts")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Basic <encoded-value>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"email\": \"jane.smith@example.com\",\n \"firstName\": \"Jane\",\n \"lastName\": \"Smith\",\n \"linkedinUrl\": \"https://linkedin.com/in/janesmith\",\n \"jobTitle\": \"Product Manager\",\n \"companyDomain\": \"acme.com\"\n}"
response = http.request(request)
puts response.read_body<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.lemlist.com/api/contacts",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'email' => 'jane.smith@example.com',
'firstName' => 'Jane',
'lastName' => 'Smith',
'linkedinUrl' => 'https://linkedin.com/in/janesmith',
'jobTitle' => 'Product Manager',
'companyDomain' => 'acme.com'
]),
CURLOPT_HTTPHEADER => [
"Authorization: Basic <encoded-value>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}{
"success": true,
"data": {
"_id": "ctc_xW8Ou6C03Csv8vatp",
"teamId": "tea_8QvkOiBfPdb2ZRhHi",
"emails": [
{
"value": "jane.smith@example.com"
}
],
"fields": {
"firstName": "Jane",
"lastName": "Smith",
"jobTitle": "Product Manager"
},
"ownerId": "usr_ahfFktBBHUIxbVG5P",
"source": "api",
"createdAt": "2025-10-28T00:40:37.917Z",
"createdBy": "usr_ahfFktBBHUIxbVG5P",
"unsubscribed": false,
"created": false,
"updated": true,
"companyLinked": false
}
}{
"success": true,
"data": {
"_id": "ctc_xW8Ou6C03Csv8vatp",
"teamId": "tea_8QvkOiBfPdb2ZRhHi",
"emails": [
{
"value": "jane.smith@example.com"
}
],
"linkedinUrl": "https://www.linkedin.com/in/janesmith",
"fields": {
"firstName": "Jane",
"lastName": "Smith",
"jobTitle": "Product Manager"
},
"ownerId": "usr_ahfFktBBHUIxbVG5P",
"source": "api",
"createdAt": "2025-10-28T00:40:37.917Z",
"createdBy": "usr_ahfFktBBHUIxbVG5P",
"unsubscribed": false,
"created": true,
"companyLinked": true,
"companyId": "cpn_gG7PsmZFpEAnpMCHO"
}
}{
"success": false,
"error": {
"code": "MISSING_IDENTIFIER",
"message": "At least one identifier is required: email, linkedinUrl or contactId"
}
}"The authentication you supplied is incorrect"{
"success": false,
"error": {
"code": "CONTACT_NOT_FOUND_BY_ID",
"message": "No contact found with the provided contactId"
}
}{
"success": false,
"error": {
"code": "CONTACT_IDENTIFIER_CONFLICT",
"message": "An identifier (email, linkedinUrl or salesnavUrl) already belongs to another contact"
}
}email, linkedinUrl, or linkedinUrlSalesNav already exists (upsert).
On update, the updateStrategy query parameter decides what the existing record keeps (see below). By default, sent values replace stored ones and empty values are ignored.
Upsert matching
At least one identifier is required:| Identifier | Description |
|---|---|
contactId | Existing contact ID — updates a specific contact directly, bypassing email/LinkedIn matching |
email | Primary email address — used as the main unique key |
linkedinUrl | LinkedIn profile URL — used as an alternative unique key |
linkedinUrlSalesNav | LinkedIn Sales Navigator profile URL — used as an alternative unique key |
email, linkedinUrl, or linkedinUrlSalesNav already exists, the endpoint updates it instead of creating a duplicate.
When contactId is provided, the contact is matched by its ID directly — email and linkedinUrl are not required and are not used for matching (they are stored as data if provided).
contactId can only be used to update an existing contact. It cannot be used to create a new contact — use email or linkedinUrl for creation.Update strategy
TheupdateStrategy query parameter decides what an existing contact keeps:
| Value | Stored value | Empty string sent |
|---|---|---|
overwriteIgnoreEmpty (default) | Replaced | Ignored |
overwrite | Replaced | Cleared |
fillEmptyOnly | Kept | Ignored |
null and absent keys never touch a field. Identifiers (email, linkedinUrl, linkedinUrlSalesNav) are never cleared. Under fillEmptyOnly, the company link, the owner and the status are kept when set and filled when missing. A new contact receives every value sent. Any other value answers 400 INVALID_UPDATE_STRATEGY.
curl -X POST "https://api.lemlist.com/api/contacts?updateStrategy=fillEmptyOnly" \
-u ":YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"email": "john@example.com", "jobTitle": "Founder", "phone": "+33600000000"}'
warnings carries a FIELDS_KEPT entry with the field names in params.fields. It appears under fillEmptyOnly on a stored value, and under the default when an empty string is ignored on a filled field, never for an identical value.
{
"success": true,
"data": { "_id": "ctc_2aB3cD4eF5gH6iJ7k", "jobTitle": "CEO", "phone": "+33600000000", "created": false, "updated": true, "companyLinked": false },
"warnings": [
{ "code": "FIELDS_KEPT", "message": "Fields kept: the record already holds a different value, left unchanged by the update strategy", "params": { "fields": ["jobTitle"] } }
]
}
Linking to a company
You can link the contact to a company that already exists in your lemlist CRM using one of the following fields (in order of priority):| Field | Description |
|---|---|
companyId | Direct company ID — takes priority over the others |
companyDomain | Company domain (e.g. lemlist.com) — used if companyId is not provided |
companyLinkedinUrl | Company LinkedIn URL — used as a last resort |
companyLinked: true and the companyId.
Owner assignment
You can assign an owner to the contact using thecontactOwner field. Accepted formats:
| Format | Example |
|---|---|
| User ID | usr_2aB3cD4eF5gH6iJ7k |
| Team member email | john@yourcompany.com |
OWNER_NOT_FOUND or INVALID_OWNER_FORMAT entry in warnings. On creation, the contact defaults to the API key owner; on update, the current owner is unchanged.updateStrategy decides the owner:
| Strategy | Current owner set | No current owner |
|---|---|---|
overwriteIgnoreEmpty (default) and overwrite | Replaced by contactOwner | Set to contactOwner |
fillEmptyOnly | Kept, reported as ownerId under warnings (FIELDS_KEPT) | Set to contactOwner |
contactOwner, the owner is never touched.Authorizations
Basic authentication header of the form Basic <encoded-value>, where <encoded-value> is the base64-encoded string username:password.
Query Parameters
What an existing record keeps. overwriteIgnoreEmpty (default): sent values replace stored ones, empty values are ignored. overwrite: an empty string clears the field. fillEmptyOnly: only empty fields are filled, identifiers, links, owner and status kept. A new record receives every value sent. Other values: 400 INVALID_UPDATE_STRATEGY.
overwrite, overwriteIgnoreEmpty, fillEmptyOnly Body
Existing contact ID. Updates a specific contact by ID, bypassing email/LinkedIn matching. Can only be used to update an existing contact, not to create a new one. When provided, email and linkedinUrl are not required. At least one of contactId, email, linkedinUrl, or linkedinUrlSalesNav is required.
Contact email address. Used as a unique key for upsert matching. At least one of contactId, email, linkedinUrl, or linkedinUrlSalesNav is required.
LinkedIn profile URL. Used as an alternative unique key for upsert matching. At least one of contactId, email, linkedinUrl, or linkedinUrlSalesNav is required.
LinkedIn Sales Navigator profile URL. Used as an alternative unique key for upsert matching.
Additional email addresses for the contact. Each must be a valid email address.
Contact first name.
Contact last name.
Contact phone number.
Contact job title. If a company is linked, this is saved as part of the job data.
Contact job description. If a company is linked, this is saved as part of the job data.
URL of the contact's profile picture.
Contact timezone.
Contact industry.
Contact languages.
Contact location.
Contact skills.
Contact summary or bio.
Contact tagline.
Owner of the contact. Can be a user ID (e.g. usr_...) or a team member's email address. If the provided value does not match a team member, the contact is still written with an OWNER_NOT_FOUND or INVALID_OWNER_FORMAT entry in warnings: the owner defaults to the API key owner on creation and is unchanged on update. On update it replaces the current owner, except under fillEmptyOnly where it is only set when the contact has none.
Origin of the contact record. Set on creation only and cannot be updated afterwards. Defaults to api.
ID of a company already existing in lemlist to link to this contact. Takes priority over companyDomain and companyLinkedinUrl.
Domain of a company already existing in lemlist to link to this contact (e.g. lemlist.com). Used if companyId is not provided.
LinkedIn URL of a company already existing in lemlist to link to this contact. Used if companyId and companyDomain are not provided.
Any additional key is treated as a custom field. Custom fields must be registered in the team's CRM field registry beforehand.
Response
Existing contact updated (upsert matched by email or LinkedIn URL)
Was this page helpful?