> ## 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.

# MaintainLeadIdentityAPI is the REST surface for previewing and performing a


> dataset's permanent-lead-identity migration.

It mirrors eventpipeline.MaintainCanonicalLeadAdoption, which is an \`auth\` endpoint outside /v1 and therefore reachable only from a signed-in browser session: API keys and scoped tokens are confined to /v1, /mcp and the documented SDK routes (backend/middleware/external\_surface.go). The migration shipped with no operator surface at all — no UI calls it, the CLI speaks /v1, and an API key is refused — so the only way to run it was a fetch() pasted into DevTools. That is not a supported way to migrate a customer's leads.

The handler stays thin on purpose: the ordering that keeps adoption, backfill and the identity config consistent lives in the eventpipeline service, and this endpoint brings only its own RBAC (dataset-level edit permission check) and a slug-addressed path. The dataset is resolved via GetDatasetByIdentifier to obtain the dataset ID for the RBAC check, which accepts a slug or a UUID, so callers can use whichever they hold.

A scoped API key is refused, for the reason the lead merge refuses one and then some. erdo\_token\_\* credentials are per-end-user, and /v1/... admits them wholesale, while the only org check downstream is CanAccessOrgResource — plain equality, which an external user with OrganizationID set satisfies. Without this guard a single lead's own credential could migrate the dataset holding every other lead: rewrite the file, rewrite every pipeline that writes to it, and publish an identity config that cannot be withdrawn.




## OpenAPI

````yaml /api/openapi.json post /v1/datasets/{datasetSlug}/lead-identity/maintain
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}/lead-identity/maintain:
    post:
      summary: >
        MaintainLeadIdentityAPI is the REST surface for previewing and
        performing a
      description: >
        dataset's permanent-lead-identity migration.


        It mirrors eventpipeline.MaintainCanonicalLeadAdoption, which is an
        \`auth\` endpoint outside /v1 and therefore reachable only from a
        signed-in browser session: API keys and scoped tokens are confined to
        /v1, /mcp and the documented SDK routes
        (backend/middleware/external\_surface.go). The migration shipped with no
        operator surface at all — no UI calls it, the CLI speaks /v1, and an API
        key is refused — so the only way to run it was a fetch() pasted into
        DevTools. That is not a supported way to migrate a customer's leads.


        The handler stays thin on purpose: the ordering that keeps adoption,
        backfill and the identity config consistent lives in the eventpipeline
        service, and this endpoint brings only its own RBAC (dataset-level edit
        permission check) and a slug-addressed path. The dataset is resolved via
        GetDatasetByIdentifier to obtain the dataset ID for the RBAC check,
        which accepts a slug or a UUID, so callers can use whichever they hold.


        A scoped API key is refused, for the reason the lead merge refuses one
        and then some. erdo\_token\_\* credentials are per-end-user, and /v1/...
        admits them wholesale, while the only org check downstream is
        CanAccessOrgResource — plain equality, which an external user with
        OrganizationID set satisfies. Without this guard a single lead's own
        credential could migrate the dataset holding every other lead: rewrite
        the file, rewrite every pipeline that writes to it, and publish an
        identity config that cannot be withdrawn.
      operationId: POST:mcp.MaintainLeadIdentityAPI
      parameters:
        - allowEmptyValue: true
          explode: false
          in: path
          name: datasetSlug
          required: true
          schema:
            type: string
          style: simple
      requestBody:
        content:
          application/json:
            schema:
              properties:
                apply:
                  description: >
                    ready, what blocks it, and how many rows would be assigned
                    an identity. Apply true performs the migration — adopt every
                    pipeline writer, backfill the IDs and publish the identity
                    config, in one transaction.
                  title: >
                    Apply false previews and mutates nothing: it reports whether
                    the dataset is
                  type: boolean
                conversion_id_column:
                  title: >
                    ConversionIDColumn is optional and must differ from both
                    contact columns.
                  type: string
                default_calling_code:
                  description: |
                    written two ways is one lead. Empty means "1".
                  title: >
                    DefaultCallingCode promotes a phone written in national form
                    so one number
                  type: string
                email_column:
                  description: |
                    from the dataset's own compatibility report when omitted.
                  title: >
                    EmailColumn / PhoneColumn name the dataset's contact
                    columns. Defaulted
                  type: string
                operation_key:
                  description: >
                    same key settles an apply whose response was lost; a
                    different key against a migrated dataset is refused rather
                    than migrating a second time.
                  title: >
                    OperationKey is the migration receipt, required for apply.
                    Replaying the
                  type: string
                phone_column:
                  type: string
              type: object
      responses:
        '200':
          content:
            application/json:
              schema:
                properties:
                  automation_inventory:
                    $ref: >-
                      #/components/schemas/job.GetCanonicalLeadAutomationInventoryResponse
                  blockers:
                    items:
                      type: string
                    type: array
                  maintenance:
                    $ref: >-
                      #/components/schemas/dataset.MaintainCanonicalLeadIdentityResponse
                  pipeline_inventory:
                    $ref: >-
                      #/components/schemas/eventpipeline.CanonicalLeadPipelineInventory
                  pipelines_adopted:
                    items:
                      type: string
                    type: array
                  ready:
                    type: boolean
                type: object
          description: Success response
        default:
          $ref: '#/components/responses/APIError'
components:
  schemas:
    job.GetCanonicalLeadAutomationInventoryResponse:
      description: >
        identify the exact consumers the migration cannot make safe on its own;
        names, scripts, parameters and trigger payloads never cross the service
        boundary.
      properties:
        compatible:
          format: int64
          type: integer
        future_consumers:
          format: int64
          type: integer
        future_reasons:
          items:
            $ref: '#/components/schemas/job.CanonicalLeadAutomationInventoryReason'
          type: array
        incompatible:
          format: int64
          type: integer
        queued:
          format: int64
          type: integer
        reasons:
          items:
            $ref: '#/components/schemas/job.CanonicalLeadAutomationInventoryReason'
          type: array
        required_changes:
          items:
            $ref: '#/components/schemas/job.CanonicalLeadAutomationRequiredChange'
          type: array
        running:
          format: int64
          type: integer
        unverifiable:
          format: int64
          type: integer
      title: >
        GetCanonicalLeadAutomationInventoryResponse contains only counts. Job
        IDs
      type: object
    dataset.MaintainCanonicalLeadIdentityResponse:
      properties:
        assigned_count:
          format: int64
          type: integer
        published_storage_key:
          type: string
        result_digest:
          type: string
        row_count:
          format: int64
          type: integer
        source_digest:
          type: string
        source_storage_item_id:
          type: string
        source_storage_key:
          type: string
        state:
          type: string
      type: object
    eventpipeline.CanonicalLeadPipelineInventory:
      properties:
        adoptable:
          description: |
            given the producer contract yet. Apply adds it; nothing else does.
          items:
            type: string
          title: >
            Adoptable names the pipelines whose only problem is that they have
            not been
          type: array
        compatible:
          format: int64
          type: integer
        in_flight:
          format: int64
          type: integer
        incompatible:
          format: int64
          type: integer
        reasons:
          items:
            $ref: >-
              #/components/schemas/eventpipeline.CanonicalLeadPipelineInventoryReason
          type: array
        unverifiable:
          format: int64
          type: integer
      type: object
    job.CanonicalLeadAutomationInventoryReason:
      properties:
        code:
          type: string
        count:
          format: int64
          type: integer
      type: object
    job.CanonicalLeadAutomationRequiredChange:
      properties:
        job_id:
          type: string
        reason_code:
          type: string
        status:
          type: string
      type: object
    eventpipeline.CanonicalLeadPipelineInventoryReason:
      properties:
        code:
          type: string
        count:
          format: int64
          type: integer
      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_...).

````