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

# ResolveLeadHandoffAPI records the sales desk marking a handoff done or not


> needed.

A handoff is accepted the moment Erdo files it: it is the desk's task, with no approval card, and the desk is emailed at once. Nothing automatic ever carries it out, so this is how it is closed — done (the desk dealt with the lead) or not needed. It answers FailedPrecondition when the lead's decision is not an open handoff, naming what it is instead.

There is deliberately no MCP tool for this, for the same reason as the hold, the close and the undo: it is the desk's word about a lead it is dealing with, not a model's. A scoped API key is refused.




## OpenAPI

````yaml /api/openapi.json post /v1/datasets/{datasetSlug}/leads/{leadRef}/next-actions/handoff
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-28'
servers:
  - url: https://api.erdo.ai
    description: Production
security:
  - bearerAuth: []
paths:
  /v1/datasets/{datasetSlug}/leads/{leadRef}/next-actions/handoff:
    post:
      summary: >
        ResolveLeadHandoffAPI records the sales desk marking a handoff done or
        not
      description: >
        needed.


        A handoff is accepted the moment Erdo files it: it is the desk's task,
        with no approval card, and the desk is emailed at once. Nothing
        automatic ever carries it out, so this is how it is closed — done (the
        desk dealt with the lead) or not needed. It answers FailedPrecondition
        when the lead's decision is not an open handoff, naming what it is
        instead.


        There is deliberately no MCP tool for this, for the same reason as the
        hold, the close and the undo: it is the desk's word about a lead it is
        dealing with, not a model's. A scoped API key is refused.
      operationId: POST:mcp.ResolveLeadHandoffAPI
      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:
                action_id:
                  title: >
                    ActionID names the exact handoff being answered, as the list
                    and history

                    endpoints report it. Omit to answer the lead's current
                    decision.
                  type: string
                decided_by_external_ref:
                  title: >
                    DecidedByExternalRef is YOUR OWN handle for the person
                    answering.

                    Stored verbatim, never interpreted. ATTRIBUTION, NEVER
                    AUTHENTICATION — see

                    decided_by_external_ref on the record endpoint. At most 200
                    characters.
                  type: string
                decision:
                  title: >
                    Decision is "done" (somebody at the desk dealt with the
                    lead: the handoff is

                    carried out, and Erdo stays off this lead until the lead
                    gets in touch) or

                    "not_needed" (the handoff did not need doing: Erdo leaves
                    the lead alone for

                    seven days unless they get in touch first). Required.
                  type: string
                note:
                  title: >
                    Note says what was done, or why it was not needed, in the
                    desk's words. Kept on

                    the decision. At most 2000 characters.
                  type: string
              type: object
              required:
                - decision
        required: true
      responses:
        '200':
          content:
            application/json:
              schema:
                properties:
                  accompanying_email:
                    $ref: '#/components/schemas/leadaction.AccompanyingEmailState'
                  action_input:
                    type: object
                  action_kind:
                    type: string
                  approval_request_id:
                    type: string
                  call_outcome:
                    $ref: '#/components/schemas/leadaction.CallOutcome'
                  canonical_lead_id:
                    type: string
                  created_by:
                    type: string
                  dataset_id:
                    type: string
                  decided_at:
                    format: date-time
                    type: string
                  decided_by:
                    type: string
                  decided_by_external_ref:
                    description: >
                      handles for the person who decided this action and the
                      person who took that decision back — opaque strings Erdo
                      stores verbatim and never interprets.


                      They exist because an application built on \`/v1\`
                      authenticates a whole team with ONE organization API key,
                      so DecidedBy — the Erdo account that key belongs to — is
                      the same person for every decision anybody there makes.
                      The caller is the only party that can say which of its
                      users acted, so it says, and it resolves these back to
                      names on its own side.


                      ATTRIBUTION, NEVER AUTHENTICATION. Anything holding the
                      organization's key can write any value here; Erdo does not
                      verify it, must never branch on it, authorize by it, or
                      resolve it against an Erdo identity. It is fine for
                      "Approved by Ana" on a card and must never be presented as
                      proof of who acted. Empty means the caller offered no
                      name, which is every decision made before this existed.
                    title: >
                      DecidedByExternalRef and UndoneByExternalRef are the
                      CALLING PRODUCT's own
                    type: string
                  due_at:
                    format: date-time
                    type: string
                  error:
                    type: string
                  evaluated_at:
                    format: date-time
                    type: string
                  executed_at:
                    format: date-time
                    type: string
                  execution_ref:
                    type: string
                  facts:
                    $ref: '#/components/schemas/leadaction.LeadFacts'
                  handoff_alert:
                    $ref: '#/components/schemas/leadaction.HandoffAlertState'
                  id:
                    type: string
                  lead_reference:
                    type: string
                  mode:
                    type: string
                  model:
                    type: string
                  playbook_revision:
                    format: int64
                    type: integer
                  policy_digest:
                    description: >
                      is what says whether the decision is stale — not the
                      revision number. The revision bumps on every save, so a
                      decision at revision 9 may have been decided under exactly
                      the rules revision 10 carries; comparing it with
                      LeadPlaybook.PolicyDigest answers the question the
                      revision only hints at. Empty on every decision written
                      before digests existed and on one a person recorded by
                      hand. See policy.go for the two halves it is made of.
                    title: >
                      PolicyDigest identifies the playbook this decision was
                      made under, and it
                    type: string
                  priority:
                    type: string
                  priority_reason:
                    type: string
                  rationale:
                    type: string
                  revisit_at:
                    format: date-time
                    type: string
                  rule:
                    type: string
                  source:
                    type: string
                  stage:
                    type: string
                  status:
                    type: string
                  undone_by_external_ref:
                    type: string
                type: object
          description: Success response
        default:
          $ref: '#/components/responses/APIError'
components:
  schemas:
    leadaction.AccompanyingEmailState:
      description: >
        out beside a gated decision: whether it went, when, in what words, and
        why it did not if it did not.


        It joins the row's own columns (the authoritative state) with the draft
        stored in action\_input, so a caller does not have to read two things
        and work out which one is true.
      properties:
        body_markdown:
          type: string
        error:
          title: |
            Error is why it has not gone, in the words a person reads.
          type: string
        execution_ref:
          title: |
            ExecutionRef is the mailer's id for the email that went.
          type: string
        sent_at:
          format: date-time
          type: string
        status:
          title: >
            Status is pending, sending, sent, failed or skipped. See the
            constants.
          type: string
        subject:
          type: string
        to:
          type: string
      title: >
        AccompanyingEmailState is what a surface needs to render the email that
        went
      type: object
    leadaction.CallOutcome:
      properties:
        duration_seconds:
          format: int64
          type: integer
        status:
          description: >
            answered, "no-answer", "busy", "failed" and so on otherwise, and a
            ringing or in-progress status while the call has not ended.
          title: |
            Status is the call record's status: "completed" for a call somebody
          type: string
      title: >
        CallOutcome is how one placed call ended, in the call record's own
        words.
      type: object
    leadaction.LeadFacts:
      description: |
        means the lead has not told us.
      properties:
        bedrooms:
          $ref: '#/components/schemas/leadaction.LeadFact'
        broker:
          $ref: '#/components/schemas/leadaction.LeadFact'
        budget:
          $ref: '#/components/schemas/leadaction.LeadFact'
        financing:
          $ref: '#/components/schemas/leadaction.LeadFact'
        language:
          $ref: '#/components/schemas/leadaction.LeadFact'
        purpose:
          $ref: '#/components/schemas/leadaction.LeadFact'
        timeline:
          $ref: '#/components/schemas/leadaction.LeadFact'
      title: |
        LeadFacts is what the evaluation understood about the lead. A nil field
      type: object
    leadaction.HandoffAlertState:
      description: >
        desk a handoff is waiting: whether it went, when, the message to open,
        and why it did not go if it did not.


        The alert is the only MESSAGE a handoff produces. Carrying a handoff out
        is a person marking it done, which records the literal string "accepted"
        and sends nothing — so a surface that wants to show what a handoff SENT
        has to read this and not the row's execution ref, and one that wants to
        know whether the desk has dealt with the lead reads the row's status. It
        is also why these facts are exposed at all: a sent alert is a real,
        openable message, and the timeline used to show a handoff as an
        unopenable line that mentioned no email whatever.


        It reads only the row's own columns, unlike AccompanyingEmailState,
        because the alert's words are not drafted onto the decision — they are
        composed at send time from the card by buildHandoffAlert. The message
        itself is read back from the email service by ExecutionRef, which is the
        only copy of it there is.
      properties:
        error:
          description: |
            a person reads.
          title: >
            Error is why it has not gone, or which recipients it missed, in the
            words
          type: string
        execution_ref:
          description: >
            it. One alert is one message per recipient and this is the first
            copy that sent; every copy carries the same words, so it opens what
            the desk read. Empty on an alert that has not gone, and on one sent
            before the id was recorded (migration 013) — in which case there is
            nothing to open and a surface must not pretend otherwise.
          title: >
            ExecutionRef is the email service's id for the alert, which is what
            opens
          type: string
        sent_at:
          format: date-time
          type: string
        status:
          description: |
            constants in handoff\_alert.go.
          title: |
            Status is pending, sending, sent or failed. See the HandoffAlert*
          type: string
      title: >
        HandoffAlertState is what a surface needs to render the alert that told
        the
      type: object
    leadaction.LeadFact:
      description: |
        read it from, so a person can check the reading against the row.
      properties:
        source_column:
          type: string
        value:
          type: string
      title: >
        LeadFact is one thing the evaluation read about the lead and the column
        it
      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_...).

````