> ## Documentation Index
> Fetch the complete documentation index at: https://docs.enginy.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# 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_READ`
    >
    > **Rate limit:** 100 requests per minute



## OpenAPI

````yaml https://openapi.enginy.ai/definitions.json get /v1/contacts/duplicates
openapi: 3.1.0
info:
  title: Enginy API
  version: 1.0.0
  description: Public API for Enginy platform
servers:
  - url: https://openapi.enginy.ai
    description: Configured server
security: []
tags:
  - name: AI Variables
    description: >-
      Manage AI variables and discover the entity fields you can reference in
      prompts and entity requests.
  - name: Inbox
    description: >-
      Inbox endpoints for listing contact threads, reading thread messages,
      managing tags, and sending manual replies.
  - name: Workflows
    description: >-
      Build, validate, publish, run, and inspect advanced (graph) workflows,
      plus the block/condition catalog needed to author a workflow plan.
paths:
  /v1/contacts/duplicates:
    get:
      tags:
        - Contacts
      summary: Find duplicate contacts
      description: >-
        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_READ`
            >
            > **Rate limit:** 100 requests per minute
      parameters:
        - schema:
            type: array
            items:
              type: string
              enum:
                - fullName
                - professionalEmail
                - linkedinUrl
                - contactCRMId
                - leadCRMId
            minItems: 1
            maxItems: 5
            description: >-
              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).
            example:
              - fullName
              - professionalEmail
          required: true
          description: >-
            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).
          name: fields
          in: query
        - schema:
            type: integer
            exclusiveMinimum: 0
            description: >-
              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.
          required: false
          description: >-
            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.
          name: listId
          in: query
        - schema:
            type: string
            pattern: ^\d+$
            description: The `nextCursor` of the previous page. Omit for the first page.
          required: false
          description: The `nextCursor` of the previous page. Omit for the first page.
          name: cursor
          in: query
        - schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 100
            description: Groups per page, at most 100.
          required: false
          description: Groups per page, at most 100.
          name: pageSize
          in: query
      responses:
        '200':
          description: A page of duplicate groups
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    enum:
                      - success
                  message:
                    type: string
                  data:
                    type: object
                    properties:
                      groups:
                        type: array
                        items:
                          type: object
                          properties:
                            values:
                              type: object
                              additionalProperties:
                                type: string
                              description: >-
                                The normalized value the contacts share for each
                                requested field, e.g. `{ "fullName": "ada
                                lovelace", "professionalEmail": "ada@acme.com"
                                }`.
                            contactIds:
                              type: array
                              items:
                                type: number
                              description: >-
                                Live contacts sharing the value, lowest id
                                first: the 100 oldest at most. `size` is the
                                full count.
                            size:
                              type: integer
                              description: >-
                                How many contacts share the value, at least 2;
                                can exceed `contactIds`.
                          required:
                            - values
                            - contactIds
                            - size
                      nextCursor:
                        type:
                          - string
                          - 'null'
                        description: >-
                          Pass as `cursor` for the next page; null on the last
                          page.
                    required:
                      - groups
                      - nextCursor
                required:
                  - status
                  - message
                  - data
        '400':
          description: >-
            Bad request - no fields, an unknown or repeated field, more than 5
            fields, or an invalid cursor or page size
        '404':
          description: List not found
        '408':
          description: >-
            The search took too long; the workspace is too large to scan right
            now, retry later
      security:
        - apiKey: []
components:
  securitySchemes:
    apiKey:
      type: apiKey
      in: header
      name: x-api-key

````

This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.