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

# PutLeadPlaybookAPI saves the playbook text, reads it into the typed


> read-back, and stores both. Organization admins only.

There is deliberately no MCP tool for this: enabling a playbook that marks email automatic lets Erdo email real leads without a person, and that is a person's decision. A scoped API key is refused.




## OpenAPI

````yaml /api/openapi.json put /v1/lead-playbook
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-22'
servers:
  - url: https://api.erdo.ai
    description: Production
security:
  - bearerAuth: []
paths:
  /v1/lead-playbook:
    put:
      summary: |
        PutLeadPlaybookAPI saves the playbook text, reads it into the typed
      description: >
        read-back, and stores both. Organization admins only.


        There is deliberately no MCP tool for this: enabling a playbook that
        marks email automatic lets Erdo email real leads without a person, and
        that is a person's decision. A scoped API key is refused.
      operationId: PUT:mcp.PutLeadPlaybookAPI
      requestBody:
        content:
          application/json:
            schema:
              properties:
                agent_id:
                  title: >
                    AgentID links the concierge every draft is written as: the
                    id of a voice agent

                    of this organization, from GET /v1/voice/agents. Unlike the
                    two fields above

                    it does NOT keep its saved value — omitted, null or empty
                    clears the link,

                    so the picker always sends its current value.
                  type: string
                body:
                  title: |
                    Body is the full playbook text. Required.
                  type: string
                daily_contact_cap:
                  format: int64
                  title: >
                    DailyContactCap bounds the emails sent or put in front of a
                    person per 24 hours,

                    0 to 500. Keeps its saved value when omitted; 40 when never
                    set.
                  type: integer
                enabled:
                  title: >
                    Enabled turns the sweep on or off for the organization.
                    Keeps its saved value

                    when omitted.
                  type: boolean
                handoff_alert_recipients:
                  items:
                    type: string
                  title: >
                    HandoffAlertRecipients are the email addresses told when the
                    engine files a

                    handoff — a decision it cannot carry out, which needs a
                    person at the sales

                    desk. An empty list switches the alert off; there is no
                    default audience and no

                    fallback to the organization's members. Addresses are
                    lowercased, de-duplicated

                    and capped at 10, and one that is not an email address is
                    refused rather than

                    dropped. Keeps its saved value when omitted — like
                    signature_name and unlike

                    agent_id — so a client that does not know this field cannot
                    take a development's

                    desk off its own alert list; send an empty list to stop
                    alerting anybody.
                  type: array
                signature_name:
                  title: >
                    SignatureName is the name a LEAD sees: follow-up emails are
                    signed and sent

                    as it, and a call brief opens with it. Left empty, the
                    linked concierge's own

                    display name is used. Keeps its saved value when omitted —
                    unlike agent_id — so

                    a client that does not know this field cannot strip the name
                    a development signs

                    its mail with; send an empty string to clear it back to the
                    concierge's name.
                  type: string
              type: object
              required:
                - body
        required: true
      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:
                    format: int64
                    type: integer
                  enabled:
                    type: boolean
                  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_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:
    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 and WorkingHours 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
        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
        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
        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.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.
      properties:
        actions:
          items:
            type: string
          type: array
        name:
          type: string
        summary:
          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.
      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
      title: |
        ContactGaps is the least time between two contacts to one lead on ONE
      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
  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_...).

````