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

# MergeLeadsAPI makes two lead identities one lead: the absorbed identity points


> at the survivor, its contact evidence moves across, and the two dataset rows become one row.

There is deliberately no MCP tool for this. It is not reversible in practice, and the captures it settles are the ones where the evidence contradicts itself — a person decides, holding both leads' evidence side by side.

A scoped API key is refused: those are per-end-user credentials, and one lead's holder must never be able to fold another lead into it.




## OpenAPI

````yaml /api/openapi.json post /v1/datasets/{datasetSlug}/leads/{leadRef}/merge
openapi: 3.0.0
info:
  title: Erdo API
  description: >-
    Erdo's REST API: query and write datasets, run agents, manage threads,
    integrations, pages, evals, workstreams, experiments, and bounded outreach.
    Authenticate with a Bearer API key (erdo_api_...) or scoped token
    (erdo_token_...).
  version: '2026-09-11'
servers:
  - url: https://api.erdo.ai
    description: Production
security:
  - bearerAuth: []
paths:
  /v1/datasets/{datasetSlug}/leads/{leadRef}/merge:
    post:
      summary: >
        MergeLeadsAPI makes two lead identities one lead: the absorbed identity
        points
      description: >
        at the survivor, its contact evidence moves across, and the two dataset
        rows become one row.


        There is deliberately no MCP tool for this. It is not reversible in
        practice, and the captures it settles are the ones where the evidence
        contradicts itself — a person decides, holding both leads' evidence side
        by side.


        A scoped API key is refused: those are per-end-user credentials, and one
        lead's holder must never be able to fold another lead into it.
      operationId: POST:mcp.MergeLeadsAPI
      parameters:
        - allowEmptyValue: true
          explode: false
          in: path
          name: datasetSlug
          required: true
          schema:
            type: string
          style: simple
        - allowEmptyValue: true
          explode: false
          in: path
          name: leadRef
          required: true
          schema:
            type: string
          style: simple
      requestBody:
        content:
          application/json:
            schema:
              properties:
                absorb_lead_id:
                  title: >
                    AbsorbLeadID names the lead that stops being its own lead,
                    as a

                    canonical_lead_id UUID or a 22-character lead reference.
                    Required.
                  type: string
                overwrite_columns:
                  items:
                    type: string
                  title: >
                    OverwriteColumns names the columns where the absorbed lead's
                    value wins over one

                    the survivor already holds; everything else only fills a
                    blank.
                  type: array
              type: object
              required:
                - absorb_lead_id
        required: true
      responses:
        '200':
          content:
            application/json:
              schema:
                properties:
                  absorbed_lead_id:
                    description: >
                      existing so the capture receipts referencing it stay
                      accurate.
                    title: >
                      AbsorbedLeadID is the identity that now points at the
                      survivor. It keeps
                    type: string
                  aliases_moved:
                    format: int64
                    type: integer
                  lead:
                    $ref: '#/components/schemas/dataset.CanonicalLead'
                  rows_combined:
                    format: int64
                    type: integer
                  state:
                    description: >
                      the two were already one lead — which is also what a retry
                      of an uncertain merge answers.
                    title: >
                      State is "merged" for the call that performed it and
                      "already_merged" when
                    type: string
                type: object
          description: Success response
        default:
          $ref: '#/components/responses/APIError'
components:
  schemas:
    dataset.CanonicalLead:
      properties:
        absorbed_lead_ids:
          items:
            type: string
          title: >
            AbsorbedLeadIDs are the identities merged INTO this one, most recent
            first.
          type: array
        aliases:
          items:
            $ref: '#/components/schemas/dataset.CanonicalLeadAlias'
          type: array
        canonical_lead_id:
          title: |
            CanonicalLeadID is the permanent identity, as a UUID.
          type: string
        captures:
          items:
            $ref: '#/components/schemas/dataset.CanonicalLeadCapture'
          type: array
        created_at:
          format: date-time
          type: string
        dataset_id:
          type: string
        dataset_slug:
          type: string
        merged_at:
          format: date-time
          type: string
        merged_by:
          type: string
        merged_into:
          description: >
            lead it became. An absorbed identity keeps existing: the capture
            receipts that reference it stay accurate about what happened at the
            time.
          title: >
            MergedIntoReference is set on an identity that was absorbed, and
            names the
          type: string
        merged_into_reference:
          type: string
        reference:
          description: |
            Either form is accepted wherever a lead is addressed.
          title: >
            Reference is the same identity in 22 characters, for a link or an
            SMS.
          type: string
        requested_lead_id:
          description: >
            CanonicalLeadID when the caller held an id that has since been
            absorbed — which keeps working on purpose, because ids travel in
            capture responses and outbound links.
          title: >
            RequestedLeadID is the identity the caller asked for. It differs
            from
          type: string
        row:
          additionalProperties:
            type: string
          description: >
            which is what an absorbed identity looks like after its values were
            combined into its survivor.
          title: >
            Row is the lead's dataset row. Absent when the identity holds no row
            —
          type: object
      title: |
        CanonicalLead is everything an operator needs to judge one lead.
      type: object
    dataset.CanonicalLeadAlias:
      properties:
        created_at:
          format: date-time
          type: string
        provenance:
          type: string
        type:
          title: |
            Type is email, phone, legacy_lid, legacy_id or conversion_id.
          type: string
        value:
          title: >
            Value is the normalized form the resolver matches on, not what was
            typed.
          type: string
      title: |
        CanonicalLeadAlias is one piece of contact evidence bound to a lead.
      type: object
    dataset.CanonicalLeadCapture:
      properties:
        candidate_lead_ids:
          description: |
            Either of them is what a merge is addressed with.
          items:
            type: string
          title: >
            CandidateLeadIDs are the leads a refused capture could not choose
            between.
          type: array
        conversion_id:
          type: string
        conversion_status:
          type: string
        created_at:
          format: date-time
          type: string
        id:
          type: string
        operation_key:
          type: string
        producer:
          type: string
        resolution:
          title: >
            Resolution is created, matched, enriched, ambiguous, conflict or
            unresolved.
          type: string
        rows_affected:
          format: int64
          type: integer
      title: |
        CanonicalLeadCapture is one recorded capture against a lead.
      type: object
  responses:
    APIError:
      content:
        application/json:
          schema:
            externalDocs:
              url: https://pkg.go.dev/encore.dev/beta/errs#Error
            properties:
              code:
                description: Error code
                example: not_found
                externalDocs:
                  url: https://pkg.go.dev/encore.dev/beta/errs#ErrCode
                type: string
              details:
                description: Error details
                type: object
              message:
                description: Error message
                type: string
            title: APIError
            type: object
      description: Error response
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: An Erdo API key (erdo_api_...) or scoped token (erdo_token_...).

````