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

# PutLeadPlaybookHandoffAlertAPI changes only who is emailed about a handoff


> and which buttons that email carries. Organization admins only. Unlike PUT /v1/lead-playbook it takes no body, does not re-read the playbook and mints no revision: these are delivery details, not rules a decision was made under. A scoped API key is refused, as on the full save.




## OpenAPI

````yaml /api/openapi.json put /v1/lead-playbook/handoff-alert
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-10-22'
servers:
  - url: https://api.erdo.ai
    description: Production
security:
  - bearerAuth: []
paths:
  /v1/lead-playbook/handoff-alert:
    put:
      summary: >
        PutLeadPlaybookHandoffAlertAPI changes only who is emailed about a
        handoff
      description: >
        and which buttons that email carries. Organization admins only. Unlike
        PUT /v1/lead-playbook it takes no body, does not re-read the playbook
        and mints no revision: these are delivery details, not rules a decision
        was made under. A scoped API key is refused, as on the full save.
      operationId: PUT:mcp.PutLeadPlaybookHandoffAlertAPI
      requestBody:
        content:
          application/json:
            schema:
              properties:
                handoff_alert_links:
                  items:
                    $ref: '#/components/schemas/linktemplate.Link'
                  title: >
                    HandoffAlertLinks replaces the buttons that email carries;
                    same rules as on PUT

                    /v1/lead-playbook. Keeps its saved value when omitted; an
                    empty list removes

                    them.
                  type: array
                handoff_alert_recipients:
                  items:
                    type: string
                  title: >
                    HandoffAlertRecipients replaces the addresses emailed when
                    the engine files

                    a handoff; same rules as on PUT /v1/lead-playbook. Keeps its
                    saved value when

                    omitted; an empty list stops alerting anybody.
                  type: array
              type: object
      responses:
        '200':
          content:
            application/json:
              schema:
                properties:
                  agent:
                    $ref: '#/components/schemas/leadaction.PlaybookAgent'
                  agent_id:
                    description: |
                      the same organization. Null when none is linked.
                    title: >
                      AgentID is the concierge this playbook writes as: a voice
                      managed agent in
                    type: string
                  body:
                    type: string
                  daily_contact_cap:
                    description: >
                      It counts every contact the engine makes or puts in front
                      of a person: emails, texts, WhatsApp messages and calls,
                      each one place (an email sent beside a gated text or call
                      is a second). Handoffs, waits and closes never count, and
                      nor does a concierge answering a lead inside a WhatsApp,
                      SMS, chat or phone conversation. 200 when never set.
                    format: int64
                    title: >
                      DailyContactCap is the engine's limit on contacts per
                      rolling 24 hours.
                    type: integer
                  enabled:
                    type: boolean
                  enabled_at:
                    description: >
                      before it is left alone until somebody starts the engine
                      on it; absent for a playbook that was never on.
                    format: date-time
                    title: >
                      EnabledAt is when the engine was last switched on. A lead
                      captured
                    type: string
                  exists:
                    description: |
                      then carries the default template and ReadBack is empty.
                    title: >
                      Exists is false when the organization has never saved a
                      playbook; Body
                    type: boolean
                  handoff_alert_links:
                    description: >
                      caller declared them: URL templates filled from the lead
                      (backend/utils/linktemplate). Empty means the alert
                      carries no button. Erdo has no page of its own for a
                      lead's follow-up, so it never invents a destination; the
                      application the organization works its leads in declares
                      where "open this lead" goes. Like the recipients, it is a
                      delivery detail read live at send time and never frozen on
                      a revision. Never nil.
                    items:
                      $ref: '#/components/schemas/linktemplate.Link'
                    title: >
                      HandoffAlertLinks are the buttons the handoff alert
                      carries, as the
                    type: array
                  handoff_alert_recipients:
                    description: >
                      handoff — a decision it cannot carry out, which needs a
                      person at the sales desk. Empty means the alert is off:
                      there is no default audience and no fallback to the
                      organization's members, so a development that has not
                      filled this in is told nothing and no email is sent.


                      It is deliberately not on a playbook REVISION, unlike
                      SignatureName: it is who gets told, not what a decision
                      says, so it is read live at send time and changing it
                      takes effect at once rather than at the next save. See
                      migration 012.
                    items:
                      type: string
                    title: >
                      HandoffAlertRecipients are the addresses emailed when the
                      engine files a
                    type: array
                  policy_digest:
                    description: >
                      a decision that is genuinely out of date from one whose
                      playbook has merely been re-saved. A decision whose
                      policy\_digest equals this one was decided under the
                      playbook as it stands; one that differs is due to be
                      decided again, and the sweep will. See policy.go.
                    title: >
                      PolicyDigest identifies these rules and these words, so a
                      surface can tell
                    type: string
                  read_back:
                    $ref: '#/components/schemas/leadaction.ReadBack'
                  revision:
                    format: int64
                    type: integer
                  signature_name:
                    description: >
                      signed and sent as, and what a call brief opens with.
                      Empty falls back to the linked concierge's display name,
                      which is what every playbook written before this setting
                      existed does. See writeIdentitySection.
                    title: >
                      SignatureName is the name a LEAD sees: what an email from
                      the engine is
                    type: string
                  updated_at:
                    format: date-time
                    type: string
                  updated_by:
                    type: string
                type: object
          description: Success response
        default:
          $ref: '#/components/responses/APIError'
components:
  schemas:
    linktemplate.Link:
      properties:
        label:
          title: |
            Label is the button's caption, e.g. "Open lead".
          type: string
        requires:
          description: >
            for a link that uses none of them in its URL — a "Call" button needs
            a phone number even though the URL addresses the lead by its id.
          items:
            type: string
          title: >
            Requires names fields that must be non-empty for the link to be
            shown,
          type: array
        url:
          description: |
            {{a|b}}) may appear only in the path, query or fragment.
          title: |
            URL is an absolute https URL template. Placeholders ({{field}} or
          type: string
      title: |
        Link is one declared button.
      type: object
    leadaction.PlaybookAgent:
      properties:
        id:
          type: string
        name:
          type: string
        phone_number:
          description: |
            it has none.
          title: >
            PhoneNumber is the number the concierge calls and texts from, empty
            when
          type: string
        slug:
          type: string
      title: |
        PlaybookAgent is the concierge a playbook names, as the API returns it.
      type: object
    leadaction.ReadBack:
      description: >
        Only Actions\[].Mode, WorkingHours, EmailHours and TextHours are
        enforced; the stages and the priority ladder are a picture of the text
        drawn for the person who wrote it, and every evaluation still reads the
        whole text.
      properties:
        accompanying_email:
          allOf:
            - $ref: '#/components/schemas/leadaction.ReadBackAccompaniment'
          description: >
            person — a text or a call — may also send the lead an email at once.
            Like the cadence it is always filled in, so a read-back stored
            before it existed answers with the default rather than with an empty
            string.
          title: |
            AccompanyingEmail is whether a decision this playbook gates behind a
        actions:
          items:
            $ref: '#/components/schemas/leadaction.ReadBackAction'
          type: array
        cadence:
          allOf:
            - $ref: '#/components/schemas/leadaction.ReadBackCadence'
          description: >
            contact gaps in each reachability state and the new-lead grace.
            Unlike the priority ladder it IS enforced, and normalization always
            fills it in, so a read-back stored before it existed reads back as
            today's behaviour rather than as zeros. See ReadBackCadence in
            reachability.go.
          title: |
            Cadence is how fast this playbook may work one lead: the per-channel
        call_plan:
          description: >
            sentences: the playbook's call retries as Erdo read them, then the
            limits no playbook can move. The platform writes it from the settled
            cadence, so it is what will happen and a person can check it before
            saving. It is not read from storage and is no part of the policy
            digest.
          items:
            type: string
          title: >
            CallPlan is how Erdo will space calls to a lead who has not
            answered, in
          type: array
        contact_hours:
          description: >
            the platform from the two fields above so a person can check it
            before saving. Like CallPlan it is not read from storage and is no
            part of the policy digest.
          items:
            type: string
          title: >
            ContactHours is which hours hold each channel, in sentences, written
            by
          type: array
        email_hours:
          description: >
            a text (SMS or WhatsApp): HoursAnyTime or HoursWorkingHours. Each is
            stored only when the playbook's text moves that channel off what
            Erdo does by itself, so EmailHours is "any\_time" or absent and
            TextHours is "working\_hours" or absent. Absent means the default:
            an email inside the working hours, a text at any hour. A read-back
            stored before these existed therefore reads, encodes and digests
            exactly as it did. A call always keeps the working hours and has no
            such field. See heldByWorkingHours.
          title: >
            EmailHours and TextHours are whether the working hours hold an email
            and
          type: string
        flow:
          allOf:
            - $ref: '#/components/schemas/leadaction.ReadBackFlow'
          description: >
            steps of each stage in sequence, the rules that win from any stage,
            and the facts that change which steps happen. Nothing enforces it —
            the engine reads the text. Null when the playbook was saved before
            flows existed or the flow could not be read. See ReadBackFlow.
          title: >
            Flow is the order the playbook works a lead in, read out for
            drawing: the
        limits:
          items:
            $ref: '#/components/schemas/leadaction.ReadBackLimit'
          type: array
        priority:
          allOf:
            - $ref: '#/components/schemas/leadaction.ReadBackPriority'
          description: |
            none. Nothing enforces it — see ReadBackPriority.
          title: >
            Priority is the ladder read out of the text, or null when the text
            states
        stages:
          items:
            $ref: '#/components/schemas/leadaction.ReadBackStage'
          type: array
        text_hours:
          type: string
        working_hours:
          $ref: '#/components/schemas/leadaction.WorkingHours'
      title: >
        ReadBack is what Erdo understood from a playbook's text when it was
        saved.
      type: object
    leadaction.ReadBackAccompaniment:
      description: >
        goes out beside a gated text or call.


        Mode is "automatic" (an email may go on its own beside the card) or
        "never". There is deliberately no "approval": an accompanying email that
        itself needed approving would be a second thing in front of a person
        about one lead, which is exactly the shape this engine does not have.
      properties:
        mode:
          type: string
        when:
          description: |
            empty when the text does not say.
          title: >
            When is one short sentence from the text saying when it wants one,
            or
          type: string
      title: >
        ReadBackAccompaniment is what the playbook's text says about the email
        that
      type: object
    leadaction.ReadBackAction:
      description: |
        person. Mode is after platform limits.
      properties:
        kind:
          type: string
        mode:
          type: string
        when:
          type: string
      title: >
        ReadBackAction is one action kind and whether the text lets it run
        without a
      type: object
    leadaction.ReadBackCadence:
      description: >
        the per-channel gaps in each reachability state, and how soon after
        capture the engine may start. It is read out of the text like everything
        else in the read-back, and then clamped to what the platform allows.
      properties:
        confirmed:
          allOf:
            - $ref: '#/components/schemas/leadaction.ContactGaps'
          title: |
            Confirmed applies from the first inbound of any kind onwards.
        new_lead_grace_minutes:
          format: int64
          title: |
            NewLeadGraceMinutes is how long after capture a lead is left alone.
          type: integer
        unanswered_attempts:
          description: >
            never answered, in order, one entry per attempt — "email", "text" or
            "call" — including the last message it sends before giving up. Empty
            when the text schedules no such chase, or the read-back was stored
            before it existed. It sets how many unanswered messages may go
            without a person; see unansweredCeiling.
          items:
            type: string
          title: >
            UnansweredAttempts is every contact the text schedules for a lead
            who has
          type: array
        unanswered_call_retries:
          description: >
            for a lead who has never answered, when it schedules one on the same
            day as the call before it: entry N times call N+1. Empty otherwise,
            and on every read-back stored before it existed — and it must stay
            ABSENT from their JSON, not merely empty, because the policy digest
            is taken over this struct and a new key would make every open
            decision look decided under an older playbook. See call\_retries.go.
          items:
            $ref: '#/components/schemas/leadaction.CallRetryStep'
          title: >
            UnansweredCallRetries is every further call the text schedules, in
            order,
          type: array
        unconfirmed:
          allOf:
            - $ref: '#/components/schemas/leadaction.ContactGaps'
          title: >
            Unconfirmed applies while the lead has never answered on any
            channel.
      title: >
        ReadBackCadence is how fast the playbook's text says Erdo may work a
        lead:
      type: object
    leadaction.ReadBackFlow:
      description: >
        a surface can draw it: each stage's steps in the order they happen, the
        rules that win from any stage, and the facts about a lead that change
        which steps happen.


        It is a DRAWING, never a guard. The engine decides from the playbook's
        text; nothing here is enforced and no decision is checked against it. It
        exists because the rest of the read-back lists the parts of a playbook
        (stages, actions, a priority ladder) with no arrows between them, and
        the order is what a reader of the follow-up queue needs to understand
        why a card says what it says.


        Every step, rule and condition carries the passage of the text it was
        read from, and SourceFound says whether that passage really is in the
        text. A surface draws an unfound one as unverified rather than as the
        playbook's.
      properties:
        any_time:
          items:
            $ref: '#/components/schemas/leadaction.FlowRule'
          type: array
        conditions:
          items:
            $ref: '#/components/schemas/leadaction.FlowCondition'
          type: array
        conflicts:
          description: >
            both happen. Only passages found in the text are kept as sources,
            and a conflict with none left is dropped (normalizeFlow). The
            extraction reports them rather than choosing, because the engine has
            to choose silently and the owner should choose instead.
          items:
            $ref: '#/components/schemas/leadaction.FlowConflict'
          title: >
            Conflicts are places where the text tells Erdo two things that
            cannot
          type: array
        stages:
          items:
            $ref: '#/components/schemas/leadaction.FlowStage'
          type: array
      title: >
        ReadBackFlow is the order a playbook works a lead in, read out of its
        text so
      type: object
    leadaction.ReadBackLimit:
      description: |
        renderer can say which rules the operator wrote and which Erdo applied.
      properties:
        kind:
          type: string
        mode:
          type: string
        reason:
          type: string
      title: >
        ReadBackLimit is a restriction the platform imposed on top of the text,
        so a
      type: object
    leadaction.ReadBackPriority:
      description: >
        the playbook's own rules put a lead at each level, and the factors the
        playbook forbids taking into account.


        The three levels are fixed — high, medium, low. They are the platform's,
        not the playbook's: the evaluation's response schema constrains the
        model to exactly those three (see evaluationFormat in evaluate.go) and
        the queue's sort, its filters and its badges are built on them. A
        playbook cannot invent a tier; what it decides is which of its rules map
        onto each level.


        NOTHING HERE IS ENFORCED, and this struct is not a guard. The model
        still chooses a decision's priority and writes its priority\_reason, and
        no decision is rejected, downgraded or rewritten because its priority
        disagrees with this ladder. The read-back exists so an operator can SEE
        what Erdo understood — until now the ladder reached the model only as
        raw playbook text, so a misread rule was invisible until decisions came
        out wrong. Whether to enforce it is a product decision nobody has taken.
      properties:
        forbidden_factors:
          description: >
            a lead's country, city, name or language, say. Empty when it forbids
            none.
          items:
            type: string
          title: >
            ForbiddenFactors are the things the text says must NOT affect
            priority —
          type: array
        high:
          description: >
            one short phrase each, in the text's own terms ("budget $2M or
            more", "they replied to us", "timeline beyond a year").
          items:
            type: string
          title: >
            High, Medium and Low are the conditions the text gives for each
            level,
          type: array
        low:
          items:
            type: string
          type: array
        medium:
          items:
            type: string
          type: array
      title: >
        ReadBackPriority is the priority ladder Erdo read out of a playbook:
        which of
      type: object
    leadaction.ReadBackStage:
      description: >
        from that stage.


        Label and Tone are how a surface shows a lead in this stage. Label is
        the playbook's own name for the stage written out — "qualified\_out"
        reads "Qualified out" — derived here rather than asked of the model, so
        a badge never carries wording the playbook did not use. Tone says
        whether being here is good news, still in progress, not a fit, or
        finished, which a badge colours because nothing in the name says so; it
        is read from the text like the summary, never enforced, and is empty on
        a read-back stored before it existed.
      properties:
        actions:
          items:
            type: string
          type: array
        label:
          type: string
        name:
          type: string
        summary:
          type: string
        tone:
          type: string
      title: >
        ReadBackStage is one stage the text describes, with the actions it
        allows
      type: object
    leadaction.WorkingHours:
      description: |
        lowercase three-letter names; Start and End are "HH:MM" in Timezone.
      properties:
        days:
          items:
            type: string
          type: array
        end:
          type: string
        start:
          type: string
        timezone:
          type: string
      title: |
        WorkingHours is the window automatic contact is allowed in. Days are
      type: object
    leadaction.ContactGaps:
      description: >
        channel. Zero means "the text did not say", which reads as the default,
        never as "no gap at all".


        BetweenChannels is how long after a contact on one channel the next
        contact on a DIFFERENT channel waits ("text them two minutes after the
        call"). Empty when the playbook sets no such wait.


        WhatsAppMinutes is zero on every read-back stored before WhatsApp was a
        channel, and on any the text says nothing about WhatsApp in. Both read
        as the text gap, not as the WhatsApp default: see normalizeCadence.
      properties:
        between_channels:
          items:
            $ref: '#/components/schemas/leadaction.ChannelStep'
          type: array
        call_minutes:
          format: int64
          type: integer
        email_minutes:
          format: int64
          type: integer
        sms_minutes:
          format: int64
          type: integer
        whatsapp_minutes:
          format: int64
          type: integer
      title: |
        ContactGaps is the least time between two contacts to one lead on ONE
      type: object
    leadaction.CallRetryStep:
      description: >
        never answered: how long after the previous call it may go. Minutes is
        that wait; NextDay places the call no sooner than the day after the
        previous one, counted where the lead is, and then Minutes is not read.
      properties:
        minutes:
          format: int64
          type: integer
        next_day:
          type: boolean
      title: |
        CallRetryStep is one further call a playbook schedules to a lead who has
      type: object
    leadaction.FlowRule:
      description: |
        the text gives them precedence.
      properties:
        action:
          type: string
        exit_to:
          type: string
        if:
          type: string
        source:
          type: string
        source_found:
          type: boolean
      title: >
        FlowRule is a rule that applies whatever stage the lead is in, in the
        order
      type: object
    leadaction.FlowCondition:
      description: >
        options are mutually exclusive. Built-in conditions
        (flowBuiltinConditions) are facts Erdo itself tracks for every lead and
        keep Erdo's own label and options whatever the model wrote; any other
        key is one the playbook's text defines, labelled in its own terms.
      properties:
        builtin:
          type: boolean
        key:
          type: string
        label:
          type: string
        options:
          items:
            $ref: '#/components/schemas/leadaction.FlowOption'
          type: array
        source:
          type: string
        source_found:
          type: boolean
      title: >
        FlowCondition is one fact about a lead that changes which steps happen.
        The
      type: object
    leadaction.FlowConflict:
      properties:
        description:
          type: string
        sources:
          items:
            type: string
          type: array
      type: object
    leadaction.FlowStage:
      properties:
        name:
          type: string
        phases:
          items:
            $ref: '#/components/schemas/leadaction.FlowPhase'
          type: array
      type: object
    leadaction.ChannelStep:
      description: |
        contact on After, a contact on Next waits Minutes.
      properties:
        after:
          type: string
        minutes:
          format: int64
          type: integer
        next:
          type: string
      title: |
        ChannelStep is one wait the playbook sets between two channels: after a
      type: object
    leadaction.FlowOption:
      properties:
        label:
          type: string
        value:
          type: string
      type: object
    leadaction.FlowPhase:
      description: |
        a lead first answers us, typically. Most stages have one, with no name.
      properties:
        aim:
          type: string
        name:
          type: string
        steps:
          items:
            $ref: '#/components/schemas/leadaction.FlowStep'
          type: array
      title: >
        FlowPhase is a part of a stage the text treats separately — before and
        after
      type: object
    leadaction.FlowStep:
      description: >
        a short label read relative to the step before it ("at once", "+5 min",
        "next day"). OnlyIf lists the conditions it applies under; empty means
        every lead. ExitTo names the stage the step moves the lead to, "desk"
        for a handoff that takes the lead off Erdo's hands, or is empty.
      properties:
        action:
          type: string
        detail:
          type: string
        exit_to:
          type: string
        label:
          type: string
        only_if:
          items:
            $ref: '#/components/schemas/leadaction.FlowMatch'
          type: array
        source:
          type: string
        source_found:
          type: boolean
        when:
          type: string
      title: >
        FlowStep is one thing Erdo does, or one thing that moves the lead on.
        When is
      type: object
    leadaction.FlowMatch:
      properties:
        key:
          type: string
        values:
          items:
            type: string
          type: array
      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_...).

````

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