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

# List campaigns

> Newest-first. Required scope `campaigns:read`.


Returns your workspace's campaigns, newest first, with `agent`, `from_phone_number` and `stop_dispositions` expanded to the full objects they reference. Archived campaigns are excluded.

Required scope: `campaigns:read`.

## Query parameters

| Parameter | Type    | Default | Notes                                                                                                                                                |
| --------- | ------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| `status`  | string  | —       | Comma-separated list of statuses to keep. Whitespace around each value is trimmed, so `running, paused` works. Omit for every non-archived campaign. |
| `limit`   | integer | `50`    | Page size. Clamped to `1`–`200`.                                                                                                                     |
| `offset`  | integer | `0`     | Rows to skip. Negative values are treated as `0`.                                                                                                    |

## Statuses

| Status                       | Meaning                                                                                                                                                                     |
| ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `draft`                      | Created but never launched. Nothing dials.                                                                                                                                  |
| `scheduled`                  | Queued to start; treated like `running` once its schedule opens.                                                                                                            |
| `running`                    | Actively working the list.                                                                                                                                                  |
| `paused`                     | Paused by you. **Does not auto-resume** — a top-up or a config fix will not restart it.                                                                                     |
| `paused_insufficient_credit` | Balance fell below the minimum needed to place a call. **Auto-resumes** once you top up.                                                                                    |
| `paused_budget`              | `budget_cents` reached, counting in-flight worst case. Raise the budget and resume.                                                                                         |
| `paused_infra`               | Voice capacity is unhealthy or the queue is not draining. Transient.                                                                                                        |
| `paused_config`              | A permanent configuration problem — inactive from-number, agent with no duration cap, no reachable calling window. Pauses on first occurrence rather than retrying forever. |
| `completed`                  | Every enrolled contact reached a terminal state.                                                                                                                            |
| `stopped`                    | Stopped by you. Terminal.                                                                                                                                                   |
| `archived`                   | Soft-deleted. Never returned by this endpoint.                                                                                                                              |

## Example request

```bash theme={null}
# Everything (first page)
curl "https://api.goyappr.com/campaigns" \
  -H "Authorization: Bearer $YAPPR_API_KEY"

# Only the ones that need attention
curl "https://api.goyappr.com/campaigns?status=paused,paused_insufficient_credit,paused_budget,paused_config" \
  -H "Authorization: Bearer $YAPPR_API_KEY"

# Second page of 25
curl "https://api.goyappr.com/campaigns?limit=25&offset=25" \
  -H "Authorization: Bearer $YAPPR_API_KEY"
```

## Example response

```json theme={null}
{
  "data": [
    {
      "id": "b3f1c0d2-5a44-4f0e-9c11-7a2e8d3f0001",
      "name": "July renewals",
      "status": "running",
      "agent_id": "7e8a91c1-0000-4c11-9a00-000000000001",
      "from_phone_number_id": "2d9f4b6a-0000-4a3c-8b21-000000000002",
      "stop_disposition_ids": ["5c1d9a2e-0000-4b10-9f31-000000000010"],
      "randomize_retry_time": true,
      "max_attempts": 3,
      "max_calls_per_day": 150,
      "min_seconds_between_calls": 45,
      "max_in_flight": 2,
      "daily_admitted_count": 37,
      "daily_window_date": "2026-07-28",
      "last_admitted_at": "2026-07-28T11:58:04.010Z",
      "budget_cents": 50000,
      "spent_cents": 8140,
      "reserved_cents": 1000,
      "regulatory_basis": "existing_customer",
      "last_tick_at": "2026-07-28T11:59:00.412Z",
      "last_tick_result": "spacing",
      "last_error": null,
      "started_at": "2026-07-27T06:00:11.900Z",
      "completed_at": null,
      "total_leads": 412,
      "stats": {},
      "created_at": "2026-07-26T09:14:22.123Z",
      "updated_at": "2026-07-28T11:59:00.412Z",
      "agent": { "id": "7e8a91c1-0000-4c11-9a00-000000000001", "name": "Renewals agent" },
      "from_phone_number": { "id": "2d9f4b6a-0000-4a3c-8b21-000000000002", "phone_number": "+972737000000" },
      "stop_dispositions": [
        { "id": "5c1d9a2e-0000-4b10-9f31-000000000010", "label": "Do Not Call" }
      ]
    }
  ],
  "pagination": { "total": 4, "limit": 50, "offset": 0 },
  "company_id": "fe493f11-0000-0000-0000-000000000001"
}
```

Each row carries the full campaign object; the sample above is trimmed to the interesting fields. See `GET /campaigns/{id}` for the complete field reference.

An unknown value in `status` is not an error — it simply matches nothing, so check `pagination.total` before concluding a campaign disappeared.

## Errors

| HTTP | Code                 | When                                  |
| ---- | -------------------- | ------------------------------------- |
| 401  | `INSUFFICIENT_SCOPE` | API key lacks `campaigns:read`.       |
| 500  | —                    | The list query failed. Safe to retry. |


## OpenAPI

````yaml GET /campaigns
openapi: 3.1.0
info:
  title: Yappr API
  description: >
    Create and manage AI voice agents, purchase phone numbers, configure tools,
    and initiate calls — all via REST.
  version: 1.0.0
  contact:
    url: https://goyappr.com
servers:
  - url: https://api.goyappr.com
    description: Production
security:
  - apiKey: []
paths:
  /campaigns:
    get:
      tags:
        - Campaigns
      summary: List campaigns
      description: |
        Newest-first. Required scope `campaigns:read`.
      operationId: listCampaigns
      parameters:
        - in: query
          name: status
          schema:
            type: string
          description: >-
            Comma-separated list of statuses to include, e.g.
            `running,paused_insufficient_credit`. Omit for every non-archived
            campaign.
          example: running,paused
        - in: query
          name: limit
          schema:
            type: integer
            default: 50
            maximum: 200
        - in: query
          name: offset
          schema:
            type: integer
            default: 0
      responses:
        '200':
          description: List of campaigns
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Campaign'
                  pagination:
                    type: object
                    properties:
                      total:
                        type: integer
                      limit:
                        type: integer
                      offset:
                        type: integer
                  company_id:
                    type: string
                    format: uuid
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          description: Insufficient scope
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
components:
  schemas:
    Campaign:
      type: object
      description: >
        A paced bulk-outbound dialing job over enrolled contacts.


        A campaign is an **admission-only scheduler**. It decides *when* each
        enrolled

        contact becomes eligible, then performs one ordinary outbound call — the
        same

        queue, the same priority, and the same concurrency limits as a call
        placed with

        `POST /calls`. The pacing fields below only control how fast a campaign
        hands

        calls to that queue; a campaign call never takes precedence over
        anything else.


        Campaigns are always created as `draft` and are launched with

        `POST /campaigns/{id}/launch`.


        **Two independent per-contact stop conditions** apply, whichever fires
        first:

        `max_attempts`, and the stop-disposition set (`stop_disposition_ids`
        plus the

        `stop_on_*` booleans). Landing a stop disposition retires that contact

        permanently; any other outcome retries until the attempt cap.


        Fields marked *Engine-owned* are written by the campaign engine and are
        never

        writable. Sending one on create or update returns `400`, as does any
        field name

        that isn't on the writable list — a misspelled `stop_dispositions` is
        rejected

        rather than silently ignored, so you can never believe a kill switch is
        armed

        when it is not.
      properties:
        id:
          type: string
          format: uuid
        company_id:
          type: string
          format: uuid
        name:
          type: string
          example: July reactivation
        description:
          type: string
          nullable: true
        status:
          type: string
          enum:
            - draft
            - scheduled
            - running
            - paused
            - paused_insufficient_credit
            - paused_budget
            - paused_infra
            - paused_config
            - completed
            - stopped
            - archived
          description: >
            Engine-owned. Move it with the transition endpoints

            (`launch` / `pause` / `resume` / `stop`), never with `PATCH`.


            `draft` — created, not dialing.

            `scheduled` — waiting for `starts_at`.

            `running` — admitting contacts.

            `paused` — paused by you. A manual pause is **never** lifted
            automatically; it

            survives a top-up.

            `paused_insufficient_credit` — balance is below the minimum needed
            to place a

            call. **Auto-resumes** shortly after the balance is topped up.

            `paused_budget` — `budget_cents` reached.

            `paused_infra` — repeated platform-side failures.

            `paused_config` — a permanent configuration problem (from-number no
            longer

            active, agent missing a call-duration cap, no reachable calling
            window).

            `completed` — every enrolled contact reached a terminal state.

            `stopped` — stopped by you. Terminal.

            `archived` — soft-deleted by `DELETE /campaigns/{id}`. Terminal.
        agent_id:
          type: string
          format: uuid
          nullable: true
          description: Agent that runs the calls. Required before launch.
        agent:
          nullable: true
          description: Full Agent object for `agent_id`.
          allOf:
            - $ref: '#/components/schemas/Agent'
        from_phone_number_id:
          type: string
          format: uuid
          nullable: true
          description: >-
            Number the campaign calls from. Required before launch, and must
            still be active at launch time.
        from_phone_number:
          nullable: true
          description: Full PhoneNumber object for `from_phone_number_id`.
          allOf:
            - $ref: '#/components/schemas/PhoneNumber'
        from_number:
          type: string
          nullable: true
          description: >-
            Engine-owned. E.164 snapshot of the from-number, kept for audit even
            if the number is later released.
          example: '+972551234567'
        retry_rules:
          type: object
          additionalProperties: true
          description: >
            Optional per-outcome retry overrides. Free-form object — the
            explicit `max_attempts` / `retry_*_seconds` fields cover the common
            cases.
        calling_window:
          type: object
          additionalProperties: true
          description: >
            Optional campaign-level narrowing of the workspace calling hours.
            Omit to inherit the workspace schedule (`GET /call-windows`). A
            campaign can only ever dial inside the workspace window — this field
            cannot widen it.
        stop_disposition_ids:
          type: array
          items:
            type: string
            format: uuid
          description: >
            Disposition **IDs** that retire a contact permanently when assigned
            — the

            campaign's kill switch. Always IDs, never labels: labels are
            renameable, and a

            rename would silently disarm the rule.


            Do **not** list `No Answer`, `Failed` or `Voicemail` here. Those
            three are also

            auto-assigned to calls nobody picked up, so using them as stop
            dispositions

            retires contacts you did in fact speak to. Use `stop_on_no_answer`
            and

            `stop_on_voicemail` instead.


            Must contain dispositions belonging to your company; anything else
            is rejected

            with `400`.
        stop_dispositions:
          type: array
          description: Full Disposition objects for `stop_disposition_ids`.
          items:
            $ref: '#/components/schemas/Disposition'
        stop_on_no_answer:
          type: boolean
          example: false
          description: Retire a contact the first time a call goes unanswered.
        stop_on_voicemail:
          type: boolean
          example: false
          description: Retire a contact the first time a call reaches an answering machine.
        stop_on_unclassified:
          type: boolean
          example: false
          description: >
            What to do when the outcome that arrives is `Unclassified` — the
            call happened but could not be matched to any of your outcomes.
            `false` retries the contact, `true` retires it.

            This is not a timeout. A call's outcome is authoritative and a
            contact is never advanced without one: if classification is slow,
            only that contact waits while the campaign keeps calling everyone
            else.
        max_attempts:
          type: integer
          minimum: 1
          maximum: 999
          example: 3
          description: >
            Per-contact dial cap. One of the two independent stop conditions —
            the other is the stop-disposition set.
        max_infra_retries:
          type: integer
          minimum: 0
          maximum: 20
          example: 3
          description: >
            Separate budget for retries after a platform-side failure — a call
            that never reached the person. These do not consume `max_attempts`
            and are never terminal.
        retry_no_answer_seconds:
          type: integer
          minimum: 30
          maximum: 604800
          example: 3600
          description: Delay before retrying a contact who did not answer.
        retry_completed_seconds:
          type: integer
          minimum: 60
          maximum: 604800
          example: 86400
          description: >-
            Delay before retrying a contact whose call completed but landed on a
            non-stop disposition.
        randomize_retry_time:
          type: boolean
          example: false
          description: >
            Which time of day a retry lands on. `false` keeps the wait exact, so
            a one-week wait retries at the same hour a week later. `true` picks
            a different hour inside the campaign's calling window, so repeat
            attempts do not always arrive at the same moment. The wait length
            itself is unchanged either way — a randomized retry is still never
            earlier than the configured wait.
        double_dial_enabled:
          type: boolean
          example: false
          description: Ring a second time shortly after an unanswered first ring.
        double_dial_gap_seconds:
          type: integer
          minimum: 10
          maximum: 3600
          example: 90
          description: Gap between the two rings when `double_dial_enabled` is true.
        max_calls_per_day:
          type: integer
          minimum: 1
          maximum: 100000
          example: 200
          description: Daily admission cap, counted against the workspace timezone day.
        min_seconds_between_calls:
          type: integer
          minimum: 0
          maximum: 86400
          example: 30
          description: >-
            Minimum spacing between two calls handed to the queue by this
            campaign.
        max_in_flight:
          type: integer
          minimum: 1
          maximum: 8
          example: 2
          description: >
            Calls this campaign may have live at once. Platform concurrency
            limits still apply on top — raising this does not buy the campaign
            extra capacity.
        budget_cents:
          type: integer
          minimum: 1
          nullable: true
          description: >
            Hard spend cap for the campaign, enforced against `spent_cents` +
            `reserved_cents` (credits are debited when a call ends, so in-flight
            cost has to count). `null` means no cap. On hit the campaign moves
            to `paused_budget`.
        regulatory_basis:
          type: string
          nullable: true
          enum:
            - lawful_basis_confirmed
            - consent
            - existing_customer
            - non_marketing
            - registry_screened
          description: >
            Your lawful basis for calling this list. **Required before launch**
            — recorded on the campaign's launch audit record alongside the
            enrolled count.

            `lawful_basis_confirmed` is what the dashboard records: a single
            attestation that the caller has consent or another lawful basis for
            everyone on the list. The four specific values are for callers that
            know which one applies.
        starts_at:
          type: string
          format: date-time
          nullable: true
          description: Do not admit contacts before this time.
        ends_at:
          type: string
          format: date-time
          nullable: true
          description: Do not admit contacts after this time.
        daily_admitted_count:
          type: integer
          description: >-
            Engine-owned. Calls handed to the queue during the current day
            window.
        daily_window_date:
          type: string
          format: date
          nullable: true
          description: Engine-owned. Day `daily_admitted_count` belongs to.
        last_admitted_at:
          type: string
          format: date-time
          nullable: true
          description: Engine-owned. When this campaign last handed a call to the queue.
        estimate_cents:
          type: integer
          nullable: true
          description: Engine-owned. Projected campaign cost at the time of launch.
        spent_cents:
          type: integer
          description: Engine-owned. Settled spend so far.
        reserved_cents:
          type: integer
          description: >-
            Engine-owned. Worst-case cost of in-flight calls, held against
            `budget_cents` until they settle.
        last_tick_at:
          type: string
          format: date-time
          nullable: true
          description: Engine-owned. Last time the engine evaluated this campaign.
        last_tick_result:
          type: string
          nullable: true
          description: >
            Engine-owned. Machine-readable answer to "why is nothing happening
            right now" — e.g. daily cap reached, outside the calling window,
            waiting on in-flight calls.
        last_error:
          type: string
          nullable: true
          description: Engine-owned. Last error the engine recorded for this campaign.
        started_at:
          type: string
          format: date-time
          nullable: true
          description: Engine-owned. First launch time.
        completed_at:
          type: string
          format: date-time
          nullable: true
          description: Engine-owned. When the last contact reached a terminal state.
        total_leads:
          type: integer
          description: Engine-owned. Contacts currently enrolled.
        stats:
          type: object
          additionalProperties: true
          description: Engine-owned counter roll-up. Prefer `GET /campaigns/{id}/stats`.
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        created_by:
          type: string
          format: uuid
          nullable: true
          description: Null when the campaign was created via the API.
    Error:
      type: object
      properties:
        error:
          type: string
        code:
          type: string
    Agent:
      type: object
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        type:
          type: string
          enum:
            - prompt
            - flow
          default: prompt
          description: >
            `prompt` (default, legacy): a single `system_prompt` drives the
            call; the LLM decides when to call attached tools. `flow`: the call
            is driven by a `flow_config` graph of conversation, tool-call, and
            routing nodes. `type` is **immutable** post-create — to change
            types, create a new agent.
        flow_config:
          oneOf:
            - $ref: '#/components/schemas/FlowConfig'
            - type: 'null'
          description: >
            The flow graph. Required when `type='flow'`, must be omitted/null
            when `type='prompt'`.
        system_prompt:
          type: string
        description:
          type: string
          nullable: true
        voice:
          type: string
          enum:
            - Michal
            - Yonatan
            - David
            - Rachel
            - Gil
            - Noa
            - Maya
            - Adam
            - Shira
            - Avigail
            - Amir
            - Liat
            - Omer
            - Tamar
            - Tom
            - Benny
            - Nir
            - Natan
            - Yael
            - Dvora
            - Yosef
            - Shir
            - Anat
            - Ariel
            - Roi
            - Shlomo
            - Dana
            - Alon
            - Ruth
            - Yuval
            - Keren
            - Eitan
            - Hila
            - Ido
            - Boaz
            - Tali
            - Erez
            - Efrat
          description: >
            The persona the agent speaks in, and the only thing that selects
            between Yappr's two voice families — there is no separate engine
            field, and `engine` / `engine_voice` are rejected if you send them.

            `Keren, Eitan, Hila, Ido, Boaz, Tali, Erez, Efrat` are the second
            family, available to every workspace. Agents on these voices handle
            their own turn-taking and expressiveness, so `temperature`,
            `vad_stop_secs`, `vad_start_secs` and `vad_confidence` are rejected
            on them, and they cannot be used on a `type='flow'` agent. Any other
            name returns `400` listing every voice you may use.

            Always the name the agent actually speaks in, whichever family it is
            on.
        background_sound:
          type: string
          nullable: true
          enum:
            - call_center
            - open_office
            - cafe
            - outdoor
          description: >-
            Ambient background sound mixed under the agent's voice during calls.
            Null means silent (default).
        background_sound_volume:
          type: number
          minimum: 0
          maximum: 0.6
          default: 0.3
          description: >-
            Volume of the background sound, 0.0–0.6. Capped to protect
            turn-taking.
        language:
          type: string
          enum:
            - he
            - en
        temperature:
          type: number
          minimum: 0
          maximum: 2
        greeting_message:
          type: string
          nullable: true
        agent_speaks_first:
          type: boolean
        vad_stop_secs:
          type: number
          minimum: 0.05
          maximum: 5
          default: 0.5
          description: Seconds of silence before VAD confirms speech has stopped
        vad_start_secs:
          type: number
          minimum: 0.05
          maximum: 2
          default: 0.2
          description: Seconds of speech before VAD confirms speech has started
        vad_confidence:
          type: number
          minimum: 0
          maximum: 1
          default: 0.7
          description: Minimum confidence threshold for voice detection
        silence_timeout_secs:
          type: number
          minimum: 10
          maximum: 900
          default: 60
          description: >-
            Seconds of caller silence before auto-hangup. Prevents idle calls
            wasting credits.
        max_continuous_speech_secs:
          type: number
          minimum: 0
          maximum: 300
          default: 120
          description: >-
            Max seconds one party can speak non-stop before auto-hangup. Catches
            answering machines. 0 = disabled.
        max_call_duration_secs:
          type: number
          minimum: 0
          maximum: 3600
          default: 600
          description: >-
            Hard cap on total call duration regardless of activity. 0 = no cap
            of the agent's own; the platform still ends the call after 65
            minutes (3900 s), with the disconnect reason `Platform call limit
            reached`.
        lead_memory_enabled:
          type: boolean
          default: true
          description: >-
            When true, the matched lead's long-term memory context is injected
            into the system prompt at call time.
        is_active:
          type: boolean
        webhook_url:
          type: string
          format: uri
          nullable: true
        webhook_events:
          type: array
          description: >
            Events posted to `webhook_url` as `{event, timestamp, agent_id,
            company_id, call_id, data}`.

            `call.failed` and `call.no_answer` carry `data.hangup_cause` when
            the cause is known. For a call

            from a number in [your own Telnyx
            account](/concepts/bring-your-own-carrier) that Telnyx

            refused while it rang, `call.failed` has `data.hangup_cause`:


            - `carrier_rejected` — Telnyx refused the call (SIP 401, 403 or
            407).

            - `carrier_number_invalid` — Telnyx could not route the number
            called (SIP 404, 484 or 604).


            The SIP code is not in the webhook; the carrier account keeps the
            last one in `last_error.sip_code`.

            One Telnyx refused before it rang sends no webhook when `POST
            /calls` was placing it (the

            request answers `422`); from the queue it sends `call.failed` with
            `data.error_reason` instead.
          items:
            type: string
            enum:
              - call.started
              - call.answered
              - call.ended
              - call.failed
              - call.no_answer
              - call.dnc_blocked
              - transcript.ready
              - call.analyzed
        webhook_headers:
          type: object
          nullable: true
          additionalProperties:
            type: string
          description: >
            Custom HTTP headers sent with every webhook delivery for this agent
            (e.g. an auth token).

            Flat name → string-value map, or null to clear. Headers that would
            override request routing

            or HTTP framing (Host, Content-Length, Transfer-Encoding,
            Connection, Expect, Keep-Alive,

            TE, Trailer, Upgrade, Proxy-*) are rejected with 400.
          example:
            Authorization: Bearer sk_live_…
            X-Source: yappr
        extraction_parameters:
          type: array
          description: >-
            AI extraction parameters — after each call, values are extracted
            from the transcript and included in the call.analyzed webhook
            payload (and stored on the call log).
          items:
            type: object
            required:
              - name
              - description
            properties:
              name:
                type: string
                example: customerName
                description: Parameter key in the extracted_data output
              description:
                type: string
                example: The caller's full name as mentioned during the conversation
                description: Instructions for the AI on what to extract
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    PhoneNumber:
      type: object
      properties:
        id:
          type: string
          format: uuid
        number:
          type: string
          example: '+972551234567'
        friendly_name:
          type: string
          nullable: true
        provider:
          type: string
          description: >
            Where the number comes from: `telnyx` for a number bought from
            Yappr,

            `external` for a number in your own Telnyx account, added to a

            [carrier account](/api-reference/carrier-accounts/add-number).
        status:
          type: string
          enum:
            - active
            - pending_requirements
            - suspended
        is_active:
          type: boolean
        inbound_agent_id:
          type: string
          format: uuid
          nullable: true
        outbound_agent_id:
          type: string
          format: uuid
          nullable: true
        sip_inbound_configured:
          type: boolean
        sip_outbound_configured:
          type: boolean
        country_code:
          type: string
          nullable: true
        monthly_cost:
          type: number
          nullable: true
        created_at:
          type: string
          format: date-time
        carrier_account:
          type: object
          nullable: true
          description: >
            The Telnyx account an `external` number calls through. `null` on
            numbers

            bought from Yappr.
          properties:
            id:
              type: string
              format: uuid
            name:
              type: string
            provider:
              type: string
              enum:
                - telnyx
            status:
              type: string
              enum:
                - untested
                - active
                - paused
        ownership_verified_at:
          type: string
          format: date-time
          nullable: true
          description: >
            When your Telnyx API key last proved that this `external` number is
            an

            active number in your account. `null` means Telnyx no longer lists
            it: the

            number is not callable until a
            [test](/api-reference/carrier-accounts/test)

            finds it again. Always `null` on numbers bought from Yappr.
    Disposition:
      type: object
      properties:
        id:
          type: string
          format: uuid
        label:
          type: string
        color:
          type: string
          nullable: true
          example: '#22c55e'
        position:
          type: integer
        is_protected:
          type: boolean
          description: Protected dispositions cannot be deleted.
        created_at:
          type: string
          format: date-time
    FlowConfig:
      type: object
      required:
        - nodes
      description: >
        The full flow definition stored in `agents.flow_config`. Constraints
        enforced at validate-time

        (any failure returns 400 `FLOW_INVALID` with one issue per problem — see
        `FlowInvalidError`):


        - Exactly one node of `type=start` (`no_start` / `multiple_starts`).

        - Start node has a `next_step_id` (`start_unwired`).

        - All `next_step_id` references resolve to a real node id
        (`unknown_target_node`).

        - Node ids are unique.

        - Every conversation node has non-empty `instructions`
        (`instructions_missing`).

        - Every conversation transition has a non-empty `description`
        (`transition_description_missing`).
          The description is the natural-language trigger the voice agent reads at runtime to pick a path —
          a bare `label` is not sufficient.
        - Every tool_call node has a `tool_id` (`tool_id_missing`).

        - At most 127 unique typed extraction contracts may be exposed in one
        flow
          (`too_many_extraction_contracts`). Nodes with identical effective tool/config schemas
          deduplicate; integration arguments participate only in `ai_extract` mode.
        - Every integration_call node has a valid `action` for its `provider`
        (`action_invalid`),
          an `integration_id` (`integration_id_missing`), and that integration belongs to the caller's
          company and is `active` (`integration_not_in_company`). Provider validity is enforced at
          schema parse time (`schema_invalid` if missing/unknown).
        - tool_call / integration_call nodes have a wired `success_next_step_id`
        (`success_not_wired`).

        - transfer nodes have a `transfer_to` (`transfer_to_missing`).

        - **Only `end` and `transfer` nodes may be terminal.** conversation,
        tool_call, and integration_call
          nodes must have at least one outgoing edge (a transition for conversation; `success` for the others).
          Violations: `terminal_not_allowed` per offending node, `no_terminal` if the flow has none at all.
        - Every node must be reachable from start (`unreachable_node`).
      properties:
        nodes:
          type: array
          minItems: 1
          items:
            $ref: '#/components/schemas/FlowNode'
        flow_config_version:
          type: string
          default: '1'
        metadata:
          type: object
          additionalProperties: true
          description: >-
            Optional metadata surfaced to the eval LLM context (e.g. agent
            purpose, customer name conventions).
          properties:
            custom_metadata_keys:
              type: array
              items:
                type: string
              description: >
                User-declared keys that this flow's `args_template` token
                interpolation expects to

                find under the `{{metadata.<key>}}` namespace at dispatch time.
                Surfaced in the

                dashboard so callers know which keys to pass in `POST /calls
                body.metadata`.

                Keys not listed here still resolve at runtime if passed (and to
                empty string if

                not) — this list is purely a hint for the UI / API consumers.
    FlowNode:
      description: Discriminated by `type`.
      oneOf:
        - $ref: '#/components/schemas/StartNode'
        - $ref: '#/components/schemas/ConversationNode'
        - $ref: '#/components/schemas/ToolCallNode'
        - $ref: '#/components/schemas/IntegrationCallNode'
        - $ref: '#/components/schemas/TransferNode'
        - $ref: '#/components/schemas/EndNode'
      discriminator:
        propertyName: type
        mapping:
          start:
            $ref: '#/components/schemas/StartNode'
          conversation:
            $ref: '#/components/schemas/ConversationNode'
          tool_call:
            $ref: '#/components/schemas/ToolCallNode'
          integration_call:
            $ref: '#/components/schemas/IntegrationCallNode'
          transfer:
            $ref: '#/components/schemas/TransferNode'
          end:
            $ref: '#/components/schemas/EndNode'
    StartNode:
      type: object
      required:
        - id
        - type
      properties:
        id:
          type: string
        type:
          type: string
          enum:
            - start
        name:
          type: string
        position:
          $ref: '#/components/schemas/FlowNodePosition'
        agent_speaks_first:
          type: boolean
          default: true
          description: >
            Whether the bot speaks first on call connect. When false, the bot
            waits silently for the caller to speak first (silence_timeout_secs
            on the agent still applies). For flow agents this OVERRIDES the
            agent-level agent.agent_speaks_first / agent.greeting_message
            fields.
        greeting:
          type: string
          nullable: true
          description: Spoken greeting (only used when agent_speaks_first is true)
        is_literal:
          type: boolean
          default: false
          description: >-
            When true, speak `greeting` verbatim. When false, treat it as an LLM
            instruction.
        next_step_id:
          type: string
          nullable: true
        auto_advance:
          type: boolean
          default: true
          description: >
            Whether to enter the first conversation node immediately on session
            start (default `true`). When `false`, the bot's greeting is
            delivered in start-node context only; the first conversation node is
            entered only after the user's first reply (via internal
            advancement).
    ConversationNode:
      type: object
      required:
        - id
        - type
        - instructions
        - transitions
      allOf:
        - $ref: '#/components/schemas/GlobalNodeFields'
      properties:
        id:
          type: string
        type:
          type: string
          enum:
            - conversation
        name:
          type: string
        position:
          $ref: '#/components/schemas/FlowNodePosition'
        instructions:
          type: string
          description: >-
            Layered on top of agent.system_prompt as a system message at step
            entry
        transitions:
          type: array
          items:
            $ref: '#/components/schemas/Transition'
    ToolCallNode:
      type: object
      required:
        - id
        - type
        - transitions
      description: >
        Deterministic tool dispatch. Tool args are owned by the tool itself

        (`payload_config.static_parameters` for literals plus

        `payload_config.extraction_parameters` for runtime-extracted values) —

        a `tool_call` node carries no per-node `args_template`. The same tool

        used by N flow nodes always sends the same shape; if you need a

        different shape per step, create a separate tool or supply a

        `config_override`. At call start, the effective referenced-tool config
        is

        registered as a flat argument-submission schema: one named string field

        per `extraction_parameters` entry, with `required` (default `true`)

        controlling which fields must be collected. The model never submits a

        `node_id` or nested `args` object. The runtime assembles standard call

        metadata and static parameters; the submitter exposes only extraction

        fields. Payload merge order is standard metadata, then static
        parameters,

        then extracted values, so extracted values win deliberate name

        collisions. Keep names unique unless that override is intentional.

        Schemas remain fixed for that live call; tool/config-override edits
        apply

        on the next call. Stale

        `args_template` payloads on this node type are silently dropped at parse

        time.
      properties:
        id:
          type: string
        type:
          type: string
          enum:
            - tool_call
        name:
          type: string
        position:
          $ref: '#/components/schemas/FlowNodePosition'
        tool_id:
          type: string
          format: uuid
          nullable: true
          description: References an existing row in the company's tools table
        config_override:
          type: object
          additionalProperties: true
          nullable: true
          description: >-
            Per-node override merged shallow over the referenced tool's config
            (array replacement). Open-shape — each tool type has its own valid
            keys.
        transitions:
          $ref: '#/components/schemas/ToolCallTransitions'
        pre_fire_announcement:
          type: boolean
          nullable: true
          description: >
            When true, the platform plays a short hold tone the moment this node
            fires so the caller

            doesn't sit in silence while the action runs. The tone is
            platform-controlled (not

            configurable) and stops automatically when the action returns. Use
            it for actions you

            expect to take more than ~500 ms (webhook to a slow CRM, complex DB
            lookup); skip it for

            fast actions to avoid making the call feel chatty.
        timeout_secs:
          type: number
          nullable: true
          minimum: 1
          maximum: 600
          description: >
            Per-node hard cap on execution time. When the action doesn't return
            in this many seconds,

            the runtime cancels it and routes to `error_next_step_id` with a
            `tool_timeout_after_Ns`

            error. When null/omitted on a webhook tool, the controller uses the
            effective tool

            `config.timeout_seconds` plus one second of dispatch overhead. Other
            tool nodes use the

            platform default (30s). Set this only when the flow needs a
            different node-level limit.
    IntegrationCallNode:
      type: object
      required:
        - id
        - type
        - provider
        - integration_id
        - action
        - transitions
      description: >
        Calls an OAuth-backed third-party integration (e.g. Google Calendar,
        Gmail) directly from the flow,

        without going through the `tools` table. The integration config —
        provider, account, action — lives on

        the node itself; the runtime resolves each entry in `args_template` per
        its declared mode (literal /

        ai_extract), interpolating `{{node.arg}}` and `{{metadata.key}}`
        mustache tokens, and dispatches

        against the matching integration client. Routing is deterministic and
        identical to `tool_call`

        (success / error / custom JSONPath branches, mutually exclusive, exactly
        one out-edge per fire).


        `provider` is locked at node creation; you cannot flip Calendar → Gmail
        on an existing node.

        `integration_id` must reference an `active` row in the caller's company
        `integrations` table whose

        `provider` matches this node's `provider`. `action` must be one of the
        actions in the catalog for the

        chosen provider.


        Action catalog:

        - `google_calendar.create_event` — required args: `summary`,
        `start_time`, `end_time`. Optional: `attendees`, `description`,
        `location`, `calendar_id`, `time_zone`.

        - `google_calendar.list_events` — optional args: `time_min`, `time_max`,
        `max_results`, `query`, `calendar_id`, `time_zone`.

        - `google_calendar.check_availability` — required args: `start_time`,
        `end_time`. Optional: `calendar_id`, `time_zone`.

        - `google_calendar.cancel_event` — required args: `event_id`. The
        runtime auto-resolves which calendar the event lives on (tries `primary`
        first, falls back to scanning the user's other writable calendars on a
        404), so `calendar_id` is intentionally NOT in the catalog for this
        action.

        - `gmail.send_email` — required args: `to`, `subject`, `body`. Optional:
        `html`, `cc`, `bcc`.


        For Google Calendar actions: `calendar_id` defaults to the user's
        `primary` calendar when blank. `time_zone` is an IANA name
        (`"Asia/Jerusalem"`) — when set, both Google's response is pinned to
        that zone and the event being created is stamped with it.


        **Calendar response post-processing**: `create_event`, `list_events`,
        and `check_availability` responses are sanitized for the LLM before they
        reach the agent — the voice model's ISO 8601 parser handles offsets
        unreliably, so the runtime strips the offset and seconds from each
        event's `dateTime` (leaving wall-clock format `"2026-05-10 16:30"`),
        removes per-event `timeZone` fields, and prepends a top-level `timeZone`
        + `timeZone_note` anchor. The agent only ever sees the sanitized view;
        JSONPath custom transitions (`transitions.custom[].jsonpath`) match
        against the sanitized view too. The raw, untouched Google response is
        preserved separately on the call event as `raw_response_preview` (see
        `FlowTraceToolCall` schema) for audit.
      properties:
        id:
          type: string
        type:
          type: string
          enum:
            - integration_call
        name:
          type: string
        position:
          $ref: '#/components/schemas/FlowNodePosition'
        provider:
          type: string
          enum:
            - google_calendar
            - gmail
          description: >-
            Locked at node creation. Must match the linked integration's
            provider.
        integration_id:
          type: string
          format: uuid
          description: >-
            References an active row in the caller's company integrations table
            whose provider matches this node's provider.
        action:
          type: string
          description: >-
            Provider-scoped action identifier — see the action catalog in the
            description.
          example: create_event
        args_template:
          type: object
          additionalProperties:
            $ref: '#/components/schemas/ArgValue'
          description: >
            Map of arg name → `ArgValue`. Required keys depend on the chosen
            action (see catalog above).

            Each `ArgValue` is one of: literal string, `{mode:'literal'}`, or
            `{mode:'ai_extract'}`.

            Both `literal.value` and `ai_extract.description` may contain
            `{{<node_id>.<arg_name>}}`

            tokens (cross-node slot refs to earlier `ai_extract` args) and
            `{{metadata.<key>}}`

            tokens (per-call metadata, built-in keys plus any user-declared
            custom keys; see

            `CallMetadataKey` and `FlowConfig.metadata.custom_metadata_keys`).
        pre_fire_announcement:
          type: boolean
          nullable: true
          description: >
            When true, the platform plays a short hold tone the moment this node
            fires so the caller

            doesn't sit in silence while the action runs. Tone is
            platform-controlled (not

            configurable) and stops automatically when the action returns.
            Recommended for

            `create_event` / `send_email` / network-bound actions; skip for
            `check_availability`

            which is fast.
        timeout_secs:
          type: number
          nullable: true
          minimum: 1
          maximum: 600
          description: >
            Per-node hard cap on execution time. When the action doesn't return
            in this many seconds,

            the runtime cancels it and routes to `error_next_step_id`. Null =
            use the platform

            default (30s).
        transitions:
          $ref: '#/components/schemas/ToolCallTransitions'
    TransferNode:
      type: object
      required:
        - id
        - type
      allOf:
        - $ref: '#/components/schemas/GlobalNodeFields'
      properties:
        id:
          type: string
        type:
          type: string
          enum:
            - transfer
        name:
          type: string
        position:
          $ref: '#/components/schemas/FlowNodePosition'
        transfer_to:
          type: string
          nullable: true
          example: '+972501234567'
        transfer_message:
          type: string
          nullable: true
    EndNode:
      type: object
      required:
        - id
        - type
      allOf:
        - $ref: '#/components/schemas/GlobalNodeFields'
      description: >
        Terminal. Reaching an End node hangs up the call. For per-call data
        extraction or webhook delivery, use the agent-level
        `extraction_parameters` and `webhook_url` / `webhook_events` fields on
        the agent (those apply uniformly to prompt + flow agents).
      properties:
        id:
          type: string
        type:
          type: string
          enum:
            - end
        name:
          type: string
        position:
          $ref: '#/components/schemas/FlowNodePosition'
        farewell:
          type: string
          nullable: true
        is_literal:
          type: boolean
          default: false
    FlowNodePosition:
      type: object
      description: >-
        UI-only persisted x/y on the React Flow canvas. The runtime ignores
        this.
      properties:
        x:
          type: number
        'y':
          type: number
    GlobalNodeFields:
      type: object
      description: >
        Optional fields available on every node EXCEPT start and tool_call. When
        `is_global`

        is true, the node is reachable from any conversation node without an
        explicit edge —

        the eval LLM gets it as an additional candidate transition with a strong
        "prefer

        labeled transitions" bias. Use sparingly (recommended ≤3 globals per
        flow). Good for

        misclassification recovery and universal escape hatches; bad for
        happy-path shortcuts.
      properties:
        is_global:
          type: boolean
          default: false
          description: >-
            Mark this node as globally reachable. Only valid on
            conversation/end/transfer nodes.
        global_jump_description:
          type: string
          nullable: true
          description: >
            REQUIRED when is_global=true. Natural-language condition the eval
            LLM uses to decide whether

            to jump here. Describe a clear user-side signal, not an agent
            intent.

            Example: "User reveals they're actually an owner, not a tenant".
    Transition:
      type: object
      required:
        - id
        - label
        - description
        - next_step_id
      description: >
        A labeled outgoing branch from a conversation node. The voice agent
        picks one

        (or stays) at each user-turn boundary by calling the `pick_transition`
        tool.
      properties:
        id:
          type: string
          description: Stable transition id (used in the flow graph and traces)
        label:
          type: string
          description: >-
            Short human-readable label, also surfaced in the dashboard and call
            traces
        description:
          type: string
          minLength: 1
          description: >
            REQUIRED, non-empty. The natural-language trigger the voice agent
            reads at runtime

            to decide whether to take this path. Describe a user-side signal
            (what the caller

            said or implied), not an agent intent. Example: "Caller confirmed
            they want a demo

            scheduled" — not "agent books the demo". An empty description is
            rejected with

            error code `transition_description_missing`.
        next_step_id:
          type: string
          description: Destination node id
    ToolCallTransitions:
      type: object
      description: >
        Routing for a tool_call node. Routing is deterministic, no LLM. error
        fires on hard failures

        (timeout, 4xx/5xx, integration disconnected). Otherwise custom[] is
        evaluated in declaration order

        and the first match wins; success fires only if no custom matched.
        Exactly one out-edge per fire.
      properties:
        success_next_step_id:
          type: string
          nullable: true
        error_next_step_id:
          type: string
          nullable: true
        custom:
          type: array
          default: []
          items:
            $ref: '#/components/schemas/CustomTransition'
    ArgValue:
      description: >
        Discriminated union for one entry in
        `IntegrationCallNode.args_template`. Each arg can be one of

        three shapes:

          1. Bare string — shorthand for `{mode: 'literal', value: '<the string>'}`.
          2. `{mode: 'literal',    value}` — sent as-is.
          3. `{mode: 'ai_extract', description}` — the live agent runtime extracts this arg from the
             conversation right before the action fires; `description` guides extraction.

        Both `value` and `description` strings may contain mustache tokens that
        are interpolated at

        dispatch time:

          - `{{<node_id>.<arg_name>}}` — resolves to the value an earlier node AI-extracted from
            the conversation. Both `integration_call` AND `tool_call` source nodes are addressable.
            For `integration_call` sources the referenced arg must be declared in `ai_extract` mode
            in that node's `args_template` (validated at save). For `tool_call` sources the arg
            name must match an entry in the linked tool's `config.payload_config.extraction_parameters`
            — these aren't double-checked at save time because they live on the tool config (not the
            flow_config), so a typo renders to empty string at runtime; design an `error` branch
            on the downstream node. Refs to nodes that don't exist raise `args_template_dangling_reference`.
          - `{{metadata.<key>}}` — resolves against per-call metadata. Built-in keys: `id`,
            `direction`, `agent_number`, `user_number`, `agent_name`. User-defined keys come from
            the `metadata` dict passed to `POST /calls`; declare expected custom keys via
            `FlowConfig.metadata.custom_metadata_keys`. Missing metadata keys resolve to an empty
            string at runtime — NOT a save-time error.
      oneOf:
        - type: string
          description: >-
            Literal-mode shorthand. May contain `{{node.arg}}` /
            `{{metadata.key}}` tokens.
        - type: object
          required:
            - mode
            - value
          properties:
            mode:
              type: string
              enum:
                - literal
            value:
              type: string
              description: >-
                Literal value. May contain `{{node.arg}}` / `{{metadata.key}}`
                tokens.
        - type: object
          required:
            - mode
            - description
          properties:
            mode:
              type: string
              enum:
                - ai_extract
            description:
              type: string
              description: >-
                Natural-language hint the runtime uses to bind this slot from
                conversation. May contain `{{node.arg}}` / `{{metadata.key}}`
                tokens to splice prior context into the extraction prompt.
    CustomTransition:
      type: object
      required:
        - id
        - label
        - jsonpath
        - equals
        - next_step_id
      description: >
        Deterministic branch for a tool-call node, matched by JSONPath equality
        against the tool's parsed response body.

        Custom branches are evaluated top-to-bottom, first match wins; mutually
        exclusive with success/error (only one

        out-edge fires per tool fire). See
        /docs/api-reference/agents/flow-config-schema for the full runtime
        semantics.
      properties:
        id:
          type: string
        label:
          type: string
        jsonpath:
          type: string
          example: $.status
          description: >
            Dotted path into the tool's parsed response body (root `$`).
            Supported: `$.foo.bar`, `$.list[0].name`,

            `$.items[2]`. NOT supported: recursive descent (`$..`), wildcards
            (`$.*`), filter expressions.
        equals:
          type: string
          example: no_availability
          description: >
            Compared via string equality after JSON-style stringification of the
            extracted value:

            booleans become "true"/"false" (lowercase), null becomes "null",
            numbers via str(). Match the

            stringified form exactly or the branch never fires.
        next_step_id:
          type: string
  securitySchemes:
    apiKey:
      type: http
      scheme: bearer
      bearerFormat: API Key
      description: >-
        Your Yappr API key (e.g. `ypr_live_...`). Generate one in the dashboard
        under **Settings → API Keys**.

````