curl --request GET \
--url https://openapi.enginy.ai/v1/contacts/duplicates \
--header 'x-api-key: <api-key>'import requests
url = "https://openapi.enginy.ai/v1/contacts/duplicates"
headers = {"x-api-key": "<api-key>"}
response = requests.get(url, headers=headers)
print(response.text)const options = {method: 'GET', headers: {'x-api-key': '<api-key>'}};
fetch('https://openapi.enginy.ai/v1/contacts/duplicates', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://openapi.enginy.ai/v1/contacts/duplicates",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => [
"x-api-key: <api-key>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://openapi.enginy.ai/v1/contacts/duplicates"
req, _ := http.NewRequest("GET", url, nil)
req.Header.Add("x-api-key", "<api-key>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.get("https://openapi.enginy.ai/v1/contacts/duplicates")
.header("x-api-key", "<api-key>")
.asString();require 'uri'
require 'net/http'
url = URI("https://openapi.enginy.ai/v1/contacts/duplicates")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Get.new(url)
request["x-api-key"] = '<api-key>'
response = http.request(request)
puts response.read_body{
"status": "success",
"message": "<string>",
"data": {
"groups": [
{
"values": {},
"contactIds": [
123
],
"size": 123
}
],
"nextCursor": "<string>"
}
}Find duplicate contacts
Find groups of duplicate contacts: live contacts of your workspace that share the same normalized value of ALL the fields you pick (1 to 5 of fullName, professionalEmail, linkedinUrl, contactCRMId, leadCRMId; AND: fields=fullName,professionalEmail groups only contacts with the same full name AND the same email). A contact with an empty value for any selected field never appears in a group, and fullName needs both a first and a last name. fullName alone also groups different people who share a common name: pair it with another field or read the details before deciding. Returns { groups: [{ values, contactIds, size }], nextCursor } where values maps each field to the shared normalized value, contactIds lists at most the 100 oldest ids of the group and size is the full count (when a group is large, narrow it by adding more fields), 100 groups per page at most, ordered by the lowest contact id of each group. Only ids are returned: read the contacts with get_contacts or get_a_single_contact to decide which copy to keep. Pass listId to look only for groups that touch a contact list. Contacts you cannot access, merged, blocklisted, opted-out or deleted contacts never appear. Contacts that are in a campaign do appear. A search that takes too long answers 408: the workspace is too large to scan right now, so retry later (listId only filters the groups, it does not make the scan cheaper). To remove duplicates, show the user which contacts will go and get their confirmation first, then call delete_a_contact once per contact: deleting removes the contact from every contact list and from its campaigns (its conversations are marked deleted and its running workflows are cancelled), and it cannot be undone from the product. If two duplicates carry different CRM ids, the duplicate lives in the CRM itself: tell the user to merge the records there, or a later CRM import can recreate the contact.
Required scope:
CONTACTS_READRate limit: 100 requests per minute
curl --request GET \
--url https://openapi.enginy.ai/v1/contacts/duplicates \
--header 'x-api-key: <api-key>'import requests
url = "https://openapi.enginy.ai/v1/contacts/duplicates"
headers = {"x-api-key": "<api-key>"}
response = requests.get(url, headers=headers)
print(response.text)const options = {method: 'GET', headers: {'x-api-key': '<api-key>'}};
fetch('https://openapi.enginy.ai/v1/contacts/duplicates', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://openapi.enginy.ai/v1/contacts/duplicates",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => [
"x-api-key: <api-key>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://openapi.enginy.ai/v1/contacts/duplicates"
req, _ := http.NewRequest("GET", url, nil)
req.Header.Add("x-api-key", "<api-key>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.get("https://openapi.enginy.ai/v1/contacts/duplicates")
.header("x-api-key", "<api-key>")
.asString();require 'uri'
require 'net/http'
url = URI("https://openapi.enginy.ai/v1/contacts/duplicates")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Get.new(url)
request["x-api-key"] = '<api-key>'
response = http.request(request)
puts response.read_body{
"status": "success",
"message": "<string>",
"data": {
"groups": [
{
"values": {},
"contactIds": [
123
],
"size": 123
}
],
"nextCursor": "<string>"
}
}Authorizations
Query Parameters
The values two contacts must ALL share to be duplicates (AND): 1 to 5 different fields of fullName, professionalEmail, linkedinUrl, contactCRMId, leadCRMId. Comma-separated (fields=fullName,professionalEmail) or repeated (fields=fullName&fields=professionalEmail); through MCP, an array. A contact whose value is empty for any selected field never matches. fullName needs both a first and a last name and ignores case, punctuation and extra spaces, professionalEmail ignores case and surrounding spaces, linkedinUrl compares the normalized profile URL, contactCRMId and leadCRMId compare CRM ids (a Salesforce 15-character id equals its 18-character form).
1 - 5 elementsfullName, professionalEmail, linkedinUrl, contactCRMId, leadCRMId ["fullName", "professionalEmail"]
Only report groups with at least one contact in this contact list. Each group still names every copy in the workspace, including copies outside the list.
The nextCursor of the previous page. Omit for the first page.
^\d+$Groups per page, at most 100.
1 <= x <= 100