curl --request POST \
--url https://api.lemlist.com/api/companies \
--header 'Authorization: Basic <encoded-value>' \
--header 'Content-Type: application/json' \
--data '
{
"name": "Acme Corp",
"domain": "acme.com",
"linkedinUrl": "https://linkedin.com/company/acme",
"industry": "Technology",
"location": "San Francisco, CA"
}
'import requests
url = "https://api.lemlist.com/api/companies"
payload = {
"name": "Acme Corp",
"domain": "acme.com",
"linkedinUrl": "https://linkedin.com/company/acme",
"industry": "Technology",
"location": "San Francisco, CA"
}
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({
name: 'Acme Corp',
domain: 'acme.com',
linkedinUrl: 'https://linkedin.com/company/acme',
industry: 'Technology',
location: 'San Francisco, CA'
})
};
fetch('https://api.lemlist.com/api/companies', 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/companies")
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 \"name\": \"Acme Corp\",\n \"domain\": \"acme.com\",\n \"linkedinUrl\": \"https://linkedin.com/company/acme\",\n \"industry\": \"Technology\",\n \"location\": \"San Francisco, CA\"\n}"
response = http.request(request)
puts response.read_body<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.lemlist.com/api/companies",
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([
'name' => 'Acme Corp',
'domain' => 'acme.com',
'linkedinUrl' => 'https://linkedin.com/company/acme',
'industry' => 'Technology',
'location' => 'San Francisco, CA'
]),
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;
}Add and update company
Creates a new company or updates an existing one based on domain or LinkedIn URL.
curl --request POST \
--url https://api.lemlist.com/api/companies \
--header 'Authorization: Basic <encoded-value>' \
--header 'Content-Type: application/json' \
--data '
{
"name": "Acme Corp",
"domain": "acme.com",
"linkedinUrl": "https://linkedin.com/company/acme",
"industry": "Technology",
"location": "San Francisco, CA"
}
'import requests
url = "https://api.lemlist.com/api/companies"
payload = {
"name": "Acme Corp",
"domain": "acme.com",
"linkedinUrl": "https://linkedin.com/company/acme",
"industry": "Technology",
"location": "San Francisco, CA"
}
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({
name: 'Acme Corp',
domain: 'acme.com',
linkedinUrl: 'https://linkedin.com/company/acme',
industry: 'Technology',
location: 'San Francisco, CA'
})
};
fetch('https://api.lemlist.com/api/companies', 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/companies")
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 \"name\": \"Acme Corp\",\n \"domain\": \"acme.com\",\n \"linkedinUrl\": \"https://linkedin.com/company/acme\",\n \"industry\": \"Technology\",\n \"location\": \"San Francisco, CA\"\n}"
response = http.request(request)
puts response.read_body<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.lemlist.com/api/companies",
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([
'name' => 'Acme Corp',
'domain' => 'acme.com',
'linkedinUrl' => 'https://linkedin.com/company/acme',
'industry' => 'Technology',
'location' => 'San Francisco, CA'
]),
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;
}domain, 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
The endpoint matches existing companies by:| Identifier | Description |
|---|---|
companyId | Existing company ID — updates a specific company directly, bypassing domain/LinkedIn matching |
domain | Company website domain (e.g. lemlist.com) — primary unique key |
linkedinUrl | LinkedIn company page URL — alternative unique key |
linkedinUrlSalesNav | LinkedIn Sales Navigator company URL — alternative unique key |
domain, linkedinUrl, or linkedinUrlSalesNav already exists, the endpoint updates it instead of creating a duplicate.
Creating a company requires the “Create companies” permission on the API key’s user, otherwise the request fails with 403 COMPANY_CREATE_NOT_ALLOWED and nothing is written. Updating an existing company does not require it. The error names that user (The user jane@acme.com does not have the permission to create companies); when the API key’s user is no longer in the team, it fails with 403 API_USER_NOT_IN_TEAM instead.
Without companyId, name is required together with at least one identifier: domain, linkedinUrl or linkedinUrlSalesNav. A company known only by its LinkedIn page can be created with name and linkedinUrl; a name alone is refused with 400 MISSING_REQUIRED_FIELD.
A domain that designates no single company is not stored: a personal email provider (gmail.com) or a platform host (facebook.com, linkedin.com, a bare wordpress.com). The company is still written, without it, and warnings carries a COMPANY_DOMAIN_IGNORED entry naming the domain in params.companyDomain. A site hosted on a site builder keeps its own address (acme.wordpress.com).
When companyId is provided, the company is matched by its ID directly. name and the identifiers are not required and are not used for matching (they are stored as data if provided).
companyId can only be used to update an existing company. It cannot be used to create a new company: use name and an identifier for creation.Update strategy
TheupdateStrategy query parameter decides what an existing company 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 (name, domain, linkedinUrl, linkedinUrlSalesNav) are never cleared. Under fillEmptyOnly, the owner and the status are kept when set and filled when missing. A new company receives every value sent. Any other value answers 400 INVALID_UPDATE_STRATEGY.
curl -X POST "https://api.lemlist.com/api/companies?updateStrategy=fillEmptyOnly" \
-u ":YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "Acme Inc", "domain": "acme.com", "industry": "Retail"}'
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": "cpn_2aB3cD4eF5gH6iJ7k", "industry": "Software", "location": "Lyon", "created": false, "updated": true },
"warnings": [
{ "code": "FIELDS_KEPT", "message": "Fields kept: the record already holds a different value, left unchanged by the update strategy", "params": { "fields": ["industry"] } }
]
}
Linking to a CRM record
SendcrmId with the id of a record in your connected CRM to link the company to that record. lemlist links it without searching the CRM, then runs a first sync that follows your field mapping (the CRM wins conflicts by default).
crmId is in closed beta. Unless the CRM ID remap beta is enabled for your team, it is refused like any unknown field, with 400 INVALID_FIELD. It also requires an admin API key.| CRM | Accepted id |
|---|---|
| HubSpot, Pipedrive, Zoho | Numeric record id. Send a Zoho id as a string: its 19 digits are too long for a JSON number. |
| Attio | Record id (UUID) |
| Salesforce | Account (001…) id, 15 or 18 characters. A 15-character id is stored as 18. |
| Situation | Result |
|---|---|
| New company, the id is linked to no lemlist company | The company is created already linked to that record (201). |
Existing company, named by companyId, or found by its domain and name, its LinkedIn URL or its Sales Navigator URL | Its link moves to that record (200), and each of its contacts is associated with the new CRM company, following your association settings. Updates queued for the previous record are cancelled. |
| Existing company found by its name alone, linked to another CRM record | Refused: 409 COMPANY_LINKED_TO_OTHER_CRM_RECORD. A name does not identify a company well enough to move its link; send its companyId with crmId. |
The id is already linked to a lemlist company, and at least one of the body’s domain, linkedinUrl or linkedinUrlSalesNav is that company’s | The crmId finds the company and that identifier confirms it: the body updates it (200), name included, and its link stays as it is. The body’s other identifiers replace the company’s, unless they already belong to another company: 409 COMPANY_IDENTIFIER_CONFLICT. Sending the same payload twice is safe. |
| The id is already linked to a lemlist company, and none of the body’s identifiers is that company’s | Refused, nothing written: 409 CRM_ID_ALREADY_LINKED, error.conflictingRecord names that company. |
name is another company’s: send companyId (or the LinkedIn URL) when your companies share a domain.crmSync block of the response shows the link the company ends up with.
curl -X POST "https://api.lemlist.com/api/companies" \
-u ":YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "Acme Inc", "domain": "acme.com", "crmId": "123456789"}'
| Status | Code | When |
|---|---|---|
400 | INVALID_CRM_ID | The id does not have the format of your CRM, or points to another kind of record (a Salesforce Contact id for a company). |
400 | NO_CRM_CONNECTED | Your team has no CRM connected. |
403 | CRM_ID_UPDATE_NOT_ALLOWED | The API key does not belong to a team admin. |
409 | CRM_ID_ALREADY_LINKED | Another lemlist company holds the id (see above); error.conflictingRecord names it. |
409 | COMPANY_LINKED_TO_OTHER_CRM_RECORD | The company was found by its name alone and is linked to another CRM record. |
422 | CRM_RECORD_NOT_FOUND | No record with this id exists in the CRM, or it was deleted. |
422 | CRM_RECORD_MERGED | The record was merged into another one; error.survivorId is the id to send. |
503 | CRM_RECORD_UNREACHABLE | The CRM could not be reached to check the record. Retry. |
Owner assignment
You can assign an owner to the company using thecompanyOwner 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 company 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 companyOwner | Set to companyOwner |
fillEmptyOnly | Kept, reported as ownerId under warnings (FIELDS_KEPT) | Set to companyOwner |
companyOwner, 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 company ID. Updates a specific company by ID, bypassing domain/LinkedIn matching. Can only be used to update an existing company, not to create a new one. When provided, name and the identifiers become optional.
Company name. Required unless companyId is provided.
Company website domain (e.g. lemlist.com). Used as a unique key for upsert matching. Without companyId, at least one identifier is required among domain, linkedinUrl and linkedinUrlSalesNav. A personal email provider or a platform host (gmail.com, facebook.com, linkedin.com, a bare wordpress.com) is not stored: the company is written without it and warnings reports COMPANY_DOMAIN_IGNORED.
LinkedIn company page URL. Used as an alternative unique key for upsert matching, and accepted as the sole identifier of a new company.
LinkedIn Sales Navigator company URL. Used as an alternative unique key for upsert matching, and accepted as the sole identifier of a new company.
Owner of the company. 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 company 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 company has none.
Industry sector of the company.
Geographic location of the company.
Company size.
Company specialties.
Company tagline.
Company type.
Company description.
Company founding date.
Company headquarters location.
URL of the company logo or picture.
Origin of the company record. Set on creation only and cannot be updated afterwards. Defaults to api.
Closed beta: refused as an unknown field (400 INVALID_FIELD) unless the CRM ID remap beta is enabled for your team. Id of the record of the connected CRM to link the company to: a HubSpot, Pipedrive or Zoho numeric id (send a Zoho id as a string: its 19 digits are too long for a JSON number), an Attio record id, or a Salesforce Account (001…) id of 15 or 18 characters. Requires an admin API key. A new company is created already linked; an existing one has its link moved to this record, and its contacts’ associations follow. An id another company holds updates that company when the body carries its domain, LinkedIn URL or Sales Navigator URL, and is refused otherwise. See the endpoint page for the refusals.
"123456789"
Any additional key is treated as a custom field. Custom fields must be registered in the team's CRM field registry beforehand.
Response
Existing company updated (upsert matched by domain or LinkedIn URL)
Was this page helpful?