openapi: 3.1.0
info:
  title: DM Champ API
  version: '1.0.0'
  description: |
    API documentation for DM Champ Cloud Functions.
    Protected endpoints are authenticated with your DM Champ API key, sent in any one of these ways:
    - 'apiKey' query parameter
    - 'X-API-Key' request header
    - 'Authorization: Bearer <your_api_key>' request header

    Example requests:
    - Query parameter: https://api.dmchamp.com/v1/contacts?apiKey=your_api_key
    - Header: curl https://api.dmchamp.com/v1/contacts -H 'X-API-Key: your_api_key'
    - Bearer: curl https://api.dmchamp.com/v1/contacts -H 'Authorization: Bearer your_api_key'

servers:
  - url: https://api.dmchamp.com/v1
    description: Production API Server

tags:
  - name: Contacts
    description: |
      Create, retrieve, and update contacts. Each contact represents a person
      your account has messaged or imported. Contacts are identified by phone
      number and channel (WhatsApp, SMS, etc.).
  - name: Tags
    description: |
      Tags let you label and segment contacts. Apply tags manually or
      automatically via the AI bot. Use tags to filter contacts, trigger
      campaigns, and drive automations.
  - name: Lists
    description: |
      Lists are named groups of contacts used for campaign targeting and
      bulk imports. A contact can belong to multiple lists.
  - name: Messages
    description: |
      Send outbound messages to contacts across WhatsApp, SMS, and other
      channels. Message delivery status is tracked asynchronously via webhooks.
  - name: Chat Sessions
    description: |
      Chat sessions group messages between your account and a contact into
      discrete conversations. Use sessions to retrieve conversation history
      and manage conversation state.
  - name: Chat Exports
    description: |
      Export full conversation histories for a contact as structured data.
      Useful for CRM sync, compliance archiving, and support handoffs.
  - name: Summaries
    description: |
      AI-generated summaries of contact conversations. Summaries are produced
      automatically after each session and capture key topics, decisions, and
      action items.
  - name: Chat Widget
    description: |
      Manage the embedded chat widget that lets website visitors start
      conversations. Configure appearance, behaviour, and the campaign the
      widget routes incoming chats to.
  - name: WhatsApp Widget
    description: |
      Configure the WhatsApp click-to-chat widget for your website. Generates
      a floating button that opens a pre-filled WhatsApp conversation.
  - name: WhatsApp Templates
    description: |
      WhatsApp Business message templates must be pre-approved by Meta before
      use in outbound campaigns. These endpoints let you create, update, and
      retrieve templates and their approval status.
  - name: WhatsApp Web
    description: |
      Send messages through a WhatsApp Web (personal number) connection.
      WhatsApp Web connections use a QR-code login rather than the Business API
      and do not require Meta approval for messaging.
  - name: Webhooks
    description: |
      Register webhook endpoints to receive real-time event notifications —
      new messages, contact updates, tag changes, and more. Events are sent
      as HTTP POST requests to your registered URL.
  - name: Users
    description: |
      Retrieve account-level information for the authenticated user, including
      subscription details, credit balance, and usage limits.
  - name: Team
    description: |
      Add people to your account and decide what each of them can do: invite or
      create teammates, set their role (admin, editor, viewer) and per-area
      permissions, limit which contacts they see, and suspend or remove them.
      Also covers the sign-in tokens used to switch into an account you have
      access to, and back out again.

      These endpoints are signed in as a PERSON, not with an API key: send
      Authorization: Bearer <Firebase ID token>. An API key gets a 401, because
      every one of these routes decides what to do based on who you are signed in
      as.
  - name: SubAccounts
    description: |
      Manage client sub-accounts under an Agency plan. Create sub-accounts,
      transfer credits, and retrieve usage across your agency.

      Acting on behalf of a sub-account: authenticate with your agency API key
      and add a sub_account_id parameter (query for GET/DELETE, JSON body for
      POST/PUT/PATCH) to target one of your managed sub-accounts. It is accepted
      on essentially every resource endpoint (campaigns, contacts, FAQs,
      knowledge-base sources, agents, channels, phone numbers, webhooks, and
      more), so one integration can provision and run a client's entire setup.
      Omit it and the call acts on your agency account. The platform verifies
      the sub-account belongs to you and returns 404 otherwise. It is not
      accepted on agency-level or self-addressed endpoints (the SubAccounts and
      API-key endpoints, agency usage, and inbound message webhooks).
  - name: Billing
    description: |
      Your own DM Champ subscription. Change which plan you are on without
      starting a second subscription — the change is applied to your existing
      subscription and prorated by Stripe.
  - name: Snapshots
    description: |
      Snapshots are reusable templates: a frozen bundle of AI Agents plus their
      knowledge (FAQs, knowledge-base sources and files), their tools (custom
      functions, MCP servers) and their media library, captured from your agency
      account and installed onto client accounts.

      Install one on demand with POST /snapshots/{snapshotId}/apply, or set a
      default with PUT /snapshots/default so every new sub-account you provision
      — via the API, the dashboard, or your own checkout link — is born with it
      already installed.

      Agency accounts only. Like the SubAccounts endpoints, these are
      agency-level: the target account is named in the path or body, not via the
      sub_account_id parameter.
  - name: Agency
    description: |
      Agency-level reporting and credit usage exports. Only available to
      master-tenant accounts.
  - name: Agency Credit Price
    description: |
      The price you charge your clients for one credit on a custom top-up, plus
      the short note shown under it. Its own tag so an API key can be limited to
      the price alone.
  - name: Widgets
    description: |
      Retrieve configuration for embedded widgets (chat widget, WhatsApp
      widget) by their public config ID. Used by the widget embed script.
  - name: System
    description: |
      Health check and system status endpoints. Use these to verify
      connectivity and confirm the API is reachable before making other calls.

  - name: Analytics
    description: |
      Read-only reporting endpoints. Retrieve daily message-event statistics
      (sent, delivered, read, replied, booked), credit usage with breakdowns by
      reason and campaign, and the per-day AI cost rollup for your account.
      All endpoints accept an optional date range (from/to, format YYYY-MM-DD)
      that defaults to the last 30 days and is capped at 366 days.
  - name: API Keys
    description: |
      Self-service management of the API key used to authenticate with this API.
      Inspect a masked preview of the active key, rotate it when you suspect it
      leaked, check your current rate-limit usage, and revoke API access
      instantly. Each account has a single API key.
  - name: Appointments
    description: |
      Book, view, reschedule, cancel and delete appointments.

      Appointments link a contact to one of your bookable event types. Booking through the API
      runs the same conflict check as in-app booking, so a time slot can never be double-booked.
      When a Google Calendar connection is active, new and changed appointments are synced
      automatically in the background - the google_calendar_event_id field on each appointment
      shows the linked calendar event once the sync has completed (it may be null for a short
      moment right after booking).
  - name: Events
    description: |
      Event types are the bookable things on your calendar: a 30 minute intro call, a treatment
      room, a table for two. Each one carries its own weekly availability, appointment length,
      notice period, rooms and meeting modality (in person, Google Meet, a link you provide).

      Every appointment is booked against an event type, so start here: list your event types to
      find the event_id that POST /appointments requires.
  - name: Campaigns
    description: |
      Campaigns define how the AI assistant engages your contacts. Each
      campaign bundles a bot configuration (instructions, rules, goal,
      availability schedule), the channels it runs on, follow-up behaviour,
      and the contacts or keywords that trigger it. Use these endpoints to
      create, update, duplicate, enable, archive, and inspect campaigns.
  - name: Phone Numbers
    description: |
      The phone numbers and messaging identities connected to your account:
      purchased WhatsApp numbers, WhatsApp Web numbers, Telegram accounts,
      LINE official accounts, and bring-your-own SMS numbers. List them,
      inspect a single record, update its display settings, and check its
      live connection status. Credentials and billing internals are never
      returned by these endpoints.
  - name: Channels
    description: |
      A unified view of every messaging channel on your account (WhatsApp,
      WhatsApp Web, SMS, Telegram, LINE, Instagram, Messenger) plus control
      over incoming-campaign routing - which campaign automatically answers
      new, unknown contacts on each channel.
  - name: FAQs
    description: |
      Manage the question-and-answer entries your AI assistant uses to answer common customer questions.
      FAQs belong to your account and can be linked to one or more campaigns, so the same answer can be reused everywhere it is relevant.
  - name: Knowledge Base
    description: |
      Everything your AI assistant knows that did not come from a hand-written FAQ: pages it read
      from a website, and documents you uploaded. Each import is called a knowledge-base source -
      the platform reads it, splits it into sections, and turns those sections into the FAQs the
      assistant searches while it replies.

      Attaching a source to an agent is a ONE-CALL job: pass autoLinkToAgentId on any ingest
      endpoint (single page, uploaded file, or bulk import) and the source - plus the FAQs it
      produces - lands on that agent's knowledge base with no follow-up linking step. This is how
      you point an agent at a website or a document.

      Imports run in the background. The ingest endpoints answer 202 with a source id; poll
      GET /kb-sources/{sourceId} until its status leaves queued/processing. Whole-website work has
      its own pair: discover the pages first, then import the ones you want.

      KB groups are named bundles of FAQs. Build a bundle once, then apply it to an agent (or, on a
      legacy setup, a campaign) in a single call.
  - name: Tasks
    description: |
      Create and manage tasks on your CRM task board. Tasks can be linked to contacts, deals, and campaigns,
      organized into board stages (columns), and given types, priorities, due dates, and assignees.
      These endpoints also let you read and configure the board itself: the stage (column) layout and the
      available task types.
  - name: LINE Channel
    description: Connect and manage a LINE Official Account. The Official Account is connected by submitting its Messaging API channel secret and a long-lived channel access token; the credentials are validated against the provider and stored. There is no browser redirect and no polling step, so connecting completes in a single request. After connecting, configure the returned webhook URL in the provider console so inbound events reach the platform.
  - name: Instagram & Messenger Connection
    description: "Connect an Instagram Business and Facebook Messenger presence to an account. The consent step is a browser redirect: the connect endpoint returns a URL the end user must open. After consent the available pages load asynchronously; poll for status, then select the page to finish. Instagram and Messenger are connected together through a single page selection."
  - name: Telegram Connection
    description: "Connect, verify, monitor, and disconnect a personal-account messaging channel. The flow is multi-step: start a session, submit the one-time login code, optionally submit a two-factor password, then poll status until the channel reports connected."
  - name: Instagram Personal Account Connection
    description: "Connect, verify, monitor, and disconnect a personal/creator Instagram account through the unofficial private API. The flow is multi-step: start with the account username and password, submit a two-factor code and/or a checkpoint confirmation code if Instagram asks for one, then poll status until the channel reports connected. A hosted credential page is also returned so the account holder can enter their own login details."
  - name: Instagram Login Connection
    description: "Connect an Instagram Business or Creator account by pushing an access token your OWN Meta app already obtained, instead of going through a DM Champ consent redirect. Built for integrators running their own Meta app with the Instagram API with Instagram Login product: you authorize the account on your app, then send us the resulting token and the numeric Instagram account ID. The token is validated live against Instagram, the account is subscribed to your app's webhooks, and the response returns the webhook URL to configure in your app. Tokens are never refreshed for you - push a fresh one before the previous one expires."
  - name: WhatsApp Business Connection
    description: "Connect a phone number to WhatsApp Business, check its connection status, and disconnect it. Connecting is browser-mediated: the connect call returns a consent URL that the account holder must open in a browser to authorize the number; the connection then completes automatically in the background, and the status endpoint is polled until the number reports ONLINE."
  - name: WhatsApp Web Connections
    description: "Connect, pair, and disconnect a WhatsApp number through a scannable QR code. Starting a connection returns a session you poll: fetch the QR code, render it for the end user to scan with their phone, then poll the status until the connection reports connected."
  - name: Agents
    description: |
      AI Agents are the assistants that reply to your contacts. An agent carries
      its own configuration - instructions, rules, goal, personality, active
      hours, AI tier - plus the knowledge, tools and media it may use while it
      talks to someone.

      Use these endpoints to create agents, read and update their configuration,
      pause and resume them, attach the custom functions they are allowed to
      call, and manage the media they are allowed to send.
  - name: Entry Points
    description: |
      Entry points decide which agent answers a conversation. A channel default
      says who takes every new contact on a channel; keyword, comment and
      follower rules hand specific conversations to a specific agent.

      Start with GET /entry-points/routing-status to see whether these rules
      decide anything on the account yet, then use
      PUT /entry-points/channel-defaults for the common case of "this agent
      answers this channel".
  - name: Custom Functions
    description: |
      Custom functions are actions the AI can take mid-conversation by calling
      an endpoint of yours - check stock, look up an order status, create a
      record in your CRM. Each function describes when it should be used, the
      URL and method to call, the headers to send, and the inputs the AI fills
      in from the conversation.

      Create a function here, test it, then attach it to the agents that are
      allowed to use it with POST /agents/{agentId}/custom-functions.
  - name: Error Log
    description: |
      Every failed action on the account in one chronological list - failed
      sends, replies the AI skipped, channel and billing alerts, and unhealthy
      webhooks. Each row carries a stable code (for example TW-63016,
      META-131047, WAW-463, APP-1101) that the help documentation explains
      together with the fix.
  - name: Account Config
    description: |
      Provider and integration settings that live on the account itself rather
      than on a single channel: your own Twilio SMS credentials, your own Meta
      app for Instagram and Messenger connections, the outbound webhook used by
      custom channels, WhatsApp Web sending limits and proxy locations, and the
      monthly bring-your-own-key spending limit an agency sets on a sub-account.

      Credentials sent to these endpoints are stored encrypted and are never
      returned again. Reads give you back identifiers and setup material only.
  - name: Broadcasts
    description: |
      A broadcast is one outbound send: an audience (a contact list or an
      explicit set of contacts), one opening message, exactly one channel, an
      optional schedule, and an optional AI agent that handles the replies.
      Broadcasts replace outgoing campaigns and carry their own send lifecycle:
      Draft, Pending Approval, Scheduled, Sending, Paused, Sent, Failed.

      These endpoints cover the whole lifecycle. Create a draft, attach the
      audience and the opening message, submit or select the WhatsApp template
      when the broadcast goes out on WhatsApp Business, estimate what the send
      will cost, then launch it. Launch validates everything up front and
      refuses with a clear message rather than silently skipping contacts.
      While a broadcast is running you can pause it, resume it, or waive the
      low-engagement safety pause, and you can duplicate any broadcast onto a
      fresh draft (optionally on another channel).

      A broadcast can also be a synced mirror of a classic campaign. Those rows
      carry a source_campaign_id and are edited, launched, and deleted through
      the campaign they came from; the endpoints below refuse the operations
      that belong to the campaign engine.
  - name: Deals
    description: |
      Create and manage the deals on your sales pipeline. A deal is one opportunity: it sits in a
      single pipeline stage (column), holds a value, and can be linked to a contact.
      Stages come from your account's own pipeline settings, so a deal can only be created in, or
      moved to, a stage you have configured - an unknown stage is rejected rather than saved into a
      column that does not exist.
      These endpoints are the write side of the pipeline board: create a deal, edit it, delete it,
      drag it to another stage or position, and set the exact order of a whole column.
  - name: MCP Servers
    description: |
      Register your own MCP (Model Context Protocol) servers so your AI agents can call their tools
      mid-conversation, the same way they call a custom function. A server is registered once per
      account and then attached to whichever agents should use it (see POST
      /agents/{agentId}/mcp-servers) - registering it here does not by itself give any agent access.
      Registering a server saves it even if the endpoint cannot be reached yet, so you can register
      first and fix connectivity afterwards; the platform tries to connect immediately and caches the
      tool list it discovers, and records what went wrong in last_error when it can't.
      Two auth methods are supported: a static header (for example an API key or a Basic credential
      pasted straight in), or OAuth 2.1, which is authorized separately through the dashboard after
      creation - these endpoints only register the OAuth client, they do not carry out the
      authorization redirect. Whichever you use, no secret is ever returned by these endpoints once
      saved - auth_header_value_set and oauth_connected tell you a credential is on file without
      exposing it.
      Per-tool controls let you scope what an attached agent can actually do: enabled_tools is an
      allow-list by tool name, and tool_policies caps how often, or whether at all, a specific tool
      may fire.
  - name: Meta Templates
    description: |
      WhatsApp message templates on the "own Meta app" (Cloud API) lane — for
      accounts sending WhatsApp with their own Meta developer app and WhatsApp
      Business Account rather than through DM Champ's managed Twilio lane.

      Templates here live directly in the account's own WABA and are managed
      through Meta's Graph API — nothing is stored on our side. Requires a
      connected own Meta app (Settings → Channels → WhatsApp Cloud API) and a
      plan that includes WhatsApp templates. This is the Cloud API sibling of
      /whatsapp-templates, which manages templates on the managed Twilio lane.
  - name: Notifications
    description: |
      Settle rows in your account's in-app notification feed - a paused-contact alert, a human-alert,
      a read receipt, a do-not-disturb flip, a channel or billing warning, and more. The feed itself
      is read over the real-time relay rather than a REST endpoint; these endpoints are the write
      side: mark one notification read, mark everything read, mark every unread notification about
      one contact read in a single call, or dismiss (permanently delete) a notification.
      A conversation typically produces several independent notifications from several producers, so
      opening that contact's chat - or clicking any one of its rows - should settle the whole set;
      that is what the read-by-contact endpoint is for.
  - name: Scheduled Tasks
    description: |
      Contact-level scheduled-task actions that sit outside the campaign and contact endpoints: force
      a scheduled task to run right now, generate the batch schedule that drives a drip campaign's
      send cadence, and schedule or cancel a one-shot manual follow-up.
      A manual follow-up is scheduled from a natural-language instruction (for example "follow up in
      2 days saying hey just checking in") - the instruction is parsed by AI into a delay and a
      message before the task is created, so a vague or unparseable instruction is rejected with a
      clear error rather than silently scheduling the wrong thing.
  - name: Short Links
    description: |
      WhatsApp short links: a single short URL that routes a visitor's click into a wa.me
      conversation with whichever of your connected WhatsApp numbers currently has the most daily
      quota remaining (the platform's live WA Web usage counter decides, not a per-link cap).
      Every phone number you list on a short link must already be connected on your account and be
      WhatsApp-reachable - either a WhatsApp Web connection, or a Twilio number with a provisioned
      and ONLINE WhatsApp Business API sender. These endpoints cover the write side only (create,
      update, delete); the public redirect itself is served at GET /l/{slug} with no authentication.
  - name: White Label
    description: |
      Manage your agency's white-label storefronts. A white-label config brands
      an entire domain — logo, colours, fonts, the auth pages, transactional
      emails, and the PWA manifest — as your own instead of DM Champ's.

      Agency accounts only (Dev accounts may also manage any account's
      configs for support). The first config comes with white-labeling
      itself; a second and third config are a Champions Circle benefit unless
      your account carries a partner grant. An account can hold several
      configs at once — the ONE named by users.white_label_config is the
      account's PRIMARY config, the fallback every by-user branding resolver
      reads (emails, PWA manifest, push icons, API base resolution, checkout).
security:
  - ApiKeyAuth: []

components:
  schemas:
    Department:
      type: object
      description: >-
        A named group of team members (Sales, Customer support, HR) that can own leads
        and hand new conversations to its own people.
      properties:
        id:
          type: string
        name:
          type: string
        color:
          type: ["string", "null"]
          description: Accent colour as #rrggbb, or null.
        member_uids:
          type: array
          items:
            type: string
          description: The team members in this department. May include the account owner.
        auto_assign_enabled:
          type: boolean
          description: >-
            Whether a lead filed under this department is also handed to a person on it.
            False means the department works from a shared queue.
        routing_agents:
          type: array
          items:
            type: string
          description: >-
            New conversations handled by these AI Agents are filed under this department
            automatically. Empty means no agent rule.
        routing_channels:
          type: array
          items:
            type: string
          description: >-
            New conversations on these channels are filed under this department
            automatically. Empty means no channel rule. When both lists are set, a
            conversation has to match both.
        created_by:
          type: ["string", "null"]
    DepartmentInput:
      type: object
      description: Fields of a department. On update, only the fields you send are changed.
      required:
        - name
      properties:
        name:
          type: string
          maxLength: 60
        color:
          type: ["string", "null"]
        member_uids:
          type: array
          items:
            type: string
        auto_assign_enabled:
          type: boolean
        routing_agents:
          type: array
          items:
            type: string
        routing_channels:
          type: array
          items:
            type: string
    SnapshotCounts:
      type: object
      description: How many of each artifact a snapshot holds (or installed, on an apply response).
      properties:
        agents:
          type: integer
        faqs:
          type: integer
        kbSources:
          type: integer
          description: Knowledge-base sources (their files ride along)
        customFunctions:
          type: integer
        mcpServers:
          type: integer
        mediaItems:
          type: integer
    UsageLimits:
      type: object
      properties:
        monthly_credits:
          type: integer
          description: Total monthly credits allocated
        monthly_credits_used:
          type: integer
          description: Number of monthly credits used
        last_reset_date:
          type: string
          format: date-time
          description: Date when the credits were last reset
        notifications:
          type: ["object", "null"]
          description: Notification settings
        credits:
          type: integer
          description: Current credit balance
        roll_over_to_next_month:
          type: boolean
          description: Whether unused credits roll over to next month
    Contact:
      type: object
      description: |
        The fields you can SEND when creating a contact. Only these are real contact
        fields; anything else you put at the top level is stored as a custom field
        (see custom_fields below).

        Contacts you READ back carry more than this - the ID, timestamps, activity
        counters, the assigned agent, the current campaign, the last message, and the
        channel identifiers we captured when the person first messaged you
        (Instagram/Messenger IDs, chat widget ID, external ID). Those are set by the
        platform, not by you, which is why they are not listed here.
      required:
        - phoneNumber
      properties:
        phoneNumber:
          type: string
          description: Contact's phone number (must include valid country code)
        firstName:
          type: string
          description: Contact's first name
          default: "Unknown"
        lastName:
          type: string
          description: Contact's last name
          default: "Unknown"
        email:
          type: string
          description: Contact's email address
          format: email
          default: "unknown@unknown.com"
        is_bot_active:
          type: boolean
          description: Whether the bot is active for this contact
          default: true
        listId:
          type: string
          description: Single list ID to add the contact to upon creation
          examples: ["list123"]
        listIds:
          type: array
          items:
            type: string
          description: Multiple list IDs to add the contact to upon creation (takes precedence over listId)
          examples: [["list123", "list456"]]
        channel:
          type: ["string", "null"]
          description: >-
            Which phone-based channel the contact is reached on. One of whatsapp,
            whatsapp_web or sms. When omitted it is resolved from the account's
            connected WhatsApp sender.
        is_private:
          type: boolean
          description: >-
            Create the contact with the AI turned off for it (a private, human-only
            conversation). Accepts true/false, including the strings "true"/"false"
            that no-code tools send.
        lead_profile:
          type: ["string", "null"]
          description: Free-text lead profile note stored on the contact.
        custom_fields:
          type: object
          additionalProperties: true
          description: |
            Arbitrary key/value pairs stored on the contact. Used by WhatsApp template
            `{{variable}}` substitution and AI tag processing. You can either nest values
            under `custom_fields` or pass them as top-level keys on the request body —
            any field not in the standard contact schema is automatically treated as a
            custom field. PUT requests merge per-key, so omitted keys are preserved.

            Note: this is what happens to ANY top-level key that is not in this
            schema. It is stored as `custom_fields.<key>`, never as a real contact
            field — so a name you invent will be saved, but it will not change how the
            contact behaves.
          examples:
            - webinar_date_nh: "Thursday, May 14, 2026 at 09:00"
              webinar_url_nh: "https://webinardo.com/abc/unique-link"
    TeamPermissionOverride:
      type: object
      description: >-
        Overrides the role's default access for ONE area of the app. Everything you
        do not override keeps the role's default.
      required:
        - area
        - level
      properties:
        area:
          type: string
          enum:
            - campaigns
            - contacts
            - messages
            - appointments
            - settings
            - billing
            - team_management
            - analytics
            - phone_numbers
            - integrations
            - faqs
            - daily_summaries
        level:
          type: string
          enum: ['none', 'view', 'edit', 'full']
          description: >-
            none hides the area, view is read only, edit can create and change, full
            adds the destructive actions.
    TeamSubAccountAccess:
      type: object
      description: For agencies - which client sub-accounts this member may open.
      required:
        - mode
      properties:
        mode:
          type: string
          enum: ['all', 'selected', 'none']
          description: all = every client, selected = only the listed ones, none = no client access.
        role:
          type: string
          enum: ['admin', 'editor', 'viewer']
          description: The role the member acts as inside a client account. Defaults to editor.
        sub_account_uids:
          type: array
          items:
            type: string
          description: Only with mode "selected" - the client accounts they may open.
    TeamMemberInput:
      type: object
      required:
        - email
        - display_name
        - role
      properties:
        email:
          type: string
          format: email
          description: The teammate's email address.
        display_name:
          type: string
          description: The name shown for them in the app.
        role:
          type: string
          enum: ['admin', 'editor', 'viewer']
          description: >-
            admin can do everything except the owner's billing-level actions, editor
            can create and change things, viewer is read only.
        permission_overrides:
          type: array
          description: Per-area exceptions to the role's defaults.
          items:
            $ref: '#/components/schemas/TeamPermissionOverride'
        contact_scope:
          type: string
          enum: ['all', 'assigned']
          description: >-
            all lets them see every contact (the default). assigned limits them to
            contacts assigned to them.
        contact_scope_unassigned:
          type: boolean
          description: With contact_scope "assigned", also let them see contacts nobody owns yet.
        contact_scope_axes:
          $ref: '#/components/schemas/TeamContactScopeAxes'
        sub_account_access:
          $ref: '#/components/schemas/TeamSubAccountAccess'
    TeamInviteInput:
      type: object
      required:
        - email
        - role
      properties:
        email:
          type: string
          format: email
          description: Where to send the invitation.
        role:
          type: string
          enum: ['admin', 'editor', 'viewer']
        permission_overrides:
          type: array
          description: Per-area exceptions, applied when they accept.
          items:
            $ref: '#/components/schemas/TeamPermissionOverride'
        contact_scope:
          type: string
          enum: ['all', 'assigned']
          description: Applied when they accept.
        contact_scope_unassigned:
          type: boolean
        contact_scope_axes:
          $ref: '#/components/schemas/TeamContactScopeAxes'
        sub_account_access:
          $ref: '#/components/schemas/TeamSubAccountAccess'
    TeamContactScopeAxes:
      type: object
      description: >-
        Limits this person to named agents and/or channels, on top of contact_scope
        (both apply). Leave a list out or empty to not limit by it. Send an object
        with no lists, or null, to remove the limit entirely.
      properties:
        agents:
          type: array
          description: Agent ids. They only see chats routed to one of these agents.
          items:
            type: string
        channels:
          type: array
          description: Channel names, e.g. whatsapp, sms, instagram.
          items:
            type: string
        include_unrouted:
          type: boolean
          description: >-
            With agents set, also let them see chats that no agent handles. Off by
            default, which means older chats and ones they start themselves stay hidden.
    TeamSessionToken:
      type: object
      properties:
        success:
          type: boolean
        customToken:
          type: string
          description: >-
            A one-time sign-in token. Exchange it for a session with the Firebase
            client SDK; it is not an API key and cannot be sent as one.
    SubAccount:
      type: object
      required:
        - email
        - first_name
        - last_name
        - usage_limits
      properties:
        email:
          type: string
          description: Sub-account user's email address
          format: email
        first_name:
          type: string
          description: Sub-account user's first name
        last_name:
          type: string
          description: Sub-account user's last name
        role:
          type: string
          description: Role of the sub-account (always "Sub Account")
          default: "Sub Account"
        is_agency_sub_account:
          type: boolean
          default: true
          description: Indicates this is a sub-account
        business_name:
          type: string
          description: Business name for the sub-account
        description:
          type: string
          description: Business description
        address_line:
          type: string
          description: Business address line
        city:
          type: string
          description: Business city
        country:
          type: string
          description: Business country
        postal_code:
          type: string
          description: Business postal code
        state:
          type: string
          description: Business state
        time_zone_id:
          type: string
          description: Time zone identifier
        language:
          type: string
          description: Preferred language
        usage_limits:
          type: object
          required:
            - monthly_credits
          properties:
            monthly_credits:
              type: integer
              description: Total monthly credits allocated
              minimum: 1
            monthly_credits_used:
              type: integer
              description: Number of monthly credits used
              default: 0
            credits:
              type: integer
              description: Current credit balance
            roll_over_to_next_month:
              type: boolean
              description: Whether unused credits roll over to next month
              default: false

    Tag:
      type: object
      required:
        - name
      properties:
        name:
          type: string
          description: Tag name
        description:
          type: string
          description: Tag description
    Message:
      type: object
      properties:
        content:
          type: string
          description: Message content
        timestamp:
          type: string
          format: date-time
        sender:
          type: string
        channel:
          type: string
    ChatMessage:
      type: object
      description: A message within a chat session
      properties:
        id:
          type: string
          description: Unique message identifier
        body:
          type: string
          description: Message body/content
        direction:
          type: string
          enum: [inbound, outbound]
          description: Message direction
        timestamp:
          type: ["string", "null"]
          format: date-time
          description: Message timestamp
        type:
          type: string
          description: Message type
        channel:
          type: string
          description: Channel the message was sent through
        status:
          type: string
          description: Message delivery status
    ChatSession:
      type: object
      description: A chat session between the user and a contact
      properties:
        id:
          type: string
          description: Unique session identifier
        start_date_time:
          type: string
          format: date-time
          description: Session start timestamp (ISO 8601)
        end_date_time:
          type: string
          format: date-time
          description: Session end timestamp (ISO 8601)
        status:
          type: string
          enum: [ChatSessionOpened, ChatSessionClosed]
          description: Current session status
        tag:
          type: ["string", "null"]
          description: Tag associated with this session
        messages:
          type: array
          description: Session messages. Only present when includeMessages=true
          items:
            $ref: '#/components/schemas/ChatMessage'
    RecentChatSession:
      type: object
      description: A recent chat session with contact details
      properties:
        session_id:
          type: string
          description: Unique session identifier
        contact_id:
          type: string
          description: Contact identifier
        contact_name:
          type: string
          description: Contact full name
        contact_phone:
          type: string
          description: Contact phone number
        contact_email:
          type: string
          description: Contact email address
        start_date_time:
          type: string
          format: date-time
          description: Session start timestamp (ISO 8601)
        end_date_time:
          type: string
          format: date-time
          description: Session end timestamp (ISO 8601)
        status:
          type: string
          enum: [ChatSessionOpened, ChatSessionClosed]
          description: Current session status
        tag:
          type: ["string", "null"]
          description: Tag associated with this session
        messages:
          type: array
          description: Session messages. Only present when includeMessages=true
          items:
            $ref: '#/components/schemas/ChatMessage'
    ChatExport:
      type: object
      description: Chat export data for a single contact
      properties:
        contactId:
          type: string
          description: Contact identifier
        contactName:
          type: string
          description: Contact full name
        phoneNumber:
          type: string
          description: Contact phone number
        email:
          type: string
          description: Contact email address
        messageCount:
          type: integer
          description: Number of messages in the export
        chatExport:
          type: string
          description: Full chat export as formatted text
    WebhookAutoRechargePayload:
      type: object
      description: |
        Payload sent by DM Champ to the agency's configured auto-recharge webhook URL
        when a sub-account's credit balance drops below the configured threshold.

        The agency's server should process payment externally and then call
        `POST /subaccounts/credits` to grant credits back to the sub-account.
      required:
        - event
        - sub_account_id
        - sub_account_email
        - agency_id
        - credits_requested
        - current_balance
        - threshold
        - timestamp
        - idempotency_key
      properties:
        event:
          type: string
          enum: ["agency_sub_account_auto_recharge"]
          description: Event type identifier (always "agency_sub_account_auto_recharge")
        sub_account_id:
          type: string
          description: Firebase UID of the sub-account that needs credits
          examples: ["abc123def456"]
        sub_account_email:
          type: string
          format: email
          description: Email address of the sub-account
          examples: ["sub@example.com"]
        sub_account_name:
          type: string
          description: Display name of the sub-account (first + last name)
          examples: ["John Doe"]
        agency_id:
          type: string
          description: Firebase UID of the agency that owns this sub-account
          examples: ["fHjQASIIzfUcrvuAvaYWdDd65Bu2"]
        credits_requested:
          type: integer
          description: Number of credits the sub-account is configured to receive on auto-recharge
          examples: [500]
        current_balance:
          type: integer
          description: Sub-account's credit balance at the time the webhook fires
          examples: [42]
        threshold:
          type: integer
          description: The "when_below" threshold that triggered this auto-recharge
          examples: [100]
        price_per_credit_cents:
          type: integer
          description: Agency's configured price per credit in smallest currency unit. Only present if the agency has per-credit pricing configured.
          examples: [10]
        price_per_credit_currency:
          type: string
          description: ISO 4217 currency code for the per-credit price. Only present if price_per_credit_cents is set.
          examples: ["usd"]
        total_amount_cents:
          type: integer
          description: Computed total (credits_requested * price_per_credit_cents). Only present if price_per_credit_cents is set.
          examples: [5000]
        timestamp:
          type: string
          format: date-time
          description: ISO 8601 timestamp of when the webhook was sent
          examples: ["2026-04-13T12:00:00.000Z"]
        idempotency_key:
          type: string
          description: |
            Unique key for deduplication, formatted as `auto_recharge_<sub_account_id>_<timestamp_ms>`.
            Use this to prevent processing duplicate webhook deliveries.
          examples: ["auto_recharge_abc123def456_1712836800000"]
    AnalyticsEventCounts:
      type: object
      description: Aggregated message-event counters.
      properties:
        total:
          type: integer
          description: Total number of recorded events.
        sent:
          type: integer
          description: Messages sent.
        delivered:
          type: integer
          description: Messages confirmed delivered.
        read:
          type: integer
          description: Messages confirmed read.
        replied:
          type: integer
          description: Replies received from contacts.
        booked:
          type: integer
          description: Appointments booked.
        contact_created:
          type: integer
          description: New contacts created.
        credits_spent:
          type: number
          description: Credits consumed.
        credits_recharged:
          type: number
          description: Credits added through recharges.
    AnalyticsCreditRecord:
      type: object
      description: A single credit usage record.
      properties:
        id:
          type: string
          description: Unique record identifier. Use as the cursor value to paginate.
        amount:
          type: number
          description: Credits charged for this record. Zero for records billed to your own provider API key.
        timestamp:
          type: ['string', 'null']
          description: ISO 8601 timestamp of the charge.
        reason:
          type: string
          description: What the credits were used for, for example AI Message or Campaign Message.
        is_adjustment:
          type: boolean
          description: True when the record is a balance adjustment (bonus, renewal, correction) rather than real consumption. Adjustments are excluded from all totals and breakdowns.
        campaign_id:
          type: ['string', 'null']
          description: ID of the campaign this usage belongs to, when applicable.
        campaign_name:
          type: ['string', 'null']
          description: Name of the campaign this usage belongs to, when applicable.
        agent_id:
          type: ['string', 'null']
          description: >-
            ID of the AI Agent this usage belongs to, when applicable. Null on older
            records and on account-level usage. Group by this rather than guessing
            from campaign_name.
        contact_id:
          type: ['string', 'null']
          description: ID of the related contact, when applicable.
        contact_name:
          type: ['string', 'null']
          description: Name of the related contact, when applicable.
        credit_type:
          type: ['string', 'null']
          description: Which credit bucket was charged.
        custom_keys_used:
          type: boolean
          description: True when the underlying AI call was billed to your own provider API key instead of credits.
        description:
          type: string
          description: Optional free-text note on the record.
        cost_usd:
          type: number
          description: USD cost of the underlying AI call. Only populated on records billed to your own provider API key; zero otherwise.
        input_tokens:
          type: integer
          description: Input tokens consumed. Only populated on records billed to your own provider API key.
        output_tokens:
          type: integer
          description: Output tokens produced. Only populated on records billed to your own provider API key.
        cache_read_tokens:
          type: integer
          description: Tokens read from prompt cache. Only populated on records billed to your own provider API key.
        cache_creation_tokens:
          type: integer
          description: Tokens written to prompt cache. Only populated on records billed to your own provider API key.
        ai_model:
          type: ['string', 'null']
          description: Model used for the underlying AI call, when exposed.
        request_id:
          type: ['string', 'null']
          description: Provider request identifier, when exposed.
        is_test:
          type: boolean
          description: True for records produced by playground or test runs, which are never billed.
    AnalyticsAiCostDay:
      type: object
      description: AI spend rollup for one calendar day (UTC).
      properties:
        date:
          type: string
          format: date
          description: Day in YYYY-MM-DD format.
        total_usd:
          type: number
          description: Total AI spend for the day in USD.
        byok_usd:
          type: number
          description: Portion of the spend billed to your own provider API keys.
        platform_usd:
          type: number
          description: Portion of the spend that ran on the platform rather than your own key. Only returned to accounts using their own provider key; zero when costs are redacted.
        input_usd:
          type: number
          description: Cost of input tokens.
        output_usd:
          type: number
          description: Cost of output tokens.
        cache_creation_usd:
          type: number
          description: Cost of prompt-cache writes.
        cache_read_usd:
          type: number
          description: Cost of prompt-cache reads.
        calls:
          type: integer
          description: Number of AI calls made that day.
        by_provider:
          type: object
          additionalProperties:
            type: number
          description: USD spend keyed by AI provider name.
    AgencySubAccountUsageSummary:
      type: object
      description: Aggregated credit usage for one sub-account over the requested range.
      properties:
        sub_account_id:
          type: string
          description: ID of the sub-account.
        sub_account_name:
          type: string
          description: Display name of the sub-account.
        sub_account_email:
          type: string
          description: Email of the sub-account.
        total_credits_used:
          type: number
          description: Credits consumed by the sub-account. Balance adjustments (bonuses, renewals, corrections) are excluded.
        total_cost_usd:
          type: number
          description: USD cost of AI calls billed to provider API keys for this sub-account.
        total_input_tokens:
          type: integer
          description: Input tokens consumed on key-billed calls.
        total_output_tokens:
          type: integer
          description: Output tokens produced on key-billed calls.
        total_cache_read_tokens:
          type: integer
          description: Prompt-cache read tokens on key-billed calls.
        total_cache_creation_tokens:
          type: integer
          description: Prompt-cache write tokens on key-billed calls.
        by_reason:
          type: object
          additionalProperties:
            type: number
          description: Credits consumed keyed by usage reason.
        by_reason_cost:
          type: object
          additionalProperties:
            type: number
          description: USD cost keyed by usage reason.
        top_campaigns:
          type: array
          description: Up to five campaigns with the highest credit consumption.
          items:
            type: object
            properties:
              campaign_name:
                type: string
              credits_used:
                type: number
        top_campaigns_cost:
          type: array
          description: Up to five campaigns with the highest USD cost.
          items:
            type: object
            properties:
              campaign_name:
                type: string
              cost_usd:
                type: number
    AnalyticsApiError:
      type: object
      description: Standard error envelope returned by analytics endpoints.
      properties:
        success:
          type: boolean
          description: Always false on errors.
        error:
          type: string
          description: Human-readable error message.
    ApiKeyMetadata:
      type: object
      properties:
        success:
          type: boolean
          examples: [true]
        api_key:
          type: ['string', 'null']
          description: The full active API key, when a retrievable copy exists. Null for keys created before retrievable copies were kept - rotate once to get a key that can be shown again.
          examples: ["abcdEFGH1234ijkl5678MNOP9012qrst"]
        api_key_masked:
          type: string
          description: Masked preview of the active API key - the first 4 and last 4 characters with the middle hidden.
          examples: ["abcd...qrst"]
        created_at:
          type: ['string', 'null']
          format: date-time
          description: When the active key was generated. Null for keys created before creation timestamps were tracked.
          examples: ["2026-06-01T10:00:00.000Z"]
    ApiKeyRotateResponse:
      type: object
      properties:
        success:
          type: boolean
          examples: [true]
        api_key:
          type: string
          description: The newly generated 32-character API key. This is the only time the full key is returned - store it securely right away.
          examples: ["abcdEFGH1234ijkl5678MNOP9012qrst"]
        message:
          type: string
          description: Human-readable confirmation that the previous key is no longer valid.
          examples: ["API key rotated. The previous key is no longer valid. Store this key now - it will not be shown again."]
    ApiKeyUsage:
      type: object
      properties:
        success:
          type: boolean
          examples: [true]
        usage:
          type: object
          properties:
            limit:
              type: integer
              description: Maximum number of authenticated requests allowed per rate-limit window.
              examples: [120]
            window_seconds:
              type: integer
              description: Length of the rate-limit window in seconds.
              examples: [60]
            used:
              type: integer
              description: Requests counted against your account in the current window.
              examples: [37]
            remaining:
              type: integer
              description: Requests you can still make before the current window resets.
              examples: [83]
            window_resets_at:
              type: string
              format: date-time
              description: When the current rate-limit window ends and the counter resets.
              examples: ["2026-06-09T12:01:00.000Z"]
        note:
          type: string
          description: Only present when no requests have been recorded in the current window yet.
          examples: ["No requests have been recorded in the current rate-limit window yet."]
    ApiKeyRevokeResponse:
      type: object
      properties:
        success:
          type: boolean
          examples: [true]
        revoked:
          type: boolean
          examples: [true]
        message:
          type: string
          examples: ["API key revoked. All requests using it will be rejected immediately."]
    ApiKeyErrorResponse:
      type: object
      properties:
        success:
          type: boolean
          examples: [false]
        error:
          type: string
          examples: ["No API key found for this account"]
    Appointment:
      type: object
      description: A booked appointment between your account and a contact.
      properties:
        id:
          type: string
          description: Unique identifier of the appointment.
          examples: ["aBcD1234eFgH5678"]
        contact_id:
          type: ['string', 'null']
          description: ID of the contact the appointment is booked with.
          examples: ["contact_abc123"]
        event_id:
          type: ['string', 'null']
          description: ID of the event type (bookable slot definition) this appointment was booked on.
          examples: ["event_xyz789"]
        status:
          type: ['string', 'null']
          enum: ["Confirmed", "Canceled", null]
          description: Current status of the appointment.
          examples: ["Confirmed"]
        start_time:
          type: ['string', 'null']
          format: date-time
          description: Start of the appointment as an ISO 8601 date-time in UTC.
          examples: ["2026-06-15T10:00:00.000Z"]
        end_time:
          type: ['string', 'null']
          format: date-time
          description: End of the appointment as an ISO 8601 date-time in UTC.
          examples: ["2026-06-15T10:30:00.000Z"]
        created_at:
          type: ['string', 'null']
          format: date-time
          description: When the appointment was created.
        last_modified_at:
          type: ['string', 'null']
          format: date-time
          description: When the appointment was last changed.
        room_name:
          type: ['string', 'null']
          description: Name of the room or resource the appointment is booked in, when the event type uses rooms.
          examples: ["Room A"]
        description:
          type: ['string', 'null']
          description: Free-form description of the appointment.
        summary:
          type: ['string', 'null']
          description: Short summary or title of the appointment.
        cancelation_reason:
          type: ['string', 'null']
          description: Reason supplied when the appointment was canceled, if any.
        google_calendar_event_id:
          type: ['string', 'null']
          description: >
            ID of the linked Google Calendar event. Set automatically once calendar sync completes;
            null when no calendar is connected or while the sync is still in progress.
        calendar_synced:
          type: boolean
          description: True when the appointment is linked to a calendar event.
        imported:
          type: boolean
          description: True when the appointment was imported from an external calendar rather than booked directly.
        is_recurring:
          type: boolean
          description: True when the appointment is part of a recurring series.
        recurrence_frequency:
          type: ['string', 'null']
          description: How often the appointment repeats, when recurring.
        recurring_event_id:
          type: ['string', 'null']
          description: ID of the recurring series this appointment belongs to, when applicable.
        recurring_interval:
          type: ['integer', 'null']
          description: Interval between repetitions, when recurring.
        recurring_sequence:
          type: ['integer', 'null']
          description: Position of this appointment within its recurring series.
        end_after_x_occurrences:
          type: ['integer', 'null']
          description: Number of occurrences after which the recurring series ends.
        booking_provider:
          type: ['string', 'null']
          description: Source system the booking came from, when booked through a connected reservation provider.
    AppointmentCreateRequest:
      type: object
      required:
        - contact_id
        - event_id
        - start_time
      properties:
        contact_id:
          type: string
          description: ID of the contact to book the appointment for. Must belong to your account.
          examples: ["contact_abc123"]
        event_id:
          type: string
          description: ID of the event type (bookable slot definition) to book on. Must belong to your account.
          examples: ["event_xyz789"]
        start_time:
          type: string
          format: date-time
          description: >
            Desired start of the appointment as an ISO 8601 date-time. The end time is calculated
            automatically from the event type's slot duration.
          examples: ["2026-06-15T10:00:00.000Z"]
        room_name:
          type: string
          description: Optional room or resource name when the event type uses rooms.
          examples: ["Room A"]
    AppointmentUpdateRequest:
      type: object
      description: Fields to change on the appointment. At least one field is required.
      properties:
        start_time:
          type: string
          format: date-time
          description: New start of the appointment as an ISO 8601 date-time.
          examples: ["2026-06-16T10:00:00.000Z"]
        end_time:
          type: string
          format: date-time
          description: New end of the appointment as an ISO 8601 date-time. Must be after the start time.
          examples: ["2026-06-16T10:30:00.000Z"]
        room_name:
          type: string
          description: New room or resource name.
        description:
          type: ['string', 'null']
          description: New description, or null to clear it.
        summary:
          type: ['string', 'null']
          description: New summary, or null to clear it.
    AppointmentCancelRequest:
      type: object
      properties:
        cancellation_reason:
          type: string
          description: Optional reason for the cancellation, stored on the appointment.
          examples: ["Client asked to reschedule next month"]
    EventSummary:
      type: object
      description: |
        One of your bookable event types, as returned by GET /events.

        This is a summary row, not the full configuration: the weekly availability grid, the room
        definitions and the list of every appointment ever booked against the event are reduced to
        has_availability and room_count, so a list of event types stays small enough to read in one
        response.
      properties:
        id:
          type: string
          description: ID of the event type. This is the event_id that POST /appointments expects.
          examples: ["event_xyz789"]
        event_name:
          type: string
          description: Name of the event type. Empty on a draft that has not been named yet.
          examples: ["30 minute intro call"]
        event_type:
          type: ['string', 'null']
          description: |
            How a booking on this event type is fulfilled. null on older event types, which behave
            like in_person.
          enum: ["in_person", "google_meet", "custom_link", "zoom", null]
          examples: ["google_meet"]
        location:
          type: ['string', 'null']
          description: Free-text location shown to the person booking.
          examples: ["Amsterdam clinic"]
        slot_duration:
          type: ['integer', 'null']
          description: Length of one appointment, in minutes.
          examples: [30]
        notice:
          type: ['integer', 'null']
          description: Minimum hours of advance notice before a slot can be booked.
          examples: [4]
        booking_until_x_days:
          type: ['integer', 'null']
          description: How many days into the future bookings are accepted.
          examples: [28]
        has_availability:
          type: boolean
          description: |
            Whether any weekly availability is set, on the event type itself or on one of its rooms.
            When this is false, booking against this event type will find no free slots.
          examples: [true]
        room_count:
          type: integer
          description: How many rooms or resources this event type defines (0 when it does not use rooms).
          examples: [2]
        start_date:
          type: ['string', 'null']
          format: date-time
          description: First day bookings are accepted, or null when there is no start boundary.
          examples: ["2026-06-01T00:00:00.000Z"]
        end_date:
          type: ['string', 'null']
          format: date-time
          description: Last day bookings are accepted, or null when there is no end boundary.
        created_at:
          type: ['string', 'null']
          format: date-time
          description: ISO 8601 timestamp of when the event type was created.
          examples: ["2026-01-01T12:00:00.000Z"]
    EventReferencingAgent:
      type: object
      description: An AI Agent that uses an event type for booking.
      properties:
        id:
          type: string
          examples: ["ag7HkQ2ZpLxR3mNb"]
        name:
          type: string
          description: The agent's name. Empty when it has none.
          examples: ["Front desk assistant"]
        active:
          type: boolean
          description: Whether the agent is switched on. Only active agents block a delete.
          examples: [true]
    EventReferencingCampaign:
      type: object
      description: A campaign that uses an event type for booking.
      properties:
        id:
          type: string
          examples: ["campaign123"]
        name:
          type: string
          description: The campaign's name. Empty when it has none.
          examples: ["Inbound leads"]
    EventWeeklyAvailability:
      type: object
      description: |
        When this event type can be booked, per weekday. Each weekday holds either a single window
        or an array of windows, in 24-hour HH:MM notation. A weekday you leave out is not bookable.
      properties:
        monday:
          $ref: '#/components/schemas/EventDayAvailability'
        tuesday:
          $ref: '#/components/schemas/EventDayAvailability'
        wednesday:
          $ref: '#/components/schemas/EventDayAvailability'
        thursday:
          $ref: '#/components/schemas/EventDayAvailability'
        friday:
          $ref: '#/components/schemas/EventDayAvailability'
        saturday:
          $ref: '#/components/schemas/EventDayAvailability'
        sunday:
          $ref: '#/components/schemas/EventDayAvailability'
    EventDayAvailability:
      description: One bookable window, or several, for a single weekday.
      anyOf:
        - $ref: '#/components/schemas/EventTimeWindow'
        - type: array
          items:
            $ref: '#/components/schemas/EventTimeWindow'
    EventTimeWindow:
      type: object
      description: A bookable window within a day, in 24-hour HH:MM notation.
      properties:
        start_time:
          type: string
          description: Window start, in 24-hour HH:MM format.
          examples: ["09:00"]
        end_time:
          type: string
          description: Window end, in 24-hour HH:MM format.
          examples: ["17:00"]
    EventRoom:
      type: object
      description: |
        A room or resource that bookings on this event type are placed in - a treatment room, a
        table, a coach. Each room has its own weekly availability and may override the event's
        per-slot capacity.
      properties:
        name:
          type: string
          description: Name of the room. Defaults to an empty string.
          examples: ["Treatment room 1"]
        daily_availability:
          $ref: '#/components/schemas/EventWeeklyAvailability'
        google_calendar_id:
          type: ['string', 'null']
          description: Google Calendar this room's bookings are written to, when it has its own calendar.
        slot_capacity:
          type: ['integer', 'null']
          minimum: 1
          maximum: 10000
          description: |
            How many bookings may share one time window in this room. Leave it out to inherit the
            event's capacity; null clears an override.
          examples: [1]
        slot_capacity_unlimited:
          type: ['boolean', 'null']
          description: Set to true to let this room take unlimited bookings per window, overriding slot_capacity.
    EventWriteRequest:
      type: object
      description: |
        Fields of an event type. Everything is optional - on create, anything you leave out is
        either blank or takes the dashboard default; on update, anything you leave out is unchanged.
      properties:
        event_name:
          type: string
          description: Name of the event type. Defaults to an empty string on create.
          examples: ["30 minute intro call"]
        event_type:
          type: ['string', 'null']
          enum: ["in_person", "google_meet", "custom_link", "zoom", null]
          description: |
            How a booking is fulfilled. in_person uses the address, google_meet generates a Meet
            link per booking, custom_link reuses the meeting_link you provide, and zoom is a legacy
            option kept for existing setups. null clears the choice.
          examples: ["google_meet"]
        location:
          type: string
          description: Free-text location shown to the person booking.
          examples: ["Amsterdam clinic"]
        address:
          type: ['string', 'null']
          description: Physical address used for an in_person booking.
        meeting_link:
          type: ['string', 'null']
          description: The fixed meeting link used when event_type is custom_link.
          examples: ["https://meet.example.com/my-room"]
        slot_duration:
          type: integer
          description: Length of one appointment in minutes. Defaults to 30 on create.
          examples: [30]
        granularity:
          type: integer
          description: How many minutes apart the offered start times are. Defaults to 15 on create.
          examples: [15]
        slot_interval:
          type: ['integer', 'null']
          description: |
            How often a new appointment start is offered, in minutes, when it should differ from
            slot_duration. Smaller than slot_duration produces overlapping windows; larger values
            are reduced to slot_duration.
        notice:
          type: integer
          description: Minimum hours of advance notice before a slot can be booked. Defaults to 4 on create.
          examples: [4]
        booking_until_x_days:
          type: integer
          description: How many days into the future bookings are accepted. Defaults to 28 on create.
          examples: [28]
        buffer_before:
          type: integer
          description: Minutes to keep free before each appointment.
          examples: [0]
        buffer_after:
          type: integer
          description: Minutes to keep free after each appointment.
          examples: [0]
        start_date:
          type: ['string', 'null']
          format: date-time
          description: First day bookings are accepted. Send null to remove the boundary.
          examples: ["2026-06-01T00:00:00.000Z"]
        end_date:
          type: ['string', 'null']
          format: date-time
          description: Last day bookings are accepted. Send null to remove the boundary.
        daily_availability:
          $ref: '#/components/schemas/EventWeeklyAvailability'
        rooms:
          type: array
          description: Replaces the whole set of rooms. Cannot be combined with add_room.
          items:
            $ref: '#/components/schemas/EventRoom'
        add_room:
          allOf:
            - $ref: '#/components/schemas/EventRoom'
          description: Appends one room, leaving the existing rooms alone. Cannot be combined with rooms.
        room_assignment_strategy:
          type: ['string', 'null']
          enum: ["first_available", "round_robin", null]
          description: |
            How a booking picks a room when this event type has more than one. first_available keeps
            the classic behaviour: the first room in your list that is free takes the booking.
            round_robin spreads bookings evenly by sending each one to the least-booked room, with
            ties falling back to your stored room order. Leave it out (or send null) for
            first_available. Only takes effect with two or more rooms.
          examples: ["round_robin"]
        slot_capacity:
          type: ['integer', 'null']
          minimum: 1
          maximum: 10000
          description: |
            How many bookings may share the same time window. Leave it out (or null) for the normal
            one-booking-per-slot behaviour. A malformed value is rejected, never rounded.
          examples: [1]
        slot_capacity_unlimited:
          type: ['boolean', 'null']
          description: Set to true to accept unlimited bookings per window, overriding slot_capacity.
        is_recurring:
          type: boolean
          description: Whether bookings on this event type repeat.
        recurrence_frequency:
          type: ['string', 'null']
          description: How often a recurring booking repeats.
        recurring_interval:
          type: ['integer', 'null']
          description: How many frequency steps between repeats.
        end_after_x_occurrences:
          type: ['integer', 'null']
          description: How many occurrences a recurring booking runs for.
        sync_to_google_calendar:
          type: boolean
          description: Whether bookings on this event type are written to the connected Google Calendar.
        send_appointment_email_to_contact:
          type: boolean
          description: Whether the contact receives a confirmation email when they book.
        time_zone:
          type: ['object', 'null']
          description: Per-event timezone override. Without it, bookings use the account timezone.
        booking_page:
          type: object
          description: |
            The public booking page for this event type. Enabling it reserves the link, which is why
            these calls can come back with a 409 when the link is already taken by another event.
          properties:
            enabled:
              type: boolean
              description: Whether the public booking page is switched on.
              examples: [true]
            slug:
              type: string
              pattern: '^[a-z0-9]([a-z0-9-]{1,58})[a-z0-9]$'
              minLength: 3
              maxLength: 60
              description: |
                The link, in lowercase letters, numbers and dashes, 3 to 60 characters, not starting
                or ending with a dash. Leave it out to have one generated.
              examples: ["intro-call"]
    ActiveTimeWindow:
      type: object
      description: A single active time window within a day, in 24-hour HH and MM notation.
      properties:
        start_time:
          type: string
          description: Window start time in 24-hour HH:MM format
          examples: ["09:00"]
        end_time:
          type: string
          description: Window end time in 24-hour HH:MM format
          examples: ["17:00"]
    AvailabilitySchedule:
      type: object
      description: |
        Active hours per weekday for the campaign bot. Each weekday accepts
        either a single time window or an array of time windows. Days omitted
        from the object are left unchanged.
      properties:
        monday:
          description: Active windows for Monday (single window or array of windows)
          anyOf:
            - $ref: '#/components/schemas/ActiveTimeWindow'
            - type: array
              items:
                $ref: '#/components/schemas/ActiveTimeWindow'
        tuesday:
          description: Active windows for Tuesday (single window or array of windows)
          anyOf:
            - $ref: '#/components/schemas/ActiveTimeWindow'
            - type: array
              items:
                $ref: '#/components/schemas/ActiveTimeWindow'
        wednesday:
          description: Active windows for Wednesday (single window or array of windows)
          anyOf:
            - $ref: '#/components/schemas/ActiveTimeWindow'
            - type: array
              items:
                $ref: '#/components/schemas/ActiveTimeWindow'
        thursday:
          description: Active windows for Thursday (single window or array of windows)
          anyOf:
            - $ref: '#/components/schemas/ActiveTimeWindow'
            - type: array
              items:
                $ref: '#/components/schemas/ActiveTimeWindow'
        friday:
          description: Active windows for Friday (single window or array of windows)
          anyOf:
            - $ref: '#/components/schemas/ActiveTimeWindow'
            - type: array
              items:
                $ref: '#/components/schemas/ActiveTimeWindow'
        saturday:
          description: Active windows for Saturday (single window or array of windows)
          anyOf:
            - $ref: '#/components/schemas/ActiveTimeWindow'
            - type: array
              items:
                $ref: '#/components/schemas/ActiveTimeWindow'
        sunday:
          description: Active windows for Sunday (single window or array of windows)
          anyOf:
            - $ref: '#/components/schemas/ActiveTimeWindow'
            - type: array
              items:
                $ref: '#/components/schemas/ActiveTimeWindow'
    CampaignBotConfig:
      type: object
      description: |
        Bot configuration for a campaign. All fields are optional; only the
        fields you send are updated, sibling fields are preserved. Additional
        bot fields beyond the ones documented here are accepted and stored
        as-is.
      additionalProperties: true
      properties:
        instructions:
          type: string
          description: Primary instructions that steer how the bot talks to contacts
        rules:
          type: string
          description: Hard rules the bot must always follow
        goal:
          type: string
          description: The outcome the bot should work towards in each conversation
        personality:
          type: string
          description: Tone-of-voice and personality description for the bot
        availability:
          $ref: '#/components/schemas/AvailabilitySchedule'
        ai_speed:
          type: string
          enum: [fast, fast_thinker, balanced, thorough]
          description: Controls how much reasoning the AI applies before replying
        anthropic_model:
          type: string
          enum: [standard, economy, max]
          description: AI quality tier used for this campaign's replies
        max_messages:
          type: integer
          description: Maximum number of bot messages per conversation
        alert_human_when:
          type: string
          description: Conditions under which the bot should alert a human teammate
        follow_up_config:
          type: object
          description: Follow-up behaviour configuration, stored as provided
          additionalProperties: true
    CampaignSummary:
      type: object
      description: Compact campaign representation returned by the list endpoint.
      properties:
        id:
          type: string
          description: Campaign ID
          examples: ["NBCXrhqGPSFsd6MV7pRo"]
        name:
          type: ['string', 'null']
          description: Campaign name
          examples: ["Inbound WhatsApp Leads"]
        type:
          type: ['string', 'null']
          enum: ['Incoming from Unknown Contacts', 'Outgoing', 'Keywords', 'Combined', null]
          description: Campaign type.
        status:
          type: ['string', 'null']
          enum: ['Draft', 'Pending Approval', 'Scheduled', 'Live', 'Paused', 'Completed', 'Completed with errors', 'Sent', 'Failed', null]
          description: Lifecycle status. An always-on incoming campaign must be "Live" to receive and reply (e.g. comment monitoring). Outgoing campaigns move Draft → Scheduled/Live → Completed.
        enabled:
          type: ['boolean', 'null']
          description: Whether the campaign is currently enabled
        archived:
          type: boolean
          description: Whether the campaign is archived
        created_at:
          type: ['integer', 'null']
          description: Creation time as epoch milliseconds
          examples: [1700000000000]
        ai_mode:
          type: ['boolean', 'null']
          description: Whether AI mode is on for this campaign
        language:
          type: ['string', 'null']
          description: Language the bot replies in
          examples: ["en"]
        enabled_channels:
          type: array
          description: Channels this campaign runs on
          items:
            type: string
          examples: [["whatsapp", "instagram"]]
    CampaignDetail:
      type: object
      description: |
        Full campaign document. Document reference fields are returned as
        path strings and timestamps as epoch milliseconds. Additional fields
        beyond the ones documented here may be present depending on the
        campaign's configuration.
      additionalProperties: true
      properties:
        id:
          type: string
          description: Campaign ID
        name:
          type: string
          description: Campaign name
        type:
          type: string
          enum: ['Incoming from Unknown Contacts', 'Outgoing', 'Keywords', 'Combined']
          description: Campaign type.
        status:
          type: string
          enum: ['Draft', 'Pending Approval', 'Scheduled', 'Live', 'Paused', 'Completed', 'Completed with errors', 'Sent', 'Failed']
          description: Lifecycle status. An always-on incoming campaign must be "Live" to receive and reply (e.g. comment monitoring). Outgoing campaigns move Draft → Scheduled/Live → Completed.
        language:
          type: string
          description: Language the bot replies in
        ai_mode:
          type: boolean
          description: Whether AI mode is on for this campaign
        archived:
          type: boolean
          description: Whether the campaign is archived
        enabled:
          type: boolean
          description: Whether the campaign is currently enabled
        created_at:
          type: integer
          description: Creation time as epoch milliseconds
        enabled_channels:
          type: array
          description: Channels this campaign runs on
          items:
            type: string
        keywords:
          type: array
          description: Trigger keywords (Keywords campaigns)
          items:
            type: string
        bot:
          description: The campaign's live bot configuration
          anyOf:
            - $ref: '#/components/schemas/CampaignBotConfig'
            - type: 'null'
        follow_up_config:
          type: ['object', 'null']
          description: Follow-up behaviour configuration
          additionalProperties: true
    CampaignCustomFunction:
      type: object
      description: A custom function (external HTTP action) the bot can call during conversations for this campaign.
      additionalProperties: true
      properties:
        id:
          type: string
          description: Custom function ID
        name:
          type: string
          description: Function name shown to the AI
        description:
          type: string
          description: What the function does, used by the AI to decide when to call it
        url:
          type: string
          description: HTTP endpoint the function calls
        method:
          type: string
          enum: [GET, POST, PUT, DELETE, PATCH]
          description: HTTP method used when calling the endpoint
        input:
          type: array
          description: Input parameters the AI fills in when calling the function
          items:
            type: object
            additionalProperties: true
        ai_action:
          type: string
          description: Instruction telling the AI what to do with the function result
        created_at:
          type: integer
          description: Creation time as epoch milliseconds
        updated_at:
          type: integer
          description: Last update time as epoch milliseconds
    PhoneNumberRecord:
      type: object
      description: A phone number or messaging identity connected to the account. Sensitive provider credentials, resource identifiers, and billing internals are never included.
      properties:
        id:
          type: string
          description: The record identifier. Usually the E.164 phone number; for LINE accounts this is the official account's bot user id.
          examples: ["+31612345678"]
        phone_number:
          type: string
          description: The phone number in E.164 format (or the connection identifier for non-phone channels).
          examples: ["+31612345678"]
        display_name:
          type: ['string', 'null']
          description: Friendly label shown in the app for this number.
          examples: ["Support line"]
        country_code:
          type: ['string', 'null']
          description: ISO 3166-1 alpha-2 country code of the number.
          examples: ["NL"]
        type:
          type: ['string', 'null']
          description: "Stored connection type. One of: Twilio (purchased WhatsApp number), whatsapp_web, telegram, line, BYO (bring-your-own SMS)."
          examples: ["whatsapp_web"]
        channel:
          type: string
          description: The messaging channel this record powers.
          enum: [whatsapp, whatsapp_web, telegram, line, sms]
          examples: ["whatsapp_web"]
        category:
          type: ['string', 'null']
          description: Number category (for purchased numbers), for example local or mobile.
          examples: ["local"]
        is_active:
          type: ['boolean', 'null']
          description: Whether the number is currently active on the account.
          examples: [true]
        whatsapp_status:
          type: ['string', 'null']
          description: Last known WhatsApp registration status for purchased numbers (for example ONLINE, PENDING, DELETED).
          examples: ["ONLINE"]
        whatsapp_status_reason:
          type: ['string', 'null']
          description: Human-readable explanation when the WhatsApp registration is not online.
          examples: [null]
        outgoing_status:
          type: ['string', 'null']
          description: Last known outbound-sending status for the number.
          examples: ["ONLINE"]
        messaging_limit:
          type: ['integer', 'null']
          description: Daily business-initiated conversation limit reported for the number, when known.
          examples: [1000]
        follow_ups_disabled:
          type: boolean
          description: When true, automated follow-ups are not sent from this number.
          examples: [false]
        unique_inbound_contacts:
          type: ['integer', 'null']
          description: Lifetime count of distinct contacts that have messaged this number.
          examples: [42]
        last_inbound_at:
          type: ['string', 'null']
          format: date-time
          description: When the most recent inbound message arrived on this number.
          examples: ["2026-06-08T14:21:00.000Z"]
        purchase_date:
          type: ['string', 'null']
          format: date-time
          description: When the number was added to the account.
          examples: ["2026-01-15T09:00:00.000Z"]
        whatsapp_web:
          type: ['object', 'null']
          description: WhatsApp Web connection state. Only present for WhatsApp Web numbers.
          properties:
            status:
              type: ['string', 'null']
              description: Connection state of the WhatsApp Web session (for example open, connecting, disconnected).
              examples: ["open"]
            proxy_country:
              type: ['string', 'null']
              examples: ["nl"]
            phone_country:
              type: ['string', 'null']
              examples: ["nl"]
            qr_scanned:
              type: ['boolean', 'null']
              examples: [true]
            last_updated:
              type: ['string', 'null']
              format: date-time
            last_activity:
              type: ['string', 'null']
              format: date-time
        telegram:
          type: ['object', 'null']
          description: Telegram account connection state. Only present for Telegram connections.
          properties:
            status:
              type: ['string', 'null']
              description: Connection state (for example connected, code_required, disconnected).
              examples: ["connected"]
            telegram_user_id:
              type: ['string', 'null']
              examples: ["123456789"]
            username:
              type: ['string', 'null']
              examples: ["mybusiness"]
            last_updated:
              type: ['string', 'null']
              format: date-time
        line:
          type: ['object', 'null']
          description: LINE official account connection state. Only present for LINE connections.
          properties:
            status:
              type: ['string', 'null']
              examples: ["connected"]
            basic_id:
              type: ['string', 'null']
              examples: ["@shop123"]
            display_name:
              type: ['string', 'null']
              examples: ["My Shop"]
            picture_url:
              type: ['string', 'null']
            chat_mode:
              type: ['string', 'null']
              examples: ["bot"]
            last_updated:
              type: ['string', 'null']
              format: date-time
    PhoneNumberListResponse:
      type: object
      properties:
        success:
          type: boolean
          examples: [true]
        phone_numbers:
          type: array
          items:
            $ref: '#/components/schemas/PhoneNumberRecord'
        next_cursor:
          type: ['string', 'null']
          description: Pass this value as the cursor query parameter to fetch the next page. Null when there are no more records.
          examples: [null]
    PhoneNumberDetailResponse:
      type: object
      properties:
        success:
          type: boolean
          examples: [true]
        phone_number:
          $ref: '#/components/schemas/PhoneNumberRecord'
    PhoneNumberUpdateRequest:
      type: object
      description: Only display-layer fields are writable. At least one field must be provided.
      properties:
        display_name:
          type: string
          maxLength: 120
          description: New friendly label for the number.
          examples: ["Sales line"]
        category:
          type: string
          maxLength: 60
          description: Free-form category label.
          examples: ["sales"]
        follow_ups_disabled:
          type: boolean
          description: Set to true to stop automated follow-ups from this number.
          examples: [true]
    PhoneNumberStatusResponse:
      type: object
      properties:
        success:
          type: boolean
          examples: [true]
        phone_number:
          type: string
          examples: ["+31612345678"]
        channel:
          type: string
          enum: [whatsapp, whatsapp_web, telegram, line, sms]
          examples: ["whatsapp_web"]
        status:
          type: string
          description: "Connection status. WhatsApp numbers report ONLINE, PENDING, or DELETED; WhatsApp Web sessions report open, connecting, disconnected, or not_initialized; Telegram and LINE report their stored connection state; SMS numbers report active or inactive."
          examples: ["open"]
        status_reason:
          type: ['string', 'null']
          description: Human-readable explanation when the status is not healthy, when available.
          examples: [null]
        live:
          type: boolean
          description: True when the status came from a real-time check against the messaging provider or connection service; false when it reflects the last stored state.
          examples: [true]
    ChannelsSummaryResponse:
      type: object
      properties:
        success:
          type: boolean
          examples: [true]
        channels:
          type: object
          description: Per-channel connection summaries. Phone-backed channels include their numbers or accounts; social channels include the connected page details.
          properties:
            whatsapp:
              type: object
              properties:
                connected:
                  type: boolean
                  examples: [true]
                numbers:
                  type: array
                  items:
                    type: object
                    properties:
                      phone_number:
                        type: string
                        examples: ["+15551234567"]
                      display_name:
                        type: ['string', 'null']
                        examples: ["Main line"]
                      status:
                        type: ['string', 'null']
                        examples: ["ONLINE"]
            whatsapp_web:
              type: object
              properties:
                connected:
                  type: boolean
                  examples: [true]
                numbers:
                  type: array
                  items:
                    type: object
                    properties:
                      phone_number:
                        type: string
                        examples: ["+31612345678"]
                      display_name:
                        type: ['string', 'null']
                      status:
                        type: ['string', 'null']
                        examples: ["open"]
            sms:
              type: object
              properties:
                connected:
                  type: boolean
                  examples: [false]
                numbers:
                  type: array
                  items:
                    type: object
                    properties:
                      phone_number:
                        type: string
                      display_name:
                        type: ['string', 'null']
                      status:
                        type: ['string', 'null']
            telegram:
              type: object
              properties:
                connected:
                  type: boolean
                  examples: [false]
                accounts:
                  type: array
                  items:
                    type: object
                    properties:
                      phone_number:
                        type: string
                      username:
                        type: ['string', 'null']
                      status:
                        type: ['string', 'null']
            line:
              type: object
              properties:
                connected:
                  type: boolean
                  examples: [false]
                accounts:
                  type: array
                  items:
                    type: object
                    properties:
                      id:
                        type: string
                      display_name:
                        type: ['string', 'null']
                      basic_id:
                        type: ['string', 'null']
                      status:
                        type: ['string', 'null']
            instagram:
              type: object
              properties:
                connected:
                  type: boolean
                  examples: [true]
                page_id:
                  type: ['string', 'null']
                  examples: ["1234567890"]
                page_name:
                  type: ['string', 'null']
                  examples: ["My Business Page"]
                instagram_business_account_id:
                  type: ['string', 'null']
                  examples: ["17890000000000000"]
            messenger:
              type: object
              properties:
                connected:
                  type: boolean
                  examples: [true]
                page_id:
                  type: ['string', 'null']
                  examples: ["1234567890"]
                page_name:
                  type: ['string', 'null']
                  examples: ["My Business Page"]
    ChannelCampaignAssignRequest:
      type: object
      required: [campaign_id, channels]
      properties:
        campaign_id:
          type: string
          description: The campaign that should answer new, unknown contacts on the listed channels. Must be a campaign of type Incoming from Unknown Contacts or Combined that belongs to your account.
          examples: ["abc123campaign"]
        channels:
          type: array
          description: The channels to route to the campaign.
          items:
            type: string
            enum: [whatsapp, whatsapp_web, telegram, instagram, messenger, chat_widget, custom_channel, sms]
          examples: [["whatsapp", "instagram"]]
    ChannelCampaignAssignResponse:
      type: object
      properties:
        success:
          type: boolean
          examples: [true]
        campaign_id:
          type: string
          examples: ["abc123campaign"]
        channels:
          type: array
          items:
            type: string
          examples: [["whatsapp", "instagram"]]
    ChannelCampaignClearResponse:
      type: object
      properties:
        success:
          type: boolean
          examples: [true]
        channel:
          type: string
          examples: ["whatsapp"]
        cleared:
          type: boolean
          description: True when a routing entry was actually removed; false when the channel had no campaign routed (the call is idempotent).
          examples: [true]
        campaign_id:
          type: ['string', 'null']
          description: The campaign that was previously routed on this channel, when one was removed.
          examples: ["abc123campaign"]
    ChannelsErrorResponse:
      type: object
      properties:
        success:
          type: boolean
          examples: [false]
        error:
          type: string
          examples: ["Phone number not found"]
    ContactSummary:
      type: object
      description: Compact contact record returned by the paginated contact list. Related lists, tags, and campaigns are returned as ID arrays.
      properties:
        id:
          type: string
          description: Unique contact identifier.
        first_name:
          type: ['string', 'null']
          description: First name of the contact.
        last_name:
          type: ['string', 'null']
          description: Last name of the contact.
        email:
          type: ['string', 'null']
          description: Email address of the contact.
        phone_number:
          type: ['string', 'null']
          description: Phone number in international format.
        channel:
          type: ['string', 'null']
          description: Messaging channel the contact is reached on, such as whatsapp, whatsapp_web, or sms.
        is_bot_active:
          type: boolean
          description: Whether the AI assistant responds to this contact.
        is_private:
          type: boolean
          description: Whether the contact is marked private.
        do_not_disturb:
          type: boolean
          description: Whether automated outreach to this contact is paused.
        has_had_activity:
          type: boolean
          description: Whether any message has ever been exchanged with this contact.
        chat_concluded:
          type: boolean
          description: Whether the current conversation has been marked as concluded.
        external_id:
          type: ['string', 'null']
          description: External system identifier stored on the contact, if any.
        lead_profile:
          type: ['string', 'null']
          description: Free-text lead profile notes.
        custom_fields:
          type: object
          description: Map of custom field keys to values stored on the contact.
        created_at:
          type: ['string', 'null']
          format: date-time
          description: When the contact was created.
        last_modified_at:
          type: ['string', 'null']
          format: date-time
          description: When the contact was last modified.
        last_activity_at:
          type: ['string', 'null']
          format: date-time
          description: When the last message activity happened.
        last_incoming_message_at:
          type: ['string', 'null']
          format: date-time
          description: When the last inbound message was received.
        first_ai_interaction_at:
          type: ['string', 'null']
          format: date-time
          description: >-
            When the AI first sent this contact a message, or null if it never has. Set once and
            never cleared - switching the contact's AI replies off or moving them to another
            campaign does not reset it.
        last_ai_interaction_at:
          type: ['string', 'null']
          format: date-time
          description: >-
            When the AI most recently sent this contact a message, or null if it never has. Range
            over this to count the contacts the AI handled in a given period.
        list_ids:
          type: array
          items:
            type: string
          description: IDs of the lists this contact belongs to.
        tag_ids:
          type: array
          items:
            type: string
          description: IDs of the tags applied to this contact.
        campaign_ids:
          type: array
          items:
            type: string
          description: IDs of the campaigns this contact has been part of.
        current_campaign_id:
          type: ['string', 'null']
          description: ID of the campaign currently handling this contact, or null.
    ContactImportSkippedRecord:
      type: object
      description: Details about one import record that was skipped instead of created.
      properties:
        index:
          type: integer
          description: Zero-based position of the skipped record in the submitted contacts array.
        phone_number:
          type: ['string', 'null']
          description: Normalized phone number of the skipped record, when one could be parsed.
        reason:
          type: string
          enum: [invalid_record, missing_phone_number, invalid_phone_number, invalid_channel, duplicate_in_request, duplicate, contact_limit_reached, create_failed, update_failed]
          description: Why the record was skipped.
    ContactsApiError:
      type: object
      description: Standard error envelope returned by contact endpoints.
      properties:
        success:
          type: boolean
          description: Always false on errors.
        error:
          type: string
          description: Human-readable error message.
    Faq:
      type: object
      description: A question-and-answer entry used by the AI assistant when replying to customers.
      properties:
        id:
          type: string
          description: Unique identifier of the FAQ
          examples: ["aBcD1234eFgH5678"]
        question:
          type: ['string', 'null']
          description: The customer question this entry answers
          examples: ["How long does shipping take?"]
        answer:
          type: ['string', 'null']
          description: The answer the AI assistant gives for this question
          examples: ["Standard shipping takes 3-5 business days."]
        category:
          type: ['string', 'null']
          description: Optional free-form category label
          examples: ["shipping"]
        tags:
          type: array
          description: Optional labels for organizing FAQs
          items:
            type: string
          examples: [["logistics", "delivery"]]
        is_active:
          type: boolean
          description: Whether the AI assistant is allowed to use this FAQ
          examples: [true]
        is_global:
          type: boolean
          description: Whether this FAQ applies to all campaigns rather than only the linked ones
          examples: [false]
        usage_count:
          type: integer
          description: How many times this FAQ has been used in AI replies
          examples: [12]
        order_index:
          type: integer
          description: Display position of this FAQ within its campaign
          examples: [0]
        campaign_ids:
          type: array
          description: Legacy. IDs of the campaigns this FAQ is linked to. Empty on an agent-based account - read agent_ids instead.
          items:
            type: string
          examples: [["campaign123"]]
        agent_ids:
          type: array
          description: IDs of the AI Agents whose knowledge base this FAQ belongs to
          items:
            type: string
          examples: [["ag7HkQ2ZpLxR3mNb"]]
        created_at:
          type: ['string', 'null']
          format: date-time
          description: ISO 8601 timestamp of when the FAQ was created
          examples: ["2026-01-01T12:00:00.000Z"]
        updated_at:
          type: ['string', 'null']
          format: date-time
          description: ISO 8601 timestamp of the last change to the FAQ
          examples: ["2026-01-02T08:30:00.000Z"]
    FaqCreateRequest:
      type: object
      description: |
        Payload for creating a new FAQ.

        Where the FAQ lands is a three-way choice, and all three are valid:
        - agent_id - add it to that AI Agent's knowledge base (recommended).
        - campaign_id - legacy; prefer agent_id. Adds it to that campaign instead.
        - neither - create it as a global FAQ that is not attached to anything yet.

        Sending both agent_id and campaign_id is rejected.
      required:
        - question
        - answer
      properties:
        agent_id:
          type: string
          description: ID of the AI Agent whose knowledge base the new FAQ is added to. Also accepted as agentId.
          examples: ["ag7HkQ2ZpLxR3mNb"]
        campaign_id:
          type: string
          description: Legacy; prefer agent_id. ID of the campaign the new FAQ is linked to.
          examples: ["campaign123"]
        question:
          type: string
          description: The customer question this entry answers
          examples: ["How long does shipping take?"]
        answer:
          type: string
          description: The answer the AI assistant should give
          examples: ["Standard shipping takes 3-5 business days."]
        is_active:
          type: boolean
          description: Whether the AI assistant may use this FAQ (defaults to true)
          examples: [true]
        is_global:
          type: boolean
          description: Whether this FAQ applies to all campaigns (defaults to false)
          examples: [false]
        category:
          type: string
          description: Optional free-form category label
          examples: ["shipping"]
        tags:
          type: array
          description: Optional labels for organizing FAQs
          items:
            type: string
          examples: [["logistics"]]
        order_index:
          type: integer
          description: Display position within the campaign (defaults to 0)
          examples: [0]
    FaqImportItem:
      type: object
      description: One FAQ item in a bulk import. If the question matches an existing FAQ in your library (case-insensitive), that FAQ is updated instead of duplicated.
      required:
        - question
        - answer
      properties:
        question:
          type: string
          description: The customer question this entry answers
          examples: ["Do you ship internationally?"]
        answer:
          type: string
          description: The answer the AI assistant should give
          examples: ["Yes, we ship to most countries worldwide."]
        is_active:
          type: boolean
          description: Whether the AI assistant may use this FAQ (defaults to true)
        is_global:
          type: boolean
          description: Whether this FAQ applies to all campaigns (defaults to false)
        category:
          type: string
          description: Optional free-form category label
        tags:
          type: array
          description: Optional labels for organizing FAQs
          items:
            type: string
        order_index:
          type: integer
          description: Display position within the campaign
    KbSourceStatus:
      type: object
      description: |
        The live state of one knowledge-base source (a page read from a website, or an uploaded
        document). This is what the ingest endpoints ask you to poll.
      properties:
        success:
          type: boolean
          examples: [true]
        source_id:
          type: string
          description: ID of the knowledge-base source.
          examples: ["kb_src_abc123"]
        status:
          type: string
          description: |
            Where the source is in the pipeline. queued and processing mean it is still working;
            ready and failed are final. cancelled means the import was stopped before it ran, paused
            means it stopped on an AI key problem and can be resumed, deleting means a removal is in
            flight, and unknown means the record carries no status at all.
          examples: ["ready"]
        faq_count:
          type: integer
          description: How many FAQs have been generated from this source so far.
          examples: [24]
        section_count:
          type: integer
          description: How many content sections the source was split into.
          examples: [31]
        error_message:
          type: ['string', 'null']
          description: Why the import failed, when the status is failed. null otherwise.
    KbDiscoveredPage:
      type: object
      description: |
        One page found while exploring a website, with an opinion on whether it is worth adding to
        your knowledge base. Nothing is selected for you - the verdict is advice, and you decide
        which pages to import.
      properties:
        url:
          type: string
          format: uri
          description: Full address of the page.
          examples: ["https://example.com/pricing"]
        title:
          type: ['string', 'null']
          description: Page title, when one could be read. Only returned by page discovery.
          examples: ["Pricing"]
        depth:
          type: integer
          description: How many links away from the starting page this page was found. Only returned by page discovery.
          examples: [1]
        score:
          type: integer
          minimum: 0
          maximum: 100
          description: How useful the page looks as knowledge, from 0 to 100.
          examples: [95]
        recommendation:
          type: string
          enum: ["add", "maybe", "skip"]
          description: |
            The advice for this page. add means clearly worth importing (score 90 or above), maybe
            is borderline, skip is content that rarely helps an assistant (changelogs, legal pages,
            duplicate translations of a page you already have).
          examples: ["add"]
        reason_key:
          type: string
          description: |
            Stable, machine-readable reason behind the recommendation, for example core_page,
            changelog_history, legal_page or locale_duplicate.
          examples: ["core_page"]
    KbCostEstimateRow:
      type: object
      description: The estimated size and credit cost of importing one URL or file.
      properties:
        ref:
          type: string
          description: The URL or storage path this row is about, echoed back so you can match it to your input.
          examples: ["https://example.com/pricing"]
        chunks:
          type: integer
          description: How many content chunks the source is expected to produce. Always at least 1.
          examples: [7]
        credits:
          type: number
          description: Estimated credits this source will consume.
          examples: [7]
        error:
          type: string
          description: Present when the page or file could not be read for the estimate; chunks falls back to 1.
          examples: ["fetch failed"]
    KbDomainRefreshJob:
      type: object
      description: Progress of a running website refresh, so you can show how far along it is.
      properties:
        domainBatchId:
          type: string
          description: ID of the refresh job. Pass this to the cancel endpoint to stop it.
          examples: ["job_7c1e"]
        host:
          type: string
          description: The website being refreshed.
          examples: ["example.com"]
        total:
          type: integer
          description: Total pages in this refresh.
          examples: [249]
        pending:
          type: integer
          description: Pages not finished yet. Pages done so far is total minus pending.
          examples: [28]
        succeeded:
          type: integer
          examples: [219]
        failed:
          type: integer
          examples: [2]
        skippedDuplicate:
          type: integer
          description: Pages skipped because the same content was already present.
          examples: [0]
        status:
          type: string
          enum: ["refreshing", "deduplicating", "completed", "failed", "cancelled"]
          description: |
            Job state. refreshing means pages are still being worked through, deduplicating is the
            clean-up pass at the end, and completed / failed / cancelled are final.
          examples: ["refreshing"]
        startedAtIso:
          type: ['string', 'null']
          format: date-time
          description: ISO 8601 timestamp of when the refresh started.
          examples: ["2026-06-15T09:00:00.000Z"]
    SmartListCondition:
      type: object
      description: |
        One rule in a smart list. Every condition names a field and an operator; most also carry a
        value. The vocabulary is fixed:

        - tags - has_any / has_all / has_none. value is an array of TAG IDs (not names).
        - lists - in_any / not_in_any. value is an array of static list IDs. A smart list may not
          reference another smart list.
        - channel - is_any / is_none. value is an array of channel names, for example whatsapp.
        - status - is_any / is_none. value is an array of contact status values.
        - deal_stage - is_any / is_none / is_set / not_set. value is an array of pipeline stage IDs.
        - created_at, last_activity_at, last_incoming_message_at, last_outgoing_message_at,
          first_ai_interaction_at, last_ai_interaction_at -
          within_last / not_within_last (value is an amount plus a unit of hours or days),
          before / after (value is an ISO 8601 date or date-time), or is_set / not_set.
        - has_interacted_with_ai - is, with a true or false value. True matches contacts the AI has
          messaged at least once; false matches contacts it never has. Use last_ai_interaction_at
          with within_last instead when you need the contacts the AI handled in a given period.
        - is_bot_active, do_not_disturb, is_private, has_ever_responded - is, with a true or false
          value. A contact with no value counts as false.
        - email, phone_number, first_name, last_name - is_set / not_set / contains / not_contains.
        - custom_field - eq / neq / contains / not_contains / is_set / not_set, with key naming the
          custom field.

        Negative operators (is_none, not_in_any, has_none, not_contains, not_within_last) also match
        contacts that have no value at all.
      required:
        - field
        - op
      properties:
        field:
          type: string
          description: The contact attribute this rule looks at.
          examples: ["tags"]
        op:
          type: string
          description: The comparison to apply. Which operators are allowed depends on the field.
          examples: ["has_any"]
        key:
          type: string
          description: Name of the custom field. Only used when field is custom_field.
          examples: ["plan"]
          maxLength: 64
        value:
          description: |
            The value to compare against. Its shape follows the operator: an array of IDs or names
            for the membership operators (at most 100 entries), a true or false for a boolean field,
            a text needle for contains, an ISO 8601 date for before and after, and an object with
            amount and unit for within_last. Omitted entirely for is_set and not_set.
          examples: [["tag_vip"]]
    SmartListRules:
      type: object
      description: The rule set that decides who is in a smart list. At most 20 conditions.
      required:
        - match
        - conditions
      properties:
        match:
          type: string
          enum: ["all", "any"]
          description: all means a contact must satisfy every condition; any means at least one.
          examples: ["all"]
        conditions:
          type: array
          minItems: 1
          maxItems: 20
          items:
            $ref: '#/components/schemas/SmartListCondition'
    SmartListEvaluation:
      type: object
      description: What a smart-list rules run changed.
      properties:
        added:
          type: integer
          description: Contacts added to the list by this run.
          examples: [12]
        removed:
          type: integer
          description: Contacts removed from the list by this run.
          examples: [3]
        total:
          type: integer
          description: How many contacts are in the list after the run.
          examples: [143]
        skipped:
          type: boolean
          description: True when another evaluation of this list was already running, so this one did nothing.
          examples: [false]
    ListCreateRequest:
      type: object
      description: Payload for creating a contact list.
      required:
        - name
      properties:
        name:
          type: string
          maxLength: 100
          description: Name of the list. Trimmed, and cut at 100 characters.
          examples: ["VIP Customers"]
        status:
          type: string
          enum: ["live", "draft"]
          description: Whether the list is in use or still being prepared. Defaults to live.
          examples: ["live"]
        type:
          type: string
          enum: ["static", "smart"]
          description: |
            static means you choose the members; smart means membership is computed from
            smart_rules and kept up to date automatically. Defaults to static.
          examples: ["static"]
        smart_rules:
          $ref: '#/components/schemas/SmartListRules'
        contact_ids:
          type: array
          description: |
            IDs of contacts to put in the list straight away. Only for a static list - sending
            members for a smart list is rejected.
          items:
            type: string
          examples: [["contact_abc123", "contact_def456"]]
    ListUpdateRequest:
      type: object
      description: Fields to change on a list. At least one field is required.
      properties:
        name:
          type: string
          maxLength: 100
          description: New name for the list.
          examples: ["VIP Customers 2026"]
        status:
          type: string
          enum: ["live", "draft"]
          description: New status.
          examples: ["draft"]
        type:
          type: string
          enum: ["static", "smart"]
          description: |
            Convert the list. Switching to smart needs smart_rules (either sent here or already on
            the list); switching to static clears the rules and keeps the current members.
          examples: ["smart"]
        smart_rules:
          $ref: '#/components/schemas/SmartListRules'
        contact_ids:
          type: array
          description: Replaces the list's members. Only for a static list.
          items:
            type: string
          examples: [["contact_abc123"]]
    MessageItem:
      type: object
      description: A single message in a conversation.
      properties:
        id:
          type: string
          description: Unique ID of the message.
          examples: ["aB3dE5fG7hI9jK1lM2nO"]
        body:
          type: string
          description: Text content of the message.
          examples: ["Hi! Thanks for reaching out."]
        direction:
          type: ['string', 'null']
          description: Whether the message was received from the contact (inbound) or sent by your account (outbound).
          examples: ["inbound"]
        channel:
          type: ['string', 'null']
          description: Channel the message was sent or received on, for example whatsapp, whatsapp_web, sms, instagram, messenger, telegram.
          examples: ["whatsapp"]
        status:
          type: ['string', 'null']
          description: Current delivery status of the message (for example Created, sent, delivered, read, failed).
          examples: ["delivered"]
        type:
          type: ['string', 'null']
          description: Message type. Regular text messages have a null type; automated assistant tool activity is marked as tool_use.
        timestamp:
          type: ['string', 'null']
          format: date-time
          description: ISO 8601 timestamp of when the message was created.
          examples: ["2026-06-01T10:00:00.000Z"]
        media_url:
          type: ['string', 'null']
          description: URL of an attached media file, if any.
        media_content_type:
          type: ['string', 'null']
          description: MIME type of the attached media file, if any.
          examples: ["image/jpeg"]
        bot_reply:
          type: boolean
          description: True when the message was generated by the AI assistant.
    ChatSessionThread:
      type: object
      description: Metadata for a single chat session.
      properties:
        id:
          type: string
          description: Unique ID of the chat session.
        status:
          type: ['string', 'null']
          description: Session state. ChatSessionOpened while the conversation is active, ChatSessionClosed once it has ended.
          examples: ["ChatSessionClosed"]
        start_date_time:
          type: ['string', 'null']
          format: date-time
          description: ISO 8601 timestamp of when the session started.
        end_date_time:
          type: ['string', 'null']
          format: date-time
          description: ISO 8601 timestamp of when the session ended (or is scheduled to end).
        tag:
          type: ['string', 'null']
          description: Human-readable label summarising the session window.
    SendMessageRequest:
      type: object
      description: |
        Payload for the channel-agnostic send endpoint. Provide the message
        text plus either a contact_id, or a channel together with the matching
        identity field (phone_number for whatsapp / whatsapp_web / sms,
        instagram_id for instagram, messenger_id for messenger,
        telegram_user_id for telegram).
      required:
        - body
      properties:
        body:
          type: string
          description: Text content of the message to send.
          examples: ["Hi! Your appointment is confirmed for tomorrow at 10:00."]
        contact_id:
          type: string
          description: ID of an existing contact to send to. When provided, the identity fields below are not needed.
        channel:
          type: string
          description: Channel to send on. Required when contact_id is not provided.
          enum: [whatsapp, whatsapp_web, sms, instagram, messenger, telegram, chat-widget, custom]
        phone_number:
          type: string
          description: Contact phone number in international format. Used with the whatsapp, whatsapp_web, and sms channels.
          examples: ["+31612345678"]
        instagram_id:
          type: string
          description: Instagram user ID of the contact. Used with the instagram channel.
        messenger_id:
          type: string
          description: Messenger user ID of the contact. Used with the messenger channel.
        telegram_user_id:
          type: string
          description: Telegram user ID of the contact. Used with the telegram channel.
        media_url:
          type: string
          description: Optional URL of a media file to attach.
        media_content_type:
          type: string
          description: MIME type of the attached media file.
          examples: ["image/jpeg"]
    SendMessageResponse:
      type: object
      description: Returned when a message has been accepted for delivery.
      properties:
        success:
          type: boolean
        message_id:
          type: string
          description: ID of the created message document. Delivery continues asynchronously.
        contact_id:
          type: string
          description: ID of the contact the message was created for.
        channel:
          type: string
          description: Channel the message will be delivered on.
          examples: ["whatsapp"]
    Task:
      type: object
      description: A task on the CRM task board.
      properties:
        id:
          type: string
          description: Unique task identifier.
        title:
          type: string
          description: Short title of the task.
        description:
          type: string
          description: Longer free-text description of the task.
        type:
          type: string
          description: Task type identifier. Built-in types include to_do, call, email, follow_up, faq_update, human_alert, user_feedback, bug_report, and feature_request. Custom types are also supported.
        priority:
          type: string
          enum: [none, low, medium, high, urgent]
          description: Task priority.
        stage:
          type: string
          description: Identifier of the board stage (column) the task is currently in.
        position:
          type: integer
          description: Zero-based position of the task within its stage. Lower numbers appear higher in the column.
        due_date:
          type: ['object', 'null']
          description: Due date as a timestamp object with _seconds and _nanoseconds fields, or null when no due date is set.
        completed_at:
          type: ['object', 'null']
          description: Completion timestamp, or null while the task is still open.
        created_at:
          type: object
          description: Creation timestamp.
        updated_at:
          type: object
          description: Last update timestamp.
        contact:
          type: ['object', 'null']
          description: Reference to the linked contact, or null when the task is not linked to a contact.
        deal:
          type: ['object', 'null']
          description: Reference to the linked deal, or null.
        campaign:
          type: ['object', 'null']
          description: Reference to the linked campaign, or null.
        assigned_to:
          type: ['object', 'null']
          description: Reference to the team member the task is assigned to, or null when unassigned.
        linked_messages:
          type: array
          items:
            type: string
          description: IDs of chat messages linked to this task.
        source:
          type: string
          enum: [manual, human_alert, ai_tool, api, automation, daily_summary]
          description: How the task was created. Tasks created through this API have source set to api.
        source_detail:
          type: string
          description: Extra information about the source, such as the originating campaign ID.
        linked_human_alert:
          type: boolean
          description: True when the task was created from a human-attention alert.
        tags:
          type: array
          items:
            type: string
          description: Free-form labels attached to the task.
        notes:
          type: string
          description: Internal notes on the task.
    TaskStage:
      type: object
      description: A column on the task board.
      required:
        - id
        - name
        - color
        - position
      properties:
        id:
          type: string
          description: Stage identifier. Letters, numbers, underscores, and dashes only; up to 64 characters. Must be unique across the board.
        name:
          type: string
          description: Display name of the stage, up to 80 characters.
        color:
          type: string
          description: "Hex color for the column, for example '#3B82F6'."
        position:
          type: integer
          description: Zero-based order of the column on the board.
        is_completed_stage:
          type: boolean
          description: Marks this stage as the done column. Completing a task moves it here. Defaults to false when omitted.
    TaskTypeConfig:
      type: object
      description: A task type definition shown on the task board.
      properties:
        id:
          type: string
          description: Type identifier.
        name:
          type: string
          description: Display name of the type.
        color:
          type: string
          description: "Hex color used for the type badge, for example '#3B82F6'."
        icon:
          type: string
          description: Icon name displayed next to the type.
    TasksApiError:
      type: object
      description: Standard error envelope returned by task endpoints.
      properties:
        success:
          type: boolean
          description: Always false on errors.
        error:
          type: string
          description: Human-readable error message.
    WebhookSubscription:
      type: object
      description: A webhook subscription configured on the account.
      properties:
        id:
          type: string
          description: Identifier for this subscription. Use it (or the exact name) as the webhookId path parameter in other webhook endpoints. Note that ids are positional and can shift after a deletion, so prefer addressing subscriptions by name.
          examples: ["0"]
        name:
          type: ['string', 'null']
          description: Display name for the subscription. Can also be used as the webhookId path parameter.
          examples: ["Order updates hook"]
        url:
          type: string
          description: HTTPS URL that receives event payloads via POST.
          examples: ["https://hooks.example.com/incoming"]
        subscribed_to:
          type: array
          description: Event types this subscription receives. See GET /webhooks/events for the full list of valid values.
          items:
            type: string
          examples: [["Contact Created", "Replies"]]
        subscribed_to_tags:
          type: array
          description: Optional tag IDs that scope tag-based notifications (such as conversation summaries) to specific tags. Empty means no tag filter.
          items:
            type: string
          examples: [["tagId123"]]
        created_at:
          type: ['string', 'null']
          format: date-time
          description: When the subscription was created.
          examples: ["2026-06-09T12:00:00.000Z"]
        enabled:
          type: boolean
          description: The account's on/off switch for this subscription. Delivery only happens when the subscription is switched on. A subscription with no stored value (created before this field existed) reports true. Set via enabled on POST /webhooks or PUT /webhooks/{webhookId}.
          examples: [true]
        retries_enabled:
          type: boolean
          description: Whether failed deliveries to this subscription are retried (at 1m, 5m, 30m and 2h). Off by default. Set via retries_enabled on POST /webhooks or PUT /webhooks/{webhookId}.
          examples: [false]
        apply_to_sub_accounts:
          type: boolean
          description: Agency inheritance. When true on an agency master account, this subscription also receives events that happen on every one of its sub-accounts, so one endpoint covers the whole agency. The user block in each payload identifies the sub-account the event belongs to. Off by default, and inert on accounts that have no sub-accounts. Set via apply_to_sub_accounts on POST /webhooks or PUT /webhooks/{webhookId}.
          examples: [false]
        signing_enabled:
          type: boolean
          description: Whether deliveries to this subscription are signed with an HMAC-SHA256 signature. On when a signing secret is set. The secret itself is never returned here — read it from GET /webhooks/{webhookId}/signing-secret.
          examples: [false]
        signing_secret_created_at:
          type: ['string', 'null']
          format: date-time
          description: When the current signing secret was generated, or null when signing is off.
          examples: ["2026-07-15T09:30:00.000Z"]
    WebhookHealthStatus:
      type: object
      description: Delivery-health information for a webhook URL. Null until the first delivery attempt has been made.
      properties:
        consecutive_failures:
          type: integer
          description: Number of delivery failures in a row. Resets to 0 on a successful delivery.
          examples: [0]
        total_failures:
          type: integer
          description: Total failed deliveries recorded for this URL.
          examples: [7]
        total_successes:
          type: integer
          description: Total successful deliveries recorded for this URL.
          examples: [42]
        is_disabled:
          type: boolean
          description: True when delivery to this URL has been paused automatically after repeated failures. Use the reenable endpoint to resume delivery.
          examples: [false]
        disabled_at:
          type: ['string', 'null']
          format: date-time
          description: When delivery was paused, if it is currently paused.
        disabled_reason:
          type: ['string', 'null']
          description: Human-readable explanation of why delivery was paused.
        last_failure:
          type: ['object', 'null']
          description: Details of the most recent failed delivery, when one has occurred.
          properties:
            timestamp:
              type: ['string', 'null']
              format: date-time
              description: When the failure happened.
            status_code:
              type: ['integer', 'null']
              description: HTTP status code returned by the receiver, when available.
              examples: [410]
            failure_type:
              type: string
              description: Classification of the failure.
              enum: [permanent, temporary, timeout, network, unknown]
            error_message:
              type: string
              description: Error message from the failed delivery attempt.
        last_success_at:
          type: ['string', 'null']
          format: date-time
          description: When the most recent successful delivery happened.
        created_at:
          type: ['string', 'null']
          format: date-time
          description: When health tracking started for this URL.
        updated_at:
          type: ['string', 'null']
          format: date-time
          description: When the health record was last updated.
    WhatsAppTemplateDetail:
      type: object
      description: A WhatsApp message template stored on the account.
      properties:
        id:
          type: string
          description: Template ID
        name:
          type: ['string', 'null']
          description: Template name
        body:
          type: ['string', 'null']
          description: Template body text. Variables use double curly braces, for example a first-name placeholder.
        language:
          type: ['string', 'null']
          description: Template language code (for example en, es, de, pt_BR, zh_CN)
        variables:
          type: array
          description: Ordered list of variable names used in the body
          items:
            type: string
        status:
          type: ['string', 'null']
          description: 'Approval status: draft, received, pending, approved, or rejected'
        sid:
          type: ['string', 'null']
          description: Content ID assigned once the template has been submitted for review. Null for drafts and auto-approved templates.
        type:
          type: ['string', 'null']
          description: Template kind (general or smart_followup)
        category:
          type: ['string', 'null']
          description: Pricing category of the template, when known (for example marketing or utility)
        rejection_reason:
          type: ['string', 'null']
          description: Reason supplied by the review process when a template is rejected
        campaign_id:
          type: ['string', 'null']
          description: ID of the campaign this template is linked to, when applicable
        date_created:
          type: ['string', 'null']
          format: date-time
          description: When the template was created
        date_updated:
          type: ['string', 'null']
          format: date-time
          description: When the template was last updated
        submitted_at:
          type: ['string', 'null']
          format: date-time
          description: When the template was last submitted for review
        approved_at:
          type: ['string', 'null']
          format: date-time
          description: When the template was approved
    LineConnectRequest:
      type: object
      required:
        - channel_access_token
        - channel_secret
      properties:
        channel_access_token:
          type: string
          description: The Official Account's long-lived Messaging API channel access token. Transmitted over TLS and stored to send and receive messages. The caller is responsible for rotating the token in the provider console and re-submitting it here if it is reissued.
        channel_secret:
          type: string
          description: The Messaging API channel secret, used to verify inbound event signatures.
        channel_id:
          type: string
          description: Optional numeric channel id. Informational only.
        sub_account_id:
          type:
            - string
            - "null"
          description: Optional. Connect the Official Account to a managed account instead of the calling account. The managed account must belong to the caller.
    LineConnectResponse:
      type: object
      properties:
        success:
          type: boolean
          example: true
        status:
          type: string
          description: Always "connected" on success.
          example: connected
        bot_user_id:
          type: string
          description: The Official Account's resolved bot user id. Also the connection identifier used in other endpoints.
        basic_id:
          type:
            - string
            - "null"
          description: The Official Account's basic id (the public handle).
        display_name:
          type:
            - string
            - "null"
        picture_url:
          type:
            - string
            - "null"
        chat_mode:
          type:
            - string
            - "null"
          description: The Official Account's chat mode as reported by the provider.
        chat_mode_ok:
          type: boolean
          description: false means the Official Account is in chat mode and will not receive or send messages until it is switched to bot mode in the account manager. Automated onboarding should gate on this flag.
        webhook_url:
          type: string
          description: The inbound webhook URL to configure in the provider console.
    LineStatusResponse:
      type: object
      properties:
        success:
          type: boolean
          example: true
        bot_user_id:
          type: string
        channel:
          type: string
          example: line
        status:
          type: string
          description: The stored connection status captured at connect time.
          example: connected
        basic_id:
          type:
            - string
            - "null"
        display_name:
          type:
            - string
            - "null"
        picture_url:
          type:
            - string
            - "null"
        chat_mode:
          type:
            - string
            - "null"
        is_active:
          type:
            - boolean
            - "null"
        live:
          type: boolean
          description: Always false. The status reflects stored state; this channel has no live status endpoint.
          example: false
    LineVerifyWebhookResponse:
      type: object
      properties:
        success:
          type: boolean
          example: true
        token_valid:
          type: boolean
          description: Whether the stored access token still authenticates against the provider.
        chat_mode:
          type:
            - string
            - "null"
        chat_mode_ok:
          type: boolean
        webhook_url:
          type: string
    LineDisconnectResponse:
      type: object
      properties:
        success:
          type: boolean
          example: true
        status:
          type: string
          example: removed
        bot_user_id:
          type: string
    LineErrorResponse:
      type: object
      properties:
        success:
          type: boolean
          example: false
        error:
          type: string
        error_code:
          type:
            - integer
            - "null"
    MetaConnectRequest:
      type: object
      description: Optional targeting for the connect flow.
      properties:
        sub_account_id:
          type:
            - string
            - "null"
          description: Optionally target a managed account instead of the calling account.
    MetaConnectResponse:
      type: object
      properties:
        success:
          type: boolean
          example: true
        oauth_url:
          type: string
          description: The consent URL the end user must open in a browser to authorize the connection.
          example: https://www.facebook.com/v21.0/dialog/oauth?client_id=...&state=...
        state_token:
          type: string
          description: Opaque one-time token correlating this connection attempt. Treat as a short-lived secret.
        expires_at:
          type: string
          format: date-time
          description: ISO-8601 instant after which the connection attempt expires and must be restarted.
        connect_url:
          type:
            - string
            - "null"
          description: >-
            A hosted page that finishes the WHOLE connection on its own — hand
            this link to the account holder instead of opening oauth_url yourself
            and building a page picker. It redirects them to Facebook to
            authorize, then brings them back to choose which Facebook Page (and
            its linked Instagram account) to connect, and confirms success. Null
            if the link could not be generated; oauth_url plus the status / pages
            / select-page endpoints remain available as a fallback. The link
            works for about 30 minutes (connect_url_expires_at).
          example: "https://api.dmchamp.com/v1/channels/meta/connect/page?token=eyJhbGciOi..."
        connect_url_expires_at:
          type:
            - integer
            - "null"
          description: Unix epoch (ms) when connect_url stops working. Start a new connection to get a fresh link.
    MetaPageSummary:
      type: object
      description: A connectable page, without any credential.
      properties:
        id:
          type:
            - string
            - "null"
          description: Facebook page id.
        name:
          type:
            - string
            - "null"
        category:
          type:
            - string
            - "null"
        instagram_business_account:
          type:
            - object
            - "null"
          properties:
            id:
              type: string
            username:
              type:
                - string
                - "null"
    MetaStatusResponse:
      type: object
      properties:
        success:
          type: boolean
          example: true
        status:
          type: string
          description: >-
            Coarse step of the connection flow. pending = consent not yet
            completed; token_received = authorized, pages still loading;
            pages_loaded = pages available, none selected; connected = a page is
            selected (terminal success); failed = the OAuth step errored before a
            token was obtained (terminal — stop polling and start a new
            connection); expired = the connection session expired before
            completing (terminal — start a new connection). See 'reason' for
            failed/expired.
          enum:
            - pending
            - token_received
            - pages_loaded
            - connected
            - failed
            - expired
        reason:
          type: string
          description: A short, non-sensitive reason code present only on the terminal failed/expired states (e.g. "session_expired", "token_exchange_failed").
        pages:
          type:
            - array
            - "null"
          description: Connectable pages once loaded, or null before authorization (and on the terminal failed/expired states).
          items:
            $ref: "#/components/schemas/MetaPageSummary"
        selected_page:
          oneOf:
            - $ref: "#/components/schemas/MetaPageSummary"
            - type: "null"
          description: The currently selected page, or null when none is selected.
    MetaPagesResponse:
      type: object
      properties:
        success:
          type: boolean
          example: true
        pages:
          type: array
          items:
            $ref: "#/components/schemas/MetaPageSummary"
    MetaPostSummary:
      type: object
      description: One Instagram media item or Facebook Page post, normalized for pickers.
      properties:
        id:
          type: string
          description: Raw Graph id. Instagram media id, or "{pageId}_{postId}" for Page posts. Use verbatim when targeting specific posts.
        caption:
          type: string
          description: Caption (Instagram) or message/story text (Facebook). Empty string when the post has none.
        thumbnailUrl:
          type:
            - string
            - "null"
          description: Preview image URL when Meta exposes one.
        permalink:
          type:
            - string
            - "null"
          description: Public link to the post when Meta exposes one.
        createdAt:
          type:
            - string
            - "null"
          format: date-time
        mediaType:
          type:
            - string
            - "null"
          description: e.g. REELS, FEED or VIDEO for Instagram; always POST for Facebook.
    MetaPostsResponse:
      type: object
      properties:
        success:
          type: boolean
          example: true
        connected:
          type: boolean
          description: False when no page is selected, no Instagram account is linked, or the stored credential was rejected. See reason.
        reason:
          type: string
          description: Present only when connected is false and the cause is known.
          enum:
            - no_instagram_account
            - token_expired
        platform:
          type: string
          description: Echo of the requested platform. Present only when connected is true.
          enum:
            - instagram
            - facebook
        posts:
          type: array
          items:
            $ref: "#/components/schemas/MetaPostSummary"
        nextCursor:
          type:
            - string
            - "null"
          description: Pass back as "after" to fetch the next page. Null when there are no more pages.
    MetaSelectPageRequest:
      type: object
      required:
        - page_id
      properties:
        page_id:
          type: string
          description: The id of the page to connect. Must be one of the pages returned by the status or pages endpoint.
        instagram:
          type:
            - object
            - "null"
          description: Optional override of the Instagram Business account to link. When omitted, the Instagram account associated with the chosen page is used automatically.
          required:
            - id
          properties:
            id:
              type: string
            username:
              type:
                - string
                - "null"
        sub_account_id:
          type:
            - string
            - "null"
          description: Optionally target a managed account instead of the calling account.
    MetaSelectPageResponse:
      type: object
      properties:
        success:
          type: boolean
          example: true
        page_id:
          type: string
        instagram_business_account_id:
          type:
            - string
            - "null"
    MetaDisconnectResponse:
      type: object
      properties:
        success:
          type: boolean
          example: true
        disconnected:
          type: boolean
          example: true
    MetaConnectError:
      type: object
      properties:
        success:
          type: boolean
          example: false
        error:
          type: string
        error_code:
          type:
            - integer
            - "null"
    AvailablePhoneNumber:
      type: object
      description: A purchasable phone number with its cost in credits.
      properties:
        phone_number:
          type: string
          description: The number in E.164 format.
          examples:
            - "+14155551234"
        purchase_credits:
          type: number
          description: One-time purchase cost in credits.
        monthly_credits:
          type: number
          description: Recurring monthly maintenance cost in credits.
        cost_usd:
          type:
            - number
            - "null"
          description: Underlying provider cost in USD, when available.
    AvailablePhoneNumbersResponse:
      type: object
      properties:
        success:
          type: boolean
          examples:
            - true
        phone_numbers:
          type: array
          items:
            $ref: "#/components/schemas/AvailablePhoneNumber"
    PurchasePhoneNumberRequest:
      type: object
      required:
        - phone_number
        - country_code
      properties:
        phone_number:
          type: string
          description: The number to purchase, in E.164 format, as returned by the available-numbers search.
          examples:
            - "+14155551234"
        country_code:
          type: string
          description: ISO 3166-1 alpha-2 country code for the number.
          minLength: 2
          maxLength: 2
          examples:
            - US
        display_name:
          type: string
          description: A label for the number. Defaults to the number itself when omitted.
          examples:
            - Support line
        category:
          type: string
          description: An optional grouping label for the number.
          examples:
            - support
        sub_account_id:
          type:
            - string
            - "null"
          description: Optional. Purchase the number for a managed account you administer instead of your own.
    PurchasePhoneNumberResponse:
      type: object
      properties:
        success:
          type: boolean
          examples:
            - true
        phone_number:
          type: string
          examples:
            - "+14155551234"
        channel:
          type: string
          examples:
            - whatsapp
        whatsapp_status:
          type: string
          description: Provisioning state of the WhatsApp sender. Starts at PURCHASED and advances asynchronously.
          examples:
            - PURCHASED
        outgoing_status:
          type: string
          examples:
            - PURCHASED
        status:
          type: string
          examples:
            - PURCHASED
        purchase_credits:
          type: number
        monthly_credits:
          type: number
    ReleasePhoneNumberResponse:
      type: object
      properties:
        success:
          type: boolean
          examples:
            - true
        phone_number:
          type: string
          examples:
            - "+14155551234"
        released:
          type: boolean
          examples:
            - true
    PhoneNumberPurchaseErrorResponse:
      type: object
      properties:
        success:
          type: boolean
          examples:
            - false
        error:
          type: string
          description: Human-readable error message.
        error_code:
          type:
            - integer
            - "null"
          description: Numeric status code echoed for client convenience.
        reason:
          type:
            - string
            - "null"
          description: Stable machine-readable reason for the failure, present on business errors. One of channel_tier_required, number_cooldown, daily_cap_reached, business_address_required, business_address_invalid, regulatory_bundle_required, insufficient_credits, number_unavailable, provider_error, phone_number_not_found, provider_credentials_missing.
    TelegramConnectRequest:
      type: object
      required:
        - phone_number
      properties:
        phone_number:
          type: string
          description: The account phone number to connect, in E.164 format.
          example: "+14155550100"
        mode:
          type: string
          enum:
            - code
            - qr
          default: code
          description: Login method. 'code' sends a one-time login code to the account; 'qr' returns a login token and QR URL to display for scanning.
        proxy_country:
          type: string
          description: Optional ISO 3166-1 alpha-2 country code for the outbound network route.
          example: NL
        force_new:
          type: boolean
          description: When true, discards any existing session and starts fresh.
        sub_account_id:
          type:
            - string
            - "null"
          description: Optional managed-account identifier to connect the channel on behalf of.
    TelegramConnectResponse:
      type: object
      properties:
        success:
          type: boolean
          example: true
        phone_number:
          type: string
          example: "+14155550100"
        status:
          type: string
          description: Session status after initiation. 'code_required' (code mode) means a one-time login code was sent to the account; 'qr_required' (qr mode) means a login token and QR URL were issued for scanning; 'initializing' means the session is still starting up.
          enum:
            - code_required
            - qr_required
            - initializing
          example: code_required
        session_id:
          type:
            - string
            - "null"
          description: Identifier of the started session, when available.
        login_token:
          type: string
          description: QR login token (qr mode only).
        qr_url:
          type: string
          description: Encoded QR login URL to display (qr mode only).
        connect_url:
          type:
            - string
            - "null"
          description: >-
            A hosted page that finishes the connection on its own — hand this link
            to the account holder instead of building your own UI and polling. In
            code mode they enter the login code (and a two-step verification
            password if the account has one); in qr mode it shows a refreshing QR
            to scan from the Telegram app. Null if the link could not be
            generated; the verify-code / verify-password / status endpoints (and
            qr_url in qr mode) remain available as a fallback. The link works for
            about 30 minutes (connect_url_expires_at).
          example: "https://api.dmchamp.com/v1/channels/telegram/connect/page?token=eyJhbGciOi..."
        connect_url_expires_at:
          type:
            - integer
            - "null"
          description: Unix epoch (ms) when connect_url stops working. Start a new connection to get a fresh link.
    TelegramVerifyCodeRequest:
      type: object
      required:
        - code
      properties:
        code:
          type: string
          description: The one-time login code received by the account.
          example: "12345"
        sub_account_id:
          type:
            - string
            - "null"
          description: Optional managed-account identifier.
    TelegramVerifyPasswordRequest:
      type: object
      required:
        - password
      properties:
        password:
          type: string
          description: The account's two-factor password.
        sub_account_id:
          type:
            - string
            - "null"
          description: Optional managed-account identifier.
    TelegramVerifyResponse:
      type: object
      properties:
        success:
          type: boolean
          example: true
        phone_number:
          type: string
          example: "+14155550100"
        status:
          type: string
          description: Verification result. 'connected' means the channel is live; 'password_required' means a two-factor password must be submitted next.
          enum:
            - connected
            - password_required
          example: connected
        telegram_user_id:
          type:
            - string
            - "null"
          description: The connected account identifier, when available.
        username:
          type:
            - string
            - "null"
          description: The connected account username, when set.
    TelegramStatusResponse:
      type: object
      properties:
        success:
          type: boolean
          example: true
        phone_number:
          type: string
          example: "+14155550100"
        status:
          type: string
          description: Current connection status.
          enum:
            - connected
            - code_required
            - password_required
            - initializing
            - disconnected
            - not_initialized
            - error
          example: connected
        telegram_user_id:
          type:
            - string
            - "null"
          description: The connected account identifier, when available.
        live:
          type: boolean
          description: True when the status was read live from the connection service.
          example: true
    TelegramDisconnectResponse:
      type: object
      properties:
        success:
          type: boolean
          example: true
        phone_number:
          type: string
          example: "+14155550100"
        status:
          type: string
          enum:
            - removed
          example: removed
    InstagramPrivateConnectRequest:
      type: object
      required:
        - username
        - password
      properties:
        username:
          type: string
          description: The Instagram account username to connect (the @handle, with or without the leading @).
          example: yourbrand
        password:
          type: string
          description: The Instagram account password. Relayed directly to Instagram via the connection worker and never stored.
          format: password
        proxy_country:
          type: string
          description: Optional ISO 3166-1 alpha-2 country code for the outbound network route.
          example: NL
        force_new:
          type: boolean
          description: When true, discards any existing session and starts fresh.
        sub_account_id:
          type:
            - string
            - "null"
          description: Optional managed-account identifier to connect the channel on behalf of.
    InstagramPrivateConnectResponse:
      type: object
      properties:
        success:
          type: boolean
          example: true
        account_id:
          type: string
          description: The normalized Instagram username (the connection identifier used by the other endpoints).
          example: yourbrand
        status:
          type: string
          description: Session status after initiation. 'connected' means login completed immediately; 'two_factor_required' means a two-factor code must be submitted next; 'challenge_required' means Instagram needs a checkpoint confirmation code; 'initializing' means the session is still starting up.
          enum:
            - connected
            - two_factor_required
            - challenge_required
            - initializing
          example: two_factor_required
        session_id:
          type:
            - string
            - "null"
          description: Identifier of the started session, when available.
        ig_user_id:
          type:
            - string
            - "null"
          description: The connected account's numeric Instagram user id, when available.
        username:
          type:
            - string
            - "null"
          description: The connected account's username, when set.
        connect_url:
          type:
            - string
            - "null"
          description: >-
            A hosted page that finishes the connection on its own — hand this link
            to the account holder instead of building your own credential form.
            They enter the Instagram username and password directly on the page
            (the credentials go straight to Instagram via DM Champ and are not
            stored), then a two-factor or confirmation code if Instagram asks for
            one. Null if the link could not be generated; the verify-2fa /
            verify-challenge / status endpoints remain available as a fallback. The
            link works for about 30 minutes (connect_url_expires_at).
          example: "https://api.dmchamp.com/v1/channels/instagram-private/connect/page?token=eyJhbGciOi..."
        connect_url_expires_at:
          type:
            - integer
            - "null"
          description: Unix epoch (ms) when connect_url stops working. Start a new connection to get a fresh link.
    InstagramPrivateVerifyRequest:
      type: object
      required:
        - code
      properties:
        code:
          type: string
          description: The two-factor or checkpoint confirmation code Instagram issued.
          example: "123456"
        sub_account_id:
          type:
            - string
            - "null"
          description: Optional managed-account identifier.
    InstagramPrivateVerifyResponse:
      type: object
      properties:
        success:
          type: boolean
          example: true
        account_id:
          type: string
          example: yourbrand
        status:
          type: string
          description: Verification result. 'connected' means the channel is live; 'two_factor_required' / 'challenge_required' mean a further code must be submitted next.
          enum:
            - connected
            - two_factor_required
            - challenge_required
          example: connected
        ig_user_id:
          type:
            - string
            - "null"
          description: The connected account's numeric Instagram user id, when available.
        username:
          type:
            - string
            - "null"
          description: The connected account's username, when set.
    InstagramPrivateStatusResponse:
      type: object
      properties:
        success:
          type: boolean
          example: true
        account_id:
          type: string
          example: yourbrand
        status:
          type: string
          description: Current connection status.
          enum:
            - connected
            - two_factor_required
            - challenge_required
            - initializing
            - disconnected
            - not_initialized
            - error
          example: connected
        ig_user_id:
          type:
            - string
            - "null"
          description: The connected account's numeric Instagram user id, when available.
        username:
          type:
            - string
            - "null"
          description: The connected account's username, when set.
        live:
          type: boolean
          description: True when the status was read live from the connection worker.
          example: true
    InstagramPrivateDisconnectResponse:
      type: object
      properties:
        success:
          type: boolean
          example: true
        account_id:
          type: string
          example: yourbrand
        status:
          type: string
          enum:
            - removed
          example: removed
    InstagramLoginTokenRequest:
      type: object
      required:
        - ig_user_id
        - access_token
      properties:
        ig_user_id:
          type: string
          description: >-
            The numeric Instagram professional account ID the token belongs to.
            This is the user_id field from GET https://graph.instagram.com/v21.0/me?fields=user_id,username
            — the same id Instagram webhooks carry as entry.id. It is NOT the
            app-scoped id (the id field from /me), which differs per Meta app and
            is rejected.
          pattern: "^[0-9]{5,25}$"
          example: "17841400000000000"
        access_token:
          type: string
          description: The Instagram user access token your own Meta app obtained for this account. Validated live against Instagram before it is stored.
          format: password
        expires_at:
          type:
            - string
            - "null"
          format: date-time
          description: When the token expires, as an ISO-8601 date-time. Takes precedence over expires_in.
          example: "2026-11-01T12:00:00.000Z"
        expires_in:
          type:
            - number
            - "null"
          description: Seconds until the token expires, as returned by Meta. Used when expires_at is omitted. With neither field, a 60-day lifetime is assumed.
          example: 5184000
        username:
          type: string
          description: Optional account username. Only used when Instagram does not return one for the token.
          example: yourbrand
        sub_account_id:
          type:
            - string
            - "null"
          description: Optional managed-account identifier to store the connection on behalf of.
    InstagramLoginTokenResponse:
      type: object
      properties:
        success:
          type: boolean
          example: true
        ig_user_id:
          type: string
          description: The connected Instagram professional account ID.
          example: "17841400000000000"
        username:
          type: string
          description: The connected account's username, as reported by Instagram.
          example: yourbrand
        expires_at:
          type: string
          format: date-time
          description: The expiry recorded for this token. Push a fresh token before it passes; these tokens are never refreshed for you.
          example: "2026-11-01T12:00:00.000Z"
        webhook_url:
          type:
            - string
            - "null"
          description: >-
            The Instagram webhook URL to configure in your own Meta app so
            messages reach this account. Null when no Instagram-Login app is
            configured yet on the account or its agency — the connection still
            sends, but nothing arrives until that setup is finished.
          example: "https://api.dmchamp.com/v1/incoming-instagram-login-message/byo/9fK2mQpX1cRt7yZbVn3L"
    InstagramLoginDisconnectResponse:
      type: object
      properties:
        success:
          type: boolean
          example: true
        disconnected:
          type: boolean
          description: True when a connection existed and was removed; false when there was nothing to disconnect.
          example: true
        ig_user_id:
          type:
            - string
            - "null"
          description: The Instagram account ID that was disconnected, or null when no connection existed.
          example: "17841400000000000"
    ErrorResponse:
      type: object
      properties:
        success:
          type: boolean
          example: false
        error:
          type: string
          description: Human-readable error message.
        error_code:
          type: integer
          description: HTTP status code, present on authentication, authorization, not-found, and rate-limit errors.
    WhatsAppConnectRequest:
      type: object
      required:
        - phone_number
      properties:
        phone_number:
          type: string
          description: The phone number to connect, in E.164 format.
          example: "+14155551234"
        only_waba_sharing:
          type: boolean
          description: Restrict the authorization to sharing an existing WhatsApp Business Account, skipping new sender setup.
          default: false
        retry:
          type: boolean
          description: Re-run the authorization for a number whose previous attempt did not complete, allowing the existing sender to be re-registered.
          default: false
        business_name:
          type: string
          maxLength: 256
          description: Cosmetic override for the business name shown on the consent screen only. Plain text. Not stored; the account's saved business name is used when omitted.
          example: Acme Studio
        description:
          type: string
          maxLength: 256
          description: Cosmetic override for the business description shown on the consent screen only. Plain text. Not stored; the account's saved description is used when omitted.
          example: Boutique hair studio in the city centre.
        sub_account_id:
          type:
            - string
            - "null"
          description: Identifier of a managed account to connect the number on behalf of. Omit to connect a number on the calling account.
    WhatsAppConnectResponse:
      type: object
      properties:
        success:
          type: boolean
          example: true
        status:
          type: string
          description: The connection step status. 'pending' means the consent URL must be opened in a browser to continue.
          enum:
            - pending
          example: pending
        oauth_url:
          type: string
          description: The consent URL to open in the account holder's browser to authorize the connection.
          example: https://www.facebook.com/v21.0/dialog/oauth?client_id=...&state=...
        state_token:
          type: string
          description: Opaque one-time token bound to this connection attempt. Treat as a short-lived secret; do not log it.
          example: 8sFq2yV0kQ7m4n1pZr3tWb6cXe9hJl2aD5gK7uN0oI
        expires_at:
          type: string
          format: date-time
          description: When the connection attempt expires and the consent URL stops working.
          example: "2026-06-10T12:30:00.000Z"
    WhatsAppConnectStatusResponse:
      type: object
      properties:
        success:
          type: boolean
          example: true
        phone_number:
          type: string
          example: "+14155551234"
        channel:
          type: string
          enum:
            - whatsapp
          example: whatsapp
        status:
          type: string
          description: Connection status. ONLINE means the number is connected and ready. PENDING means authorization succeeded but approval is still in progress. RATE_LIMITED means too many attempts were made; wait before retrying. REGISTRATION_FAILED means setup could not be completed. DELETED means the registration no longer exists.
          example: ONLINE
        status_reason:
          type:
            - string
            - "null"
          description: Human-readable detail for the current status, when available.
          example: null
        live:
          type: boolean
          description: True when the status was checked against the provider in real time, false when returned from the last cached state.
          example: true
    WhatsAppDisconnectResponse:
      type: object
      properties:
        success:
          type: boolean
          example: true
        phone_number:
          type: string
          example: "+14155551234"
        disconnected:
          type: boolean
          example: true
    WhatsAppConnectError:
      type: object
      properties:
        success:
          type: boolean
          example: false
        error_code:
          type: integer
          example: 404
        error:
          type: string
          example: Phone number not found
    WhatsAppWebConnectRequest:
      type: object
      required:
        - phone_number
      properties:
        phone_number:
          type: string
          description: The WhatsApp number to connect, in E.164 format (for example +15551230000).
          example: "+15551230000"
        proxy_country:
          type:
            - string
            - "null"
          description: Optional ISO 3166-1 alpha-2 country code used to pick the nearest routing region. Auto-detected from the number when omitted.
          example: us
        force_new:
          type: boolean
          description: Discard any existing session for this number and start a fresh pairing.
          default: false
        import_contacts:
          type: boolean
          description: Import the device's existing contacts on first connection.
          default: false
        pause_ai_for_imported_contacts:
          type: boolean
          description: When importing contacts, keep automated replies paused for them.
          default: true
        import_existing_chats:
          type: boolean
          description: Import existing chat history on first connection. Requires import_contacts to be true.
          default: false
        mark_imported_contacts_as_private:
          type: boolean
          description: When importing contacts, create them as private (no messages stored or sent for them). Incompatible with import_existing_chats.
          default: false
        sub_account_id:
          type:
            - string
            - "null"
          description: Optional identifier of a managed account to act on behalf of. Omit to act on your own account.
    WhatsAppWebConnectResponse:
      type: object
      properties:
        success:
          type: boolean
          example: true
        phone_number:
          type: string
          example: "+15551230000"
        session_id:
          type: string
          description: Identifier for the pairing session, used for support and correlation.
        status:
          type: string
          description: The pairing step. Begins at qr_pending while a QR code is being prepared.
          enum:
            - qr_pending
          example: qr_pending
        connect_url:
          type:
            - string
            - "null"
          description: >-
            A hosted web page that displays the pairing QR code and refreshes
            itself automatically until the number is connected. Give this link
            to the end user to open in a browser and scan with WhatsApp — it
            handles QR rotation and shows a success state on its own, so no
            polling is required. Null only if the link could not be generated
            (use the poll_qr_path/poll_status_path fallback in that case).
          example: "https://api.dmchamp.com/v1/channels/whatsapp-web/connect?token=eyJhbGciOi..."
        connect_url_expires_at:
          type:
            - integer
            - "null"
          description: Unix epoch (ms) when connect_url stops working. Start a new connection to get a fresh link.
        poll_qr_path:
          type: string
          description: Relative path to fetch the QR code for this connection (alternative to connect_url for integrations that render the QR themselves).
        poll_status_path:
          type: string
          description: Relative path to poll the connection status.
    WhatsAppWebQrResponse:
      type: object
      properties:
        success:
          type: boolean
          example: true
        phone_number:
          type: string
          example: "+15551230000"
        status:
          type: string
          enum:
            - qr_pending
          example: qr_pending
        qr_code:
          type:
            - string
            - "null"
          description: The raw QR payload string. Render it as a QR image for the end user to scan.
        qr_data_url:
          type:
            - string
            - "null"
          description: A data URL of the QR image that can be used directly as an image source.
        expires_at:
          type:
            - string
            - "null"
          format: date-time
          description: When this QR code expires. Restart the connection if it lapses before scanning.
    WhatsAppWebStatusResponse:
      type: object
      properties:
        success:
          type: boolean
          example: true
        phone_number:
          type: string
          example: "+15551230000"
        status:
          type: string
          description: Current connection state. connected (or open) is the terminal success state; disconnected and not_initialized are terminal failure states; qr_pending and connecting are transitional.
          enum:
            - not_initialized
            - qr_pending
            - connecting
            - connected
            - open
            - disconnected
          example: connected
        has_qr:
          type: boolean
          description: Whether a QR code is currently available to scan.
        qr_expires_at:
          type:
            - string
            - "null"
          format: date-time
        last_activity:
          type:
            - string
            - "null"
        message_count:
          type:
            - integer
            - "null"
        proxy:
          type:
            - string
            - "null"
        live:
          type: boolean
          description: True when the status was read from the connection service rather than a cached value.
    WhatsAppWebDisconnectResponse:
      type: object
      properties:
        success:
          type: boolean
          example: true
        phone_number:
          type: string
          example: "+15551230000"
        status:
          type: string
          enum:
            - removed
          example: removed
    WhatsAppWebError:
      type: object
      properties:
        success:
          type: boolean
          example: false
        error:
          type: string
        error_code:
          type: integer
    Agent:
      type: object
      description: |
        An AI Agent. The document IS the agent's configuration and is stored
        flat: every bot field (instructions, rules, goal, personality, active
        hours, and so on) lives at the top level, not nested under a bot object.

        Document reference fields are returned as path strings and timestamps as
        epoch milliseconds. Fields beyond the ones documented here may be present
        depending on how the agent was built.
      additionalProperties: true
      properties:
        id:
          type: string
          description: Agent ID
          examples: ["ag7HkQ2ZpLxR3mNb"]
        substrate_campaign_id:
          type: ['string', 'null']
          description: |
            Internal record kept alongside the agent on some older accounts, returned so you can find
            it again after the agent was created. Null on accounts that never had one, which is the
            normal case. You never need to act on it.
          examples: [null]
        name:
          type: ['string', 'null']
          description: Agent name, shown in the dashboard
          examples: ["Listing assistant"]
        active:
          type: boolean
          description: Whether the agent is currently allowed to reply. Defaults to true on create.
          examples: [true]
        language:
          type: ['string', 'null']
          description: Language the agent replies in
          examples: ["en"]
        instructions:
          type: string
          description: Primary instructions that steer how the agent talks to contacts
        rules:
          type: string
          description: Hard rules the agent must always follow
        goal:
          type: string
          description: The outcome the agent should work towards in each conversation
        personality:
          type: string
          description: Tone-of-voice and personality description for the agent
        availability:
          $ref: '#/components/schemas/AvailabilitySchedule'
        ai_speed:
          type: string
          enum: [fast, fast_thinker, balanced, thorough]
          description: Controls how much reasoning the agent applies before replying
        anthropic_model:
          type: string
          enum: [standard, economy, max, mini]
          description: AI quality tier used for this agent's replies
        max_messages:
          type: integer
          description: Maximum number of agent messages per conversation
        alert_human_when:
          type: string
          description: Conditions under which the agent should alert a human teammate
        ai_transparency:
          type: boolean
          description: Whether the agent discloses that it is an AI. Defaults to true on newly created agents.
        enable_bookings:
          type: boolean
          description: Whether the agent may book appointments
        booking_provider:
          type: string
          enum: [default, zenchef, formitable]
          description: Which appointment-booking integration the agent uses. "default" is the built-in booking flow.
        event:
          type: ['string', 'null']
          description: Path string of the bookable event type the agent books into. Set it with event_id on update.
          examples: ["events/7yTq2mLp0aBc9dEf"]
        enable_follow_ups:
          type: boolean
          description: Whether the agent sends follow-up messages
        follow_up_config:
          type: ['object', 'null']
          description: Follow-up behaviour configuration
          additionalProperties: true
        faq_refs:
          type: array
          description: FAQs linked to this agent, as path strings
          items:
            type: string
        kb_source_refs:
          type: array
          description: Knowledge-base sources linked to this agent, as path strings
          items:
            type: string
        custom_functions:
          type: array
          description: Custom functions the agent may call, as path strings
          items:
            type: string
          examples: [["users/9fQ2v1MzYhVv2S8kRq0b/custom_functions/cf_7Hk2ZpLx"]]
        mcp_servers:
          type: array
          description: MCP servers linked to this agent, as path strings
          items:
            type: string
        tags:
          type: array
          description: Tag automation entries the agent may apply during a conversation
          items:
            type: object
            additionalProperties: true
        version:
          type: integer
          description: Configuration format version. Newly created agents default to 3.
        created_at:
          type: ['integer', 'null']
          description: Creation time as epoch milliseconds
          examples: [1700000000000]
        last_modified_at:
          type: ['integer', 'null']
          description: Last update time as epoch milliseconds
          examples: [1700000000000]
    AgentSummary:
      type: object
      description: |
        A short summary of an AI Agent, returned by GET /agents when view=summary.

        Enough to pick the right agent - who it is, whether it is live, which language and AI tier it
        uses, whether it books or follows up, and how big its knowledge base is. The long parts of
        the configuration (instructions, rules, the generated question and company blocks, the FAQ
        and knowledge-source lists, saved test conversations, and page content read from your
        website) are left out on purpose: they make a full agent up to 580 KB and a full list several
        megabytes, which is more than most assistants and integrations can take in one response.

        Fetch one agent with GET /agents/{agentId} when you need the rest, optionally narrowed with
        the fields parameter.
      properties:
        id:
          type: string
          description: Agent ID
          examples: ["ag7HkQ2ZpLxR3mNb"]
        name:
          type: ['string', 'null']
          description: Agent name, shown in the dashboard
          examples: ["Listing assistant"]
        active:
          type: ['boolean', 'null']
          description: Whether the agent is currently allowed to reply
          examples: [true]
        language:
          type: ['string', 'null']
          description: Language the agent replies in
          examples: ["en"]
        goal:
          type: ['string', 'null']
          description: The outcome the agent works towards, shortened to the first 200 characters (a trailing ellipsis means it was shortened)
          examples: ["Book a viewing"]
        tags:
          type: ['array', 'null']
          description: Tag automation entries the agent may apply during a conversation
          items:
            type: object
            additionalProperties: true
        anthropic_model:
          type: ['string', 'null']
          description: AI quality tier used for this agent's replies
          examples: ["standard"]
        ai_speed:
          type: ['string', 'null']
          description: How much reasoning the agent applies before replying
          examples: ["balanced"]
        enable_bookings:
          type: ['boolean', 'null']
          description: Whether the agent may book appointments
          examples: [false]
        enable_follow_ups:
          type: ['boolean', 'null']
          description: Whether the agent sends follow-up messages
          examples: [true]
        faq_refs_count:
          type: integer
          description: How many FAQs are in this agent's knowledge base
          examples: [42]
        kb_source_refs_count:
          type: integer
          description: How many knowledge sources (pages, documents) are linked to this agent
          examples: [3]
        created_at:
          type: ['integer', 'null']
          description: Creation time as epoch milliseconds
          examples: [1700000000000]
        last_modified_at:
          type: ['integer', 'null']
          description: Last update time as epoch milliseconds
          examples: [1700000000000]
        substrate_campaign_id:
          type: string
          description: Only present on the older accounts that have one. Internal; you never need to act on it.
    AgentTagRule:
      type: object
      description: |
        One tagging rule on an agent: a tag, plus the wording that tells the
        agent when to apply it during a conversation. Applying a tag is what
        triggers any automation attached to it.
      additionalProperties: true
      required:
        - name
      properties:
        name:
          type: string
          description: The tag to apply
          examples: ["hot-lead"]
        description:
          type: string
          description: When the agent should apply this tag. Written as an instruction the agent follows, for example "Apply when the contact asks about pricing".
          examples: ["Apply when the contact asks about pricing or wants to book a call."]
        webhook:
          type: string
          description: Optional URL called when the agent applies this tag
        ai_can_remove:
          type: boolean
          description: Whether the agent may also take this tag off again. Defaults to false.
          examples: [false]
        tag_id:
          type: string
          description: |
            Optional ID of an existing tag on your account to link this rule to.
            Send it when adding or replacing a rule. In a response the link
            comes back as tag instead, holding the tag's path.
        tag:
          type: string
          description: Path of the linked tag, present in responses when the rule is linked to one
          examples: ["users/9fQ2v1MzYhVv2S8kRq0b/tags/tg8YuIoP2aSdF3gH"]
    EntryPoint:
      type: object
      description: |
        One routing rule: it says which agent takes a conversation, and when.

        The kinds are channel_default (answer every new contact on these
        channels), keyword (take over when the first message contains one of the
        keywords), instagram_comment and facebook_comment (reply to comments on
        your posts), and instagram_follower (greet new followers).

        Rules are retired rather than deleted, so a rule with enabled false is
        history, not a live setting. On a channel_default rule an empty agent_id
        means the channel is deliberately set to nobody.
      additionalProperties: true
      properties:
        id:
          type: string
          description: Entry point ID
          examples: ["ep3KmQ8vTzXr5nWd"]
        type:
          type: string
          enum: [channel_default, keyword, instagram_comment, facebook_comment, instagram_follower]
          description: What kind of rule this is
        channels:
          type: array
          description: The channels this rule covers
          items:
            type: string
          examples: [["whatsapp"]]
        agent_id:
          type: string
          description: The agent this rule routes to. Empty on a channel default that is deliberately set to nobody.
          examples: ["ag7HkQ2ZpLxR3mNb"]
        enabled:
          type: boolean
          description: False for a rule that has been retired and no longer fires
          examples: [true]
        match_config:
          type: object
          description: Type-specific matching settings. keywords and fuzzy_match on keyword rules; keywords, post_ids and delay_minutes on comment rules; phone_numbers on a channel default scoped to one connected number.
          additionalProperties: true
          properties:
            keywords:
              type: array
              description: Words that make a keyword rule fire. On a comment rule, the comments that count - empty means every comment on the watched posts.
              items:
                type: string
              examples: [["pricing", "quote"]]
            fuzzy_match:
              type: boolean
              description: Keyword rules. Defaults to true - a near miss ("info pls" against INFO) is resolved by AI. Set false for promo codes and SKUs where only the exact keyword should count.
            post_ids:
              type: array
              description: Comment rules. Only watch these posts; empty or absent means all posts.
              items:
                type: string
            delay_minutes:
              type: number
              description: Comment rules. Minutes to wait before the DM goes out.
            reply_instructions:
              type: string
              description: Comment and follower rules. How the agent should word its reply or outreach.
            phone_numbers:
              type: array
              description: Channel defaults. The connected number(s) this default is scoped to (E.164 with the leading +). Empty or absent means the whole channel. Set through phone_number on PUT /entry-points/channel-defaults.
              items:
                type: string
              examples: [["+31685101091"]]
        first_response_mode:
          type: ['string', 'null']
          enum: [ai, exact_text, null]
          description: |
            Reserved. Stored and returned so it round-trips, but not yet used to
            decide anything - the live setting is still the one on the campaign.
        first_response_exact_text:
          type: ['string', 'null']
          description: Reserved, alongside first_response_mode. Stored but not yet used.
        public_comment_reply_exact_text:
          type: ['string', 'null']
          description: Reserved, alongside first_response_mode. Stored but not yet used.
        created_at:
          type: ['integer', 'null']
          description: Creation time as epoch milliseconds
          examples: [1700000000000]
        last_modified_at:
          type: ['integer', 'null']
          description: Last update time as epoch milliseconds
          examples: [1700000000000]
    EntryPointWriteRequest:
      type: object
      description: |
        Fields accepted when creating or changing a routing rule. On create,
        type and channels are required. On update, send only what you are
        changing; the rule is re-checked as a whole so it can never be left
        unusable.
      additionalProperties: true
      properties:
        type:
          type: string
          enum: [channel_default, keyword, instagram_comment, facebook_comment, instagram_follower]
          description: What kind of rule this is
        channels:
          type: array
          description: The channels this rule covers. Must not be empty. A comment rule must list its own channel (instagram or facebook).
          minItems: 1
          items:
            type: string
          examples: [["whatsapp", "instagram"]]
        agent_id:
          type: string
          description: |
            Only on update, to hand the rule to a different agent of yours. On
            create the agent comes from the path and anything sent here is
            ignored. A blank value is rejected.
        enabled:
          type: boolean
          description: Set false to retire the rule without deleting it. New rules are enabled.
        match_config:
          type: object
          description: Type-specific matching settings. A keyword rule needs at least one entry in keywords; a comment rule may leave keywords empty to react to every comment on the watched posts.
          additionalProperties: true
          properties:
            keywords:
              type: array
              items:
                type: string
              examples: [["pricing", "quote"]]
            fuzzy_match:
              type: boolean
              description: Keyword rules. Defaults to true - a near miss ("info pls" against INFO) is resolved by AI. Set false for promo codes and SKUs where only the exact keyword should count.
            post_ids:
              type: array
              description: Comment rules. Only watch these posts; empty or absent means all posts.
              items:
                type: string
            delay_minutes:
              type: number
              description: Comment rules. Minutes to wait before the DM goes out.
            reply_instructions:
              type: string
              description: Comment and follower rules. How the agent should word its reply or outreach.
            phone_numbers:
              type: array
              description: Channel defaults. The connected number(s) this default is scoped to (E.164 with the leading +). Empty or absent means the whole channel. Set through phone_number on PUT /entry-points/channel-defaults.
              items:
                type: string
              examples: [["+31685101091"]]
        first_response_mode:
          type: ['string', 'null']
          enum: [ai, exact_text, null]
          description: Reserved. Accepted and stored, but not yet used to decide anything.
        first_response_exact_text:
          type: ['string', 'null']
          description: Reserved, alongside first_response_mode.
        public_comment_reply_exact_text:
          type: ['string', 'null']
          description: Reserved, alongside first_response_mode.
    MediaLibraryItem:
      type: object
      description: |
        One item in an AI media library - a file the agent is allowed to send
        during a conversation, together with the instruction that tells it when
        to send it.
      additionalProperties: true
      properties:
        id:
          type: string
          description: Item ID
          examples: ["mi_4RtY7uIo"]
        item_id:
          type: string
          description: Same value as id, echoed for symmetry with the write endpoints
          examples: ["mi_4RtY7uIo"]
        agent_id:
          type: string
          description: The agent the item is served for. Empty string when the item was addressed through a campaign.
          examples: ["ag7HkQ2ZpLxR3mNb"]
        campaign_id:
          type: string
          description: The campaign holding the item. Empty string for an item that lives on the agent itself.
          examples: [""]
        media_home:
          type: string
          enum: [agent, campaign]
          description: Where the item is actually stored. Items created through the agent endpoint are stored on the agent; older items may still sit on the campaign the agent was built from.
        title:
          type: string
          description: Short label shown in the library
          examples: ["Spring menu"]
        description:
          type: string
          description: The "when should the agent send this" instruction
          examples: ["Send when someone asks what is on the menu."]
        send_message:
          type: string
          description: Preferred wording the agent says when it sends the item. Empty string when not set.
        ai_description:
          type: string
          description: Auto-generated summary of what is actually in the file, produced at upload time. Empty string when it could not be generated.
        type:
          type: string
          enum: [image, video, document, audio]
          description: Media category, derived from the file type
        media_content_type:
          type: string
          description: MIME type of the stored file
          examples: ["image/png"]
        storage_path:
          type: string
          description: Storage path of the stored file
          examples: ["ai_media/agents/ag7HkQ2ZpLxR3mNb/image/spring-menu.png"]
        media_url:
          type: string
          description: |
            Signed download URL as it was minted at upload time. It expires 7 days
            after the upload, so on an older item this link may no longer work.
        is_voice_note:
          type: boolean
          description: True when the item is stored as a WhatsApp voice note
        is_animated_gif:
          type: boolean
          description: True when an animated GIF was converted to video on upload
        max_sends_per_conversation:
          type: ['integer', 'null']
          description: How many times the agent may send this item to the same contact in one conversation. Defaults to 1.
          examples: [1]
        created_at:
          type: ['integer', 'null']
          description: Creation time as epoch milliseconds
          examples: [1700000000000]
        created_by:
          type: string
          description: UID of the account the item was created under
    MediaLibraryUploadRequest:
      type: object
      description: |
        A media upload. The file is sent inline as base64, so keep it under the
        10 MB limit. The request is synchronous: it returns once the file is
        stored and described.
      required:
        - base64Data
        - mimeType
        - title
        - description
      properties:
        base64Data:
          type: string
          description: The file contents, base64 encoded, without a data URL prefix
        mimeType:
          type: string
          enum:
            - image/jpeg
            - image/png
            - image/gif
            - image/webp
            - video/mp4
            - video/quicktime
            - video/webm
            - application/pdf
            - audio/ogg
            - audio/mpeg
            - audio/wav
            - audio/mp4
            - audio/webm
          description: MIME type of the file. Animated GIFs are converted to video on upload.
          examples: ["image/png"]
        fileName:
          type: string
          description: Original filename, used to name the stored file
          examples: ["spring-menu.png"]
        title:
          type: string
          description: Short label shown in the library
          examples: ["Spring menu"]
        description:
          type: string
          description: The "when should the agent send this" instruction
          examples: ["Send when someone asks what is on the menu."]
        sendMessage:
          type: string
          description: Preferred wording the agent says when it sends the item. Trimmed to 500 characters.
        maxSendsPerConversation:
          type: integer
          minimum: 0
          description: How many times the agent may send this item to the same contact in one conversation. Defaults to 1.
        sendAsVoiceNote:
          type: boolean
          description: For audio uploads only - store the file as a WhatsApp voice note. Ignored for other file types.
    MediaLibraryUploadResponse:
      type: object
      properties:
        success:
          type: boolean
          examples: [true]
        itemId:
          type: string
          description: ID of the created item
          examples: ["mi_4RtY7uIo"]
        mediaUrl:
          type: string
          description: Signed download URL, valid for 7 days
        storagePath:
          type: string
          description: Storage path of the stored file
        mediaContentType:
          type: string
          description: MIME type of what was actually stored (an animated GIF reports as video)
        type:
          type: string
          enum: [image, video, document, audio]
          description: Media category
        isVoiceNote:
          type: boolean
          description: True when the file was stored as a WhatsApp voice note
        aiDescription:
          type: string
          description: Auto-generated summary of the file's contents. Empty string when it could not be generated.
    MediaLibraryItemUpdate:
      type: object
      description: Metadata-only edit. Send just the fields you want to change; the file itself cannot be replaced.
      properties:
        title:
          type: string
          description: Short label shown in the library
        description:
          type: string
          description: The "when should the agent send this" instruction
        send_message:
          type: string
          description: Preferred wording the agent says when it sends the item
        max_sends_per_conversation:
          type: ['integer', 'null']
          minimum: 0
          description: How many times the item may be sent to the same contact in one conversation. Pass null to clear the limit.
    CustomFunctionHeader:
      type: object
      description: One HTTP header sent with the function's request.
      required:
        - key
        - value
      properties:
        key:
          type: string
          examples: ["Authorization"]
        value:
          type: string
          examples: ["Bearer sk-live-..."]
    CustomFunctionInputParameter:
      type: object
      description: |
        One input the AI fills in when it calls the function. The name and
        description are what the AI reads to decide what to put there.
      required:
        - name
        - type
      properties:
        name:
          type: string
          description: Parameter name
          examples: ["order_id"]
        type:
          type: string
          enum: [string, number, boolean, object, array, query_param]
          description: Value type. Use query_param to have the value appended to the URL as a query string parameter instead of sent in the body.
        description:
          type: string
          description: What the AI should put in this parameter. Defaults to an empty string.
          examples: ["The order number the customer mentioned"]
        required:
          type: boolean
          description: Whether the AI must supply this parameter. Defaults to false.
        path:
          type: string
          description: Optional. Where the value is placed inside a nested request body.
        item_type:
          type: string
          enum: [string, number, boolean, object]
          description: Optional, and only for type array. The element type of the list - every value is converted to it before the request is sent. Omit to send the list's values unchanged.
    CustomFunction:
      type: object
      description: |
        A custom function definition. Timestamps are returned as epoch
        milliseconds. Fields beyond the ones documented here may be present on
        older functions.
      additionalProperties: true
      properties:
        id:
          type: string
          description: Custom function ID
          examples: ["cf7Hk2ZpLxR3mNbV"]
        custom_function_id:
          type: string
          description: Same value as id, echoed for symmetry with the write endpoints
          examples: ["cf7Hk2ZpLxR3mNbV"]
        name:
          type: string
          description: Function name shown to the AI
          examples: ["check_order_status"]
        description:
          type: string
          description: What the function does, used by the AI to decide when to call it
          examples: ["Looks up the status of an order by its order number."]
        url:
          type: string
          description: The endpoint the function calls
          examples: ["https://example.com/api/orders"]
        method:
          type: string
          enum: [GET, POST, PUT, DELETE, PATCH]
          description: HTTP method used when calling the endpoint
        ai_action:
          type: string
          description: Instruction telling the AI what to do with the result it gets back
          examples: ["Tell the customer the status and the expected delivery date."]
        skip_system_data:
          type: boolean
          description: When true, the contact and conversation details normally sent alongside your inputs are left out of the request.
        type:
          type: string
          enum: [http, automation]
          description: |
            "http" is a normal function that calls your endpoint, and is what an
            absent value means. "automation" is a function the automations
            builder created and keeps in sync - edit or delete the automation
            instead of the function.
        automation_id:
          type: string
          description: For an automation-managed function, the automation that owns it
        headers:
          type: array
          description: Headers sent with every call
          items:
            $ref: '#/components/schemas/CustomFunctionHeader'
        input:
          type: array
          description: Inputs the AI fills in when calling the function
          items:
            $ref: '#/components/schemas/CustomFunctionInputParameter'
        created_at:
          type: ['integer', 'null']
          description: Creation time as epoch milliseconds
          examples: [1700000000000]
        updated_at:
          type: ['integer', 'null']
          description: Last update time as epoch milliseconds
          examples: [1700000000000]
    CustomFunctionWrite:
      type: object
      description: |
        Fields accepted when creating or updating a custom function. Everything
        is optional - the dashboard creates an empty function first and fills it
        in afterwards - but headers and input replace the stored list in full
        when you send them.
      additionalProperties: true
      properties:
        name:
          type: string
          description: Function name shown to the AI
        description:
          type: string
          description: What the function does, used by the AI to decide when to call it
        url:
          type: string
          description: The endpoint the function calls
        method:
          type: string
          enum: [GET, POST, PUT, DELETE, PATCH]
          description: HTTP method used when calling the endpoint. Lower-case values are accepted and stored upper-case.
        ai_action:
          type: string
          description: Instruction telling the AI what to do with the result
        skip_system_data:
          type: boolean
          description: Leave out the contact and conversation details normally sent alongside your inputs. Must be a real boolean.
        type:
          type: string
          enum: [http, automation]
          description: Defaults to a normal HTTP function. Setting it to automation also requires automation_id.
        automation_id:
          type: string
          description: The automation that owns this function, when type is automation
        headers:
          type: array
          maxItems: 200
          description: Replaces the stored header list in full
          items:
            $ref: '#/components/schemas/CustomFunctionHeader'
        input:
          type: array
          maxItems: 200
          description: Replaces the stored input list in full
          items:
            $ref: '#/components/schemas/CustomFunctionInputParameter'
    SnapshotListEntry:
      type: object
      description: A snapshot as returned by the list endpoints - the captured bundles themselves are omitted because they can be large.
      properties:
        id:
          type: string
          examples: ["Qk3xR2mLp0aBc9dE"]
        name:
          type: string
          examples: ["Real-estate starter kit"]
        agent_names:
          type: array
          items:
            type: string
        counts:
          $ref: '#/components/schemas/SnapshotCounts'
        created_at:
          type: string
          format: date-time
          nullable: true
        is_default:
          type: boolean
          description: Whether new sub-accounts are born with this snapshot installed
    AnalyticsSeriesKey:
      type: object
      description: |
        The identity of one series. Only the keys for the requested group_by
        dimensions are present. A dimension whose value is unknown for a row
        (a message with no campaign, an event with no channel) is returned as
        null rather than dropped, so the series still add up to the totals.
      properties:
        campaign_id:
          type: ['string', 'null']
          description: Campaign the rows belong to. Present when group_by includes campaign.
        campaign_name:
          type: ['string', 'null']
          description: Current campaign name, resolved at read time.
        channel:
          type: ['string', 'null']
          description: Channel slug, for example whatsapp or instagram. Present when group_by includes channel.
        agent_id:
          type: ['string', 'null']
          description: Agent the rows belong to. Present when group_by includes agent.
        agent_name:
          type: ['string', 'null']
          description: Current agent name, resolved at read time.
        number:
          type: ['string', 'null']
          description: The account-side number, page or account the messages went through. Present when group_by includes number.
        number_label:
          type: ['string', 'null']
          description: Friendly connection name for that number, when one is configured.
    AnalyticsSeriesMetrics:
      type: object
      description: |
        One array per requested metric, each aligned one-to-one with the labels
        array in the response. Only the metrics you asked for are present.
      properties:
        sent:
          type: array
          items:
            type: integer
          description: Outbound messages accepted by the provider.
        delivered:
          type: array
          items:
            type: integer
          description: Outbound messages confirmed delivered.
        read:
          type: array
          items:
            type: integer
          description: Outbound messages confirmed read.
        replied:
          type: array
          items:
            type: integer
          description: Inbound messages from contacts.
        booked:
          type: array
          items:
            type: integer
          description: Appointments booked.
        contact_created:
          type: array
          items:
            type: integer
          description: New contacts created.
        credits_spent:
          type: array
          items:
            type: number
          description: Credits consumed.
    AnalyticsSeries:
      type: object
      description: One series in a series response.
      properties:
        key:
          $ref: '#/components/schemas/AnalyticsSeriesKey'
        total:
          type: number
          description: Sum of every requested metric across the whole range. This is the value the limit ranks on.
        metrics:
          $ref: '#/components/schemas/AnalyticsSeriesMetrics'
    AnalyticsSeriesOtherBucket:
      type: object
      description: |
        Everything past the limit, collapsed into one row. Computed as the range
        total minus the returned series, so a chart that renders series plus
        other_bucket always adds up to the account's real numbers. Null when
        nothing was collapsed.
      properties:
        series_count:
          type: integer
          description: How many distinct series were folded in.
        total:
          type: number
        metrics:
          $ref: '#/components/schemas/AnalyticsSeriesMetrics'
    AnalyticsOutcomeTag:
      type: object
      description: Conversation outcomes for one AI-assigned tag.
      properties:
        tag:
          type: ['string', 'null']
          description: The outcome tag, or null for conversations the AI never classified.
        total:
          type: integer
          description: Conversations carrying this tag across the whole range.
        counts:
          type: array
          items:
            type: integer
          description: Per-day counts aligned to labels. Empty when group_by is tag.
    AgencyRollupSubAccount:
      type: object
      description: Range totals plus a per-day series for one sub-account.
      properties:
        uid:
          type: string
          description: The sub-account's account ID.
        name:
          type: string
          description: Display name of the sub-account.
        email:
          type: ['string', 'null']
        sent:
          type: integer
        replied:
          type: integer
        booked:
          type: integer
        contacts:
          type: integer
          description: Contacts created in the range.
        credits_spent:
          type: number
        points:
          type: object
          description: Per-day arrays aligned one-to-one with labels.
          properties:
            sent:
              type: array
              items:
                type: integer
            replied:
              type: array
              items:
                type: integer
            booked:
              type: array
              items:
                type: integer
            contacts:
              type: array
              items:
                type: integer
            credits_spent:
              type: array
              items:
                type: number
    DashboardMetricSeries:
      type: object
      description: Five per-day metric arrays, each aligned one-to-one with the labels array.
      properties:
        sent:
          type: array
          items:
            type: integer
        delivered:
          type: array
          items:
            type: integer
        read:
          type: array
          items:
            type: integer
        replies:
          type: array
          items:
            type: integer
          description: Inbound messages from contacts.
        contacts:
          type: array
          items:
            type: integer
          description: Distinct contacts active that day.
    DashboardNumberStat:
      type: object
      description: Exact message totals for one connection (number, page or account) over the range.
      properties:
        channel:
          type: string
        identity:
          type: ['string', 'null']
          description: The account-side number, page or account ID.
        label:
          type: ['string', 'null']
          description: Friendly connection name, when one is configured.
        inbound:
          type: integer
        outbound:
          type: integer
        aiReplies:
          type: integer
          description: Outbound messages sent by the AI.
        manualSends:
          type: integer
          description: Outbound messages sent manually from the inbox.
        failed:
          type: integer
          description: Outbound messages that failed or were undelivered.
        contacts:
          type: integer
          description: Distinct contacts with any message on this connection in the range.
        lastMessageAt:
          type: ['string', 'null']
          format: date-time
    DashboardInsights:
      type: object
      description: |
        The full dashboard insights payload. Several blocks are computed from the
        reporting database and are returned as null when it cannot answer for
        this account. Null means unavailable, not zero activity, so never render
        a null block as an empty chart.
      properties:
        heatmap:
          type: object
          description: Reply rate by weekday and hour.
          properties:
            buckets:
              type: array
              items:
                type: object
                properties:
                  weekday:
                    type: integer
                    description: 0 is Sunday through 6 is Saturday.
                  hour:
                    type: integer
                    description: Hour of day, 0 to 23.
                  sent:
                    type: integer
                  replied:
                    type: integer
                  replyRate:
                    type: number
                    description: replied divided by sent, between 0 and 1. Zero when nothing was sent.
        topCampaigns:
          type: array
          description: Leaderboard rows, highest volume first.
          items:
            type: object
            properties:
              campaignId:
                type: string
              name:
                type: string
              sent:
                type: integer
              replied:
                type: integer
              booked:
                type: integer
              replyRate:
                type: number
              creditsSpent:
                type: number
        channelVolume:
          type: array
          description: Sampled per-channel volume. Prefer numberStats when it is not null.
          items:
            type: object
            properties:
              channel:
                type: string
              sent:
                type: integer
              received:
                type: integer
              lastMessageAt:
                type: ['string', 'null']
                format: date-time
        inboxSla:
          type: object
          description: How quickly the account answers a first inbound message.
          properties:
            medianFirstResponseMs:
              type: ['integer', 'null']
              description: Median first-inbound to first-outbound delta in milliseconds. Null when there is no sample.
            sampleSize:
              type: integer
        activityFeed:
          type: array
          description: Recent notable events, newest first.
          items:
            type: object
            properties:
              id:
                type: string
              kind:
                type: string
                enum: ['reply', 'sent', 'booked', 'contact_created', 'human_alert']
              at:
                type: string
                format: date-time
              contactId:
                type: ['string', 'null']
              contactName:
                type: ['string', 'null']
              campaignId:
                type: ['string', 'null']
              campaignName:
                type: ['string', 'null']
              label:
                type: string
        numberStats:
          type: ['array', 'null']
          description: Exact per-connection totals, or null when unavailable.
          items:
            $ref: '#/components/schemas/DashboardNumberStat'
        channelDailySeries:
          type: ['object', 'null']
          description: Per-day message counts, one line per channel, or null when unavailable.
          properties:
            labels:
              type: array
              items:
                type: string
                format: date
            series:
              type: array
              items:
                type: object
                properties:
                  channel:
                    type: string
                  total:
                    type: integer
                  points:
                    type: array
                    items:
                      type: integer
        metricDailyBreakdown:
          type: ['object', 'null']
          description: Per-day metrics account wide, per channel and per number, or null when unavailable.
          properties:
            labels:
              type: array
              items:
                type: string
                format: date
            all:
              $ref: '#/components/schemas/DashboardMetricSeries'
            channels:
              type: array
              items:
                type: object
                properties:
                  channel:
                    type: string
                  total:
                    type: integer
                  metrics:
                    $ref: '#/components/schemas/DashboardMetricSeries'
            numbers:
              type: array
              items:
                type: object
                properties:
                  channel:
                    type: string
                  identity:
                    type: ['string', 'null']
                  label:
                    type: ['string', 'null']
                  total:
                    type: integer
                  metrics:
                    $ref: '#/components/schemas/DashboardMetricSeries'
        contactsByCountry:
          type: ['object', 'null']
          description: |
            Where contacts created in the range are from. Contacts carry no
            country field, so it is derived from the phone dial code. Contacts
            with no phone number, or with a non-phone channel ID, are counted
            honestly under unknown. Null when unavailable.
          properties:
            countries:
              type: array
              items:
                type: object
                properties:
                  country:
                    type: string
                    description: ISO 3166-1 alpha-2 country code.
                  contacts:
                    type: integer
            unknown:
              type: integer
            total:
              type: integer
              description: All contacts created in the range, known plus unknown.
    AnalyticsAggregateCounts:
      type: object
      description: |
        Aggregated event counters in the camelCase form the in-app dashboard
        parses. The snake_case equivalents are on GET /analytics/summary.
      properties:
        total:
          type: integer
        sent:
          type: integer
        delivered:
          type: integer
        read:
          type: integer
        replied:
          type: integer
        booked:
          type: integer
        contactCreated:
          type: integer
        creditsSpent:
          type: number
        creditsRecharged:
          type: number
    ScopedApiKeyScopes:
      type: object
      description: |
        What a scoped key is allowed to do. The restriction travels WITH the credential, so handing
        someone a key is the same act as deciding what they may reach.
      properties:
        read_only:
          type: boolean
          description: When true (the default), only GET requests are permitted - any write is rejected with 403.
          examples: [true]
        tags:
          type: array
          items:
            type: string
          description: |
            Allowlist of API section names this key may use, using the same tag names shown in these
            docs (Analytics, Campaigns, Contacts, ...). An empty list means every section.
          examples: [["Analytics"]]
        sub_account_ids:
          type: array
          items:
            type: string
          description: |
            Sub-accounts this key may act on. Empty means your own account only; ["*"] means any
            sub-account you own. Ownership is still verified on every request.
          examples: [[]]
        rate_limit_per_min:
          type: integer
          description: Requests per minute allowed for this key, counted in its own budget so it cannot exhaust your other integrations.
          examples: [60]
    ScopedApiKey:
      type: object
      description: A scoped API key. The key value itself is only ever returned once, when it is created.
      properties:
        id:
          type: string
          description: Identifier of the key. Safe to store and display - it is not the key itself.
          examples: ["key_9f2c1a7b4d6e8f0a1b2c3d4e5f60718a"]
        label:
          type: ['string', 'null']
          description: Your own label for the key.
          examples: ["Client dashboard - Acme"]
        key_preview:
          type: ['string', 'null']
          description: Masked hint of the key - the first 4 and last 4 characters.
          examples: ["abcd...qrst"]
        scopes:
          $ref: '#/components/schemas/ScopedApiKeyScopes'
        expires_at:
          type: ['string', 'null']
          format: date-time
          description: When the key stops working. Null means it never expires on its own.
          examples: ["2026-12-31T23:59:59.000Z"]
        last_used_at:
          type: ['string', 'null']
          format: date-time
          description: When the key was last used to authenticate.
          examples: [null]
        created_at:
          type: ['string', 'null']
          format: date-time
          examples: ["2026-08-14T09:12:00.000Z"]
        revoked_at:
          type: ['string', 'null']
          format: date-time
          description: When the key was revoked. Revoked keys are kept in the list so you can see what was withdrawn.
          examples: [null]
        revoked:
          type: boolean
          examples: [false]
    ScopedApiKeyList:
      type: object
      properties:
        success:
          type: boolean
          examples: [true]
        api_keys:
          type: array
          items:
            $ref: '#/components/schemas/ScopedApiKey'
    ScopedApiKeyCreateRequest:
      type: object
      properties:
        label:
          type: string
          description: Your own label for the key, shown in Settings.
          examples: ["Client dashboard - Acme"]
        scopes:
          $ref: '#/components/schemas/ScopedApiKeyScopes'
        expires_at:
          type: ['string', 'null']
          format: date-time
          description: Optional expiry, ISO 8601. Must be in the future.
          examples: ["2026-12-31T23:59:59.000Z"]
    ScopedApiKeyCreateResponse:
      type: object
      properties:
        success:
          type: boolean
          examples: [true]
        api_key:
          type: string
          description: The new key. This is the ONLY time it is returned - store it now, it cannot be retrieved again.
          examples: ["abcdEFGH1234ijkl5678MNOP9012qrst"]
        key:
          $ref: '#/components/schemas/ScopedApiKey'
        message:
          type: string
          examples: ["Store this key now - it is shown once and cannot be retrieved again."]
    ScopedApiKeyUpdateRequest:
      type: object
      description: Any combination of the three fields. Scopes are REPLACED, not merged.
      properties:
        label:
          type: ['string', 'null']
          examples: ["Client dashboard - Acme (read-only)"]
        scopes:
          $ref: '#/components/schemas/ScopedApiKeyScopes'
        expires_at:
          type: ['string', 'null']
          format: date-time
          examples: ["2027-01-31T23:59:59.000Z"]
    ScopedApiKeyUpdateResponse:
      type: object
      properties:
        success:
          type: boolean
          examples: [true]
        key:
          $ref: '#/components/schemas/ScopedApiKey'
    ScopedApiKeyRevokeResponse:
      type: object
      properties:
        success:
          type: boolean
          examples: [true]
        revoked:
          type: boolean
          examples: [true]
        id:
          type: string
          examples: ["key_9f2c1a7b4d6e8f0a1b2c3d4e5f60718a"]
        message:
          type: string
          examples: ["API key revoked. All requests using it will be rejected immediately."]
    AgencyPricingTierFields:
      type: object
      description: |
        The editable fields of one plan in your SaaS-mode price list. Used whole on POST (where
        credits, price_cents and currency are required) and partially on PATCH.

        Fields not listed here are rejected with a 400 that names them; `stripe_price_id` and
        `stripe_product_id` are managed for you (created, recreated and archived on your own
        Stripe account) and cannot be set.
      properties:
        credits:
          type: integer
          minimum: 1
          description: |
            Credits the plan includes PER MONTH on the month/year cadences (a yearly plan grants one
            month at checkout and drips the rest), or per BILLING PERIOD on the week cadence.
          examples: [1000]
        price_cents:
          type: integer
          minimum: 1
          description: |
            Price per billing interval in the currency's smallest unit (a yearly plan's value is the
            whole yearly amount). Must stay above the platform's minimum price per credit — the
            error message tells you the minimum for your credits and currency.
          examples: [4900]
        currency:
          type: string
          description: ISO 4217 code, lowercased on save.
          enum: [usd, eur, gbp, cad, aud, jpy, chf, sek, nok, dkk, sgd, hkd, mxn, brl, cop, gtq, ils, omr, thb]
          examples: ["usd"]
        label:
          type: string
          maxLength: 100
          description: Plan name shown on the Stripe checkout page. Defaults to e.g. "1000 Credits Monthly".
          examples: ["Growth"]
        description:
          type: string
          maxLength: 500
          description: Custom description shown on the Stripe checkout page.
        features:
          type: array
          items:
            type: string
          description: |
            Feature IDs granted to a client that subscribes to this plan. Reselling Find Leads,
            Social Scheduler or SEO Agent requires holding that product on your own account.
          examples: [["whatsapp", "instagram", "team_seats_3"]]
        feature_settings:
          type: object
          description: |
            Explicit on/off intent for the three default-ON toggles. A key left out keeps the
            default-ON behaviour — absence is never read as "off".
          properties:
            tasks:
              type: boolean
            daily_summaries:
              type: boolean
            ai_media_library:
              type: boolean
        billing_interval:
          type: string
          enum: [month, year, week]
          description: |
            Billing cadence. ABSENT means `month` (what every plan created before this field is).
            Changing it on a saved plan recreates the Stripe price, because interval is immutable on
            a Stripe price; existing subscribers stay on the old price.
        billing_interval_count:
          type: integer
          minimum: 1
          maximum: 52
          description: |
            How many intervals make one billing period — only meaningful with `billing_interval:
            week` (e.g. 4 = billed every 4 weeks). ABSENT means 1. Saving a month/year plan strips
            the key. Changing it recreates the Stripe price.
        trial_days:
          type: integer
          minimum: 0
          maximum: 90
          description: |
            Free-trial length in days. ABSENT or 0 means NO trial. Maximum 90. Applied as the Stripe
            checkout session's trial period, so the client is only charged when the trial ends.
          examples: [14]
        trial_credits:
          type: integer
          minimum: 1
          description: |
            Credits granted when the trial STARTS. ABSENT defaults to the plan's `credits` (a trial
            hands over a full allowance). Minimum 1. Only meaningful with `trial_days > 0` — saving
            a plan without a trial strips the key. These credits come out of YOUR credit pool, the
            same as a paid purchase.
        trial_card_required:
          type: boolean
          description: |
            Whether Stripe collects a card up front for the trial. ABSENT means TRUE. `false` runs a
            no-card trial: Stripe collects no payment method and the plan CANCELS ITSELF at trial end
            if the client never adds one. Only meaningful with `trial_days > 0` — saving a plan
            without a trial strips the key. Editing it does NOT recreate the Stripe price.
        trial_hard_expiry:
          type: boolean
          description: |
            What happens when a trial ends without converting. ABSENT means FALSE — the plan is just
            marked canceled and the client keeps any leftover trial credits. `true` additionally
            REVOKES the unused trial credits back into your pool and blocks the account (the block is
            lifted automatically if the client later buys a paid plan). Only meaningful with
            `trial_days > 0` — saving a plan without a trial strips the key. Editing it does NOT
            recreate the Stripe price.
        white_label_config:
          type: ['string', 'null']
          description: |
            The id of one of YOUR white-label configs (the id is the domain) this plan is sold on.
            Absent/null means the plan is sold on your primary domain. Drives the plan's checkout
            link, the checkout branding, and which brand the client's account is pinned to.
          examples: ["clients.acme.com"]
        team_seats_limit:
          type: ['integer', 'null']
          minimum: -1
          description: |
            Team-seat cap applied to a client on this plan: 0 = no team members, -1 = unlimited,
            N = exactly N seats. Absent/null leaves seats to the plan's `team_seats_*` features.
        agent_limit:
          type: ['integer', 'null']
          minimum: -1
          description: |
            AI-agent cap applied to a client on this plan: 0 = the client cannot create AI agents,
            -1 = unlimited, N = at most N agents. Absent/null means this plan does not manage the
            cap. Enforced when the client creates or duplicates an AI agent.
        channel_limit:
          type: ['integer', 'null']
          minimum: -1
          description: |
            Connected-channel cap applied to a client on this plan: 0 = the client cannot connect
            any messaging channel, -1 = unlimited, N = at most N connected channels. Absent/null
            means this plan does not manage the cap (the client falls back to its
            `channels_1`/`channels_3`/`channels_unlimited` feature, which stays the
            presence gate). Enforced on every connect path. Instagram and Messenger share ONE page
            connection and therefore count as ONE channel.
    AgencyPricingTierInput:
      description: A whole plan, as sent to POST /agency/pricing-tiers.
      allOf:
        - $ref: '#/components/schemas/AgencyPricingTierFields'
        - type: object
          required:
            - credits
            - price_cents
            - currency
    AgencyPricingTier:
      description: A stored plan, as returned by the pricing-tier endpoints.
      allOf:
        - $ref: '#/components/schemas/AgencyPricingTierFields'
        - type: object
          properties:
            tierIndex:
              type: integer
              minimum: 0
              description: |
                Zero-based position of this plan in your price list. This is what a checkout link
                carries and what identifies the plan in PATCH/DELETE. Deleting a plan shifts every
                later plan down by one, so re-read the list after a delete.
              examples: [0]
            stripe_price_id:
              type: string
              description: The Stripe Price created on YOUR Stripe account for this plan (read-only).
              examples: ["price_1P0abcDEfGhIjKlM"]
            stripe_product_id:
              type: string
              description: The Stripe Product created on YOUR Stripe account for this plan (read-only).
              examples: ["prod_P0abcDEfGhIjKl"]
            checkout_url:
              type: string
              format: uri
              description: |
                The shareable payment link for this plan. Hosted on the plan's own white-label domain
                when `white_label_config` is set, otherwise on your primary white-label domain (or
                the platform app domain when you have none).
              examples: ["https://clients.acme.com/v1/checkout?id=fHjQASIIzfUcrvuAvaYWdDd65Bu2&tierIndex=0"]
    AgencyCreditPriceResponse:
      description: The per-credit price a client pays on a custom top-up, as returned by the credit-price endpoints.
      type: object
      properties:
        success:
          type: boolean
          examples: [true]
        data:
          type: object
          properties:
            price_per_credit_cents:
              type: ['integer', 'null']
              description: Price of ONE credit in the smallest unit of the currency. Null when no price is set.
              examples: [25]
            price_per_credit_currency:
              type: ['string', 'null']
              description: Currency the price is charged in (ISO 4217, lowercase). Null when no price is set.
              examples: [usd]
            note:
              type: ['string', 'null']
              description: The short line shown under the price on your client's top-up page. Null when none is set.
              examples: ["R$1.25 = USD 0.25 at our reference rate"]
            minimum_cents:
              type: ['integer', 'null']
              description: |
                The lowest price you may charge per credit in the current currency. Null while no
                price is set, because there is no currency to quote a minimum in yet.
              examples: [10]
    ByokSmsCredentialsInput:
      type: object
      description: Your own Twilio credentials for sending SMS from your Twilio account.
      required:
        - accountSid
        - authToken
      properties:
        accountSid:
          type: string
          description: Twilio Account SID. Starts with AC followed by 32 hexadecimal characters.
          examples: ["ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"]
        authToken:
          type: string
          format: password
          description: Twilio Auth Token. At least 20 characters. Stored securely and never returned by any endpoint.
        phoneNumber:
          type: string
          description: Optional default sending number in E.164 format.
          examples: ["+31612345678"]
    WhatsAppWebSettingsInput:
      type: object
      description: |
        WhatsApp Web sending limits. Every field is optional; only the fields you
        send are changed. These are the soft limits shown in the app. The service
        also applies its own warm-up limits at send time.
      properties:
        dailyNewContactLimit:
          type: integer
          minimum: 0
          maximum: 10000
          description: How many new contacts may be messaged per day.
          examples: [50]
        monthlyNewContactLimit:
          type: integer
          minimum: 0
          maximum: 100000
          description: How many new contacts may be messaged per month.
          examples: [1000]
        delayBetweenNewMessages:
          type: integer
          minimum: 0
          maximum: 3600
          description: Base pause between two messages to new contacts, in seconds.
          examples: [45]
        delayRandomVariation:
          type: integer
          minimum: 0
          maximum: 3600
          description: Extra random pause added on top of the base delay, in seconds.
          examples: [15]
        warningThreshold:
          type: number
          minimum: 0
          maximum: 1
          description: Share of the limit at which the account is warned. For example 0.8 warns at 80 percent.
          examples: [0.8]
        enabled:
          type: boolean
          description: Whether the limits are enforced.
          examples: [true]
    MetaAppWebhookUrls:
      type: object
      description: The callback URLs to paste into your own Meta app, one per Meta product.
      properties:
        instagram:
          type: string
          description: Callback URL for the Instagram product (Facebook Login set). Only present when Facebook Login credentials are stored.
          examples: ["https://api.dmchamp.com/v1/incoming-instagram-message/byo/user_abc123"]
        messenger:
          type: string
          description: Callback URL for the Messenger product (Facebook Login set). Only present when Facebook Login credentials are stored.
          examples: ["https://api.dmchamp.com/v1/incoming-messenger-message/byo/user_abc123"]
        instagram_login:
          type: string
          description: |
            Callback URL for the "Instagram API with Instagram login" product.
            Only present when Instagram login credentials are stored on the account.
          examples: ["https://api.dmchamp.com/v1/incoming-instagram-login-message/byo/user_abc123"]
    MetaAppConfigInput:
      type: object
      description: |
        Your own Meta app. Two independent credential sets share this one
        setting, and a save needs at least one complete set. The Facebook Login
        set (app_id, app_secret, config_id) shows YOUR brand on the Facebook
        consent screen for Instagram and Messenger connections; its App ID and
        App Secret pair is checked against Meta before it is stored. The
        Instagram login set (instagram_app_id, instagram_app_secret) enables the
        Instagram login token push lane and needs no Facebook fields at all.
        Whichever set a save leaves out is removed from the stored settings, so
        always send everything you want kept.
      properties:
        app_id:
          type: string
          description: The numeric Meta App ID from your app's basic settings. Part of the Facebook Login set, which is sent complete or not at all.
          examples: ["1234567890123456"]
        app_secret:
          type: string
          format: password
          description: The Meta App Secret, between 16 and 128 characters. Stored encrypted and never returned. Part of the Facebook Login set.
        config_id:
          type: string
          description: |
            The numeric Facebook Login for Business configuration ID created inside
            YOUR app. Configuration IDs belong to one app, so another app's ID cannot be reused.
            Part of the Facebook Login set.
          examples: ["9876543210987654"]
        instagram_app_id:
          type: string
          description: |
            The numeric Instagram App ID from your app's "Instagram API with
            Instagram login" panel. This is not the Facebook App ID. Must be sent
            together with instagram_app_secret, or both left out.
          examples: ["2233445566778899"]
        instagram_app_secret:
          type: string
          format: password
          description: |
            The Instagram App Secret that belongs with instagram_app_id, between
            16 and 128 characters. Stored encrypted and never returned. Unlike
            the Facebook pair it is not checked against Meta when saved.
    MetaAppConfigResponse:
      type: object
      description: The stored summary of your own Meta app. The app secrets are never included.
      properties:
        success:
          type: boolean
          examples: [true]
        configured:
          type: boolean
          description: False when no own Meta app is stored. All other fields are then absent.
          examples: [true]
        app_id:
          type: string
          description: Only present when Facebook Login credentials are stored.
          examples: ["1234567890123456"]
        config_id:
          type: string
          description: Only present when Facebook Login credentials are stored.
          examples: ["9876543210987654"]
        verify_token:
          type: string
          description: |
            The verify token to enter alongside the callback URLs when you set up
            the webhook subscription inside your Meta app. Generated on the first
            save and kept unchanged afterwards, so an existing subscription keeps working.
          examples: ["a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718"]
        instagram_app_id:
          type: string
          description: Only present when Instagram login credentials are stored.
          examples: ["2233445566778899"]
        webhook_urls:
          $ref: '#/components/schemas/MetaAppWebhookUrls'
    MetaAppSaveResponse:
      type: object
      description: The stored summary plus the setup steps to finish inside your Meta app.
      properties:
        success:
          type: boolean
          examples: [true]
        app_id:
          type: string
          description: Only present when Facebook Login credentials are stored.
          examples: ["1234567890123456"]
        config_id:
          type: string
          description: Only present when Facebook Login credentials are stored.
          examples: ["9876543210987654"]
        verify_token:
          type: string
          description: The verify token to use for the webhook subscription in your Meta app.
          examples: ["a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718"]
        instagram_app_id:
          type: string
          description: Only present when Instagram login credentials were supplied.
          examples: ["2233445566778899"]
        webhook_urls:
          $ref: '#/components/schemas/MetaAppWebhookUrls'
        setup:
          type: array
          description: Plain-language steps to complete inside your Meta app after saving.
          items:
            type: string
    ProxyLocation:
      type: object
      description: One country a WhatsApp Web connection can be routed through.
      properties:
        code:
          type: string
          description: ISO 3166-1 alpha-2 country code.
          examples: ["nl"]
        name:
          type: string
          examples: ["Netherlands"]
        flag:
          type: string
          description: Flag emoji for the country.
        available:
          type: boolean
          description: Whether the location can currently be selected.
          examples: [true]
        region:
          type: string
          description: Grouping used by the app's location picker. Not always present.
          examples: ["europe"]
    BroadcastOpenerTemplate:
      type: object
      description: |
        The broadcast's single opening message. On WhatsApp Business this is a
        real template that must be approved before the broadcast can launch. On
        every other channel the same body field is reused verbatim as the plain
        opening text, so there is no separate plain-text field. Additional
        provider bookkeeping fields are returned as stored.
      additionalProperties: true
      properties:
        body:
          type: string
          description: The opening message text. Placeholders are written in double curly braces.
          examples: ["Hi {{first_name}}, our spring menu just went live."]
        name:
          type: ['string', 'null']
          description: Template name registered with the provider.
        language:
          type: ['string', 'null']
          description: Template language code. Defaults to en.
          examples: ["en"]
        category:
          type: string
          enum: [marketing, utility, authentication]
          description: WhatsApp template category. Defaults to marketing when unset.
        status:
          type: string
          description: |
            Approval status of the template. Only an approved template lets a
            WhatsApp Business broadcast launch; a rejected one must be
            resubmitted; anything else means approval is still in flight.
          examples: ["approved"]
        sid:
          type: ['string', 'null']
          description: Provider content id for the submitted template. Null when the template was never submitted to a provider.
        variables:
          type: ['array', 'null']
          description: |
            Ordered list of the placeholder names used in the body. Stored on
            the broadcast because the send path resolves each contact's values
            in this order.
          items:
            type: string
        rejection_reason:
          type: ['string', 'null']
          description: Provider reason when the template was rejected.
        in_transition:
          type: boolean
          description: True while a replacement template is awaiting approval and the previously approved one is still live.
        waba_registered:
          type: boolean
          description: |
            Whether the owner's WhatsApp Business Account actually holds this
            template according to Meta. Absent means never checked. An explicit
            false blocks the send.
        media_url:
          type: ['string', 'null']
          description: Long-lived URL of the media baked into the approved template. Null for a text-only template. Server-written, never accepted from a request.
        media_storage_path:
          type: ['string', 'null']
          description: |
            Which stored file the approved template's media points at. The
            launch check compares this against the broadcast's current
            attachment and refuses the send when they differ.
        media_content_type:
          type: ['string', 'null']
          description: MIME type of the media inside the approved template. Null when text-only.
    BroadcastOpenerMedia:
      type: object
      description: |
        One image or video sent with the opening message. Absent or null means
        a text-only broadcast. Always written as a whole object: writing an
        individual field of it with a dotted key is rejected, because a partial
        write could describe a file that is not the one stored.

        Channel reality: WhatsApp Web sends it as the first chunk's attachment,
        WhatsApp Business bakes it into the approved template (so the template
        has to be resubmitted after any change), and text messages cannot carry
        it at all.
      required: [storage_path, media_content_type, type, name, source]
      properties:
        storage_path:
          type: string
          description: Storage path of the file. This is the attachment's identity; never a signed URL, which would expire before a scheduled send.
          examples: ["chat-media/abc123/spring-menu.jpg"]
        media_content_type:
          type: string
          description: Full MIME type of the file.
          examples: ["image/jpeg"]
        type:
          type: string
          enum: [image, video]
          description: Coarse media kind, derived from the MIME type.
        name:
          type: string
          description: Display name, usually the original filename or the media library item's title.
        source:
          type: object
          description: Where the attachment came from. Purely descriptive.
          additionalProperties: true
          properties:
            kind:
              type: string
              enum: [upload, library]
            item_id:
              type: string
              description: Media library item id, present when kind is library.
            agent_id:
              type: string
            campaign_id:
              type: string
    Broadcast:
      type: object
      description: |
        A broadcast document. Timestamps are returned as epoch milliseconds and
        document references (for example the entries of contacts) as path
        strings. Fields not listed here are returned as stored.
      additionalProperties: true
      properties:
        id:
          type: string
          description: Unique identifier of the broadcast.
          examples: ["bRoAdCaSt123456"]
        user_id:
          type: string
          description: UID of the account that owns the broadcast.
        name:
          type: string
          description: Broadcast name.
          examples: ["Spring menu announcement"]
        status:
          type: string
          enum: ['Draft', 'Pending Approval', 'Scheduled', 'Sending', 'Paused', 'Sent', 'Failed']
          description: |
            Current lifecycle state. Set by the launch, pause, and resume
            endpoints and by the send engine; it can never be written directly
            through create or update.
        channel:
          type: string
          description: The single channel this broadcast sends on. Duplicating onto another channel is how a multi-channel send is expressed.
          examples: ["whatsapp"]
        agent_id:
          type: ['string', 'null']
          description: AI agent that handles replies to this broadcast. Null means replies land in the team inbox with no agent.
        list_id:
          type: ['string', 'null']
          description: Contact list used as the audience.
        list_name:
          type: ['string', 'null']
          description: Cached name of the audience list.
        contacts:
          type: array
          description: Explicit contact audience, as contact document paths. An alternative to list_id.
          items:
            type: string
            examples: ["users/abc123/contacts/xyz789"]
        total_contacts:
          type: integer
          description: Size of the audience as last computed.
        send_to_new_list_members:
          type: boolean
          description: |
            When true the broadcast keeps sending to contacts added to the list
            later. Trigger-based broadcasts like this stay in Sending while they
            are armed.
        whats_app_template:
          $ref: '#/components/schemas/BroadcastOpenerTemplate'
        opener_media:
          oneOf:
            - $ref: '#/components/schemas/BroadcastOpenerMedia'
            - type: 'null'
          description: The one optional image or video attached to the opening message.
        execution_date:
          type: ['integer', 'null']
          description: When the send should start, as epoch milliseconds. A future value makes launch schedule the broadcast instead of sending immediately.
        drip_mode:
          type: boolean
          description: Whether the send is paced over time instead of going out at once. Defaults to false.
        forced_drip:
          type: boolean
          description: Set by the system when a large audience was automatically paced to protect the sending number.
        time_critical:
          type: boolean
          description: |
            Opts out of the automatic pacing applied to large sends, for a warm
            audience that needs the message now. It never lifts the sender's
            real daily provider ceiling and never overrides an explicit
            drip_mode.
        batch_size:
          type: integer
          description: Contacts per send batch.
        source_campaign_id:
          type: ['string', 'null']
          description: |
            Set when this broadcast is a synced mirror of a classic campaign.
            Mirrored broadcasts are launched and deleted through that campaign.
            Read-only: it cannot be set or cleared through this API.
        processed_contact_count:
          type: integer
          description: Contacts processed so far.
        total_contacts_sent:
          type: integer
          description: Number of opening messages actually sent.
        unique_contacts_replied:
          type: array
          description: Ids of the contacts that replied at least once.
          items:
            type: string
        overall_reply_rate:
          type: number
          description: Reply rate measured across the send so far, as a fraction between 0 and 1.
        credits_used:
          type: ['number', 'null']
          description: Credits spent on this broadcast's sends.
        paused_reason:
          type: string
          description: |
            Why the broadcast is paused. manual is an owner pause; the engine
            also writes low_engagement, limit_exceeded, and
            error_pattern_detected.
          examples: ["low_engagement"]
        paused_at:
          type: ['integer', 'null']
          description: When the broadcast was paused, as epoch milliseconds.
        paused_details:
          type: ['object', 'null']
          description: Human-readable explanation of the pause, as stored.
          additionalProperties: true
        safety_status:
          type: string
          enum: [active, paused, killed, completed]
          description: Verdict of the send safety checks.
        killed_reason:
          type: string
          description: Explanation written when the safety checks stopped the send.
        engagement_override:
          type: boolean
          description: True once the owner has waived the low-engagement pause for this broadcast.
        engagement_override_at:
          type: ['integer', 'null']
          description: When the waiver was recorded, as epoch milliseconds.
        campaign_health:
          type: object
          description: Live send-health counters written by the send engine. The field keeps its historical name.
          additionalProperties: true
          properties:
            total_sent:
              type: integer
            total_replies:
              type: integer
            reply_rate:
              type: number
            current_batch_size:
              type: integer
            last_adjustment_reason:
              type: string
            last_health_check:
              type: integer
              description: Epoch milliseconds.
        completion_summary:
          type: object
          description: Written once the send finishes.
          additionalProperties: true
          properties:
            total_contacts:
              type: integer
            total_sent:
              type: integer
            permanently_failed:
              type: integer
            unique_replied:
              type: integer
            failure_rate:
              type: number
            completed_at:
              type: integer
              description: Epoch milliseconds.
            had_errors:
              type: boolean
        follow_up_config:
          type: object
          description: Follow-up and re-engagement settings for contacts that never replied. Stored as provided.
          additionalProperties: true
        created_at:
          type: integer
          description: Creation time as epoch milliseconds.
        last_modified_at:
          type: integer
          description: Last modification time as epoch milliseconds.
    BroadcastCountryCost:
      type: object
      description: Per-destination-country slice of a WhatsApp broadcast cost estimate.
      properties:
        countryCode:
          type: string
          description: Country calling code of the destination numbers.
          examples: ["31"]
        name:
          type: string
          description: Country name.
        iso:
          type: string
          description: ISO country code.
        flag:
          type: string
          description: Flag emoji for the country.
        contactCount:
          type: integer
          description: Contacts in the audience with a number in this country.
        costPerContact:
          type: ['number', 'null']
          description: |
            Credits charged per contact for this country, already including any
            agency markup. Null when the account's WhatsApp messages are billed
            by Meta to its own WhatsApp Business Account instead of in credits.
        subtotal:
          type: ['number', 'null']
          description: Credits for this country. Null on the same lanes as costPerContact.
    BroadcastWhatsAppCostEstimate:
      type: object
      description: Cost estimate returned for a WhatsApp Business broadcast, broken down by destination country.
      properties:
        countries:
          type: array
          items:
            $ref: '#/components/schemas/BroadcastCountryCost'
        totalContacts:
          type: integer
          description: Number of audience contacts with a usable phone number.
        totalTemplateCost:
          type: ['number', 'null']
          description: |
            Total credits for the send. Null, never zero, when the account's
            WhatsApp messages are billed by Meta to its own WhatsApp Business
            Account, so that no client renders a price we do not charge.
        templateCategory:
          type: string
          description: Template category the estimate was priced at.
          examples: ["marketing"]
        billing_mode:
          type: string
          description: Who pays for this send.
          examples: ["credits"]
        lane:
          type: string
          description: Which WhatsApp connection lane the account sends on.
        service_messages_billable_soon:
          type: boolean
          description: Whether service conversations are about to become billable on this lane.
    BroadcastSmsCostEstimate:
      type: object
      description: Cost estimate returned for a text-message broadcast, priced from live Twilio rates using the account's own Twilio credentials.
      properties:
        totalContacts:
          type: integer
          description: Contacts in the audience list.
        messageLength:
          type: integer
          description: Length of the opening message after it is normalised for text-message encoding.
        segmentsPerMessage:
          type: integer
          description: Number of text-message segments each message takes.
        totalSegments:
          type: integer
          description: Contacts multiplied by segments per message.
        estimatedCostUsd:
          type: number
          description: |
            Estimated spend in US dollars. Returned as 0 when the account has no
            Twilio credentials connected, in which case only the segment counts
            are meaningful.
        priceUnit:
          type: string
          examples: ["USD"]
        billedByTwilio:
          type: boolean
          description: Always true, text-message sends are billed by Twilio to the account's own Twilio balance.
        billing_mode:
          type: string
          examples: ["twilio_direct"]
        lane:
          type: string
        service_messages_billable_soon:
          type: boolean
    DealFields:
      type: object
      description: |
        Writable fields of a deal. Fields other than the ones listed here are saved on the deal as
        sent, so you can keep your own extra attributes on it. Ownership and creation time are always
        set by the server and cannot be supplied.
      properties:
        title:
          type: string
          description: Short name of the deal, shown on the pipeline card.
        stage:
          type: string
          description: ID of the pipeline stage (column) the deal belongs to. Must be one of the stages configured on your account.
        value:
          type: number
          description: Monetary value of the deal. Must be a finite number.
        description:
          type: string
          description: Longer free-text description of the opportunity.
        company:
          type: string
          description: Company the deal belongs to.
        probability:
          type: number
          description: Your estimated chance of winning the deal, as a percentage.
        priority:
          type: string
          description: Priority label used on your board.
        status:
          type: string
          description: Status label used on your board, for example open, won, or lost.
        source:
          type: string
          description: Where the opportunity came from.
        close_date:
          type: string
          description: Expected close date as an ISO 8601 string, for example 2026-07-01T09:00:00Z.
        assigned_to:
          type: string
          description: ID of the team member responsible for the deal.
        contact_id:
          type: ['string', 'null']
          description: ID of the contact to link the deal to, or null to unlink it.
    AvailableSlotWindow:
      type: object
      description: One offered start-time window within a day's availability block.
      properties:
        start_time:
          type: string
          description: Start of the window, HH:mm, in the event's effective timezone.
          examples: ["09:00"]
        end_time:
          type: string
          description: End of the window, HH:mm, in the event's effective timezone.
          examples: ["09:30"]
        available:
          type: boolean
          description: Whether this window can currently be booked.
        slots_every_x_minutes:
          type: integer
          description: How often a new start is offered inside this window, in minutes. Only present when it differs from the event's slot duration.
        spots_left:
          type: integer
          description: |
            Remaining capacity in this window. Only present for events whose per-slot
            capacity is 2 or more; absent on capacity-1 events and on events with
            unlimited capacity.
    AvailableSlotDay:
      type: object
      description: One calendar day's worth of available booking windows for one room.
      properties:
        date:
          type: string
          description: The calendar day the windows fall on, dd/MM/yyyy, in the event's effective timezone.
          examples: ["15/06/2026"]
        day:
          type: string
          description: Weekday name of the date field.
          examples: ["Monday"]
        room_name:
          type: string
          description: The room or staff member these windows belong to.
        available_slots:
          type: array
          items:
            $ref: '#/components/schemas/AvailableSlotWindow'
    GoogleCalendarListEntry:
      type: object
      description: |
        One calendar from the caller's connected Google account, returned exactly as
        Google's calendar list API sends it. Fields not listed here are returned as
        Google sends them.
      additionalProperties: true
      properties:
        id:
          type: string
          description: Google Calendar id.
          examples: ["primary"]
        summary:
          type: string
          description: Calendar display name.
          examples: ["Front Desk Bookings"]
        description:
          type: ['string', 'null']
        timeZone:
          type: string
          examples: ["Europe/Amsterdam"]
        primary:
          type: boolean
          description: True for the connected account's own primary calendar.
        accessRole:
          type: string
          description: The caller's access level on this calendar.
          examples: ["owner"]
        backgroundColor:
          type: string
    CalendarImportJob:
      type: object
      description: Status of an asynchronous calendar-events import job.
      properties:
        success:
          type: boolean
          examples: [true]
        job_id:
          type: string
        status:
          type: string
          enum: [queued, processing, completed, failed]
          description: queued right after enqueueing, processing while the worker runs, then completed or failed.
        message:
          type: ['string', 'null']
          description: Human-readable outcome summary, set once the job completes. Null while queued or processing.
        error:
          type: ['string', 'null']
          description: Failure reason. Set only when status is failed.
    ZenchefRestaurantDetails:
      type: object
      description: Zenchef restaurant details returned once its name has been verified against the account's claim.
      properties:
        id:
          type: string
          description: The Zenchef restaurant id.
        name:
          type: string
        address:
          type: string
        status:
          type: string
          examples: ["active"]
    FormitableVerifiedRestaurantDetails:
      type: object
      description: Result of probing a Formitable restaurant id against its public products API.
      properties:
        restaurantId:
          type: string
        productCount:
          type: integer
          description: Number of active bookable products found. 0 means the restaurant id resolves but has no products configured yet.
        sampleProductTitle:
          type: string
          description: Title of one of the restaurant's products, absent when productCount is 0.
        language:
          type: string
          description: Language tag the probe was made in.
          examples: ["nl"]
    FormitableRestaurantDetails:
      type: object
      description: Full Formitable restaurant profile, including the website URL cached during restaurant setup.
      properties:
        uid:
          type: string
        name:
          type: string
        website:
          type: string
        email:
          type: string
        telephone:
          type: string
        streetAddress:
          type: string
        zipcode:
          type: string
        city:
          type: string
        country:
          type: string
        countryCode:
          type: string
        currency:
          type: string
        logo:
          type: string
        avatar:
          type: string
        coverImage:
          type: string
    CampaignTag:
      type: object
      description: |
        A tag the AI (or a human) can apply to contacts on this campaign. Stored
        on the campaign document's tags array and mirrored onto the campaign's
        agent so the model can offer it during a conversation.
      properties:
        tag:
          type: string
          description: Path to the backing tag document (users/{uid}/tags/{id}), present once the tag has been resolved to a stored tag definition.
        name:
          type: string
          description: Tag label.
        description:
          type: string
          description: What the tag means, shown to the AI so it knows when to apply it.
        webhook:
          type: string
          description: Optional webhook URL fired when the tag is applied.
        ai_can_remove:
          type: boolean
          description: Whether the AI is allowed to remove this tag from a contact once applied.
    ContactAssignResponse:
      type: object
      properties:
        success:
          type: boolean
          examples: [true]
        assigned_to:
          type: ['string', 'null']
          description: UID of the team member now responsible for the contact, or null when cleared.
    LinkedContactSummary:
      type: object
      description: A sibling contact that shares the same person as the contact it was requested for.
      properties:
        contact_id:
          type: string
          description: ID of the linked contact.
        channel:
          type: ['string', 'null']
          description: Messaging channel this sibling is on.
          examples: ["whatsapp"]
        custom_channel:
          type: ['string', 'null']
          description: Real network behind channel "custom" (for example "linkedin"), when applicable.
        first_name:
          type: ['string', 'null']
        last_name:
          type: ['string', 'null']
        phone_number:
          type: ['string', 'null']
        last_message:
          type: ['string', 'null']
          description: Body of the most recent message on this sibling's conversation.
        last_message_timestamp:
          type: ['string', 'null']
          format: date-time
        linked_from:
          type: ['object', 'null']
          description: Provenance of the link, present when this sibling was created by a link-channel call.
          properties:
            contact_id:
              type: ['string', 'null']
              description: The contact this sibling was linked from.
            channel:
              type: ['string', 'null']
            linked_at:
              type: ['string', 'null']
              format: date-time
            reason:
              type: ['string', 'null']
              examples: ["continue_on_channel"]
    ContactProfilePicResponse:
      type: object
      properties:
        success:
          type: boolean
          examples: [true]
        avatar_url:
          type: string
          description: Signed/cached URL of the contact's profile picture. Present when a picture was found.
        cached:
          type: boolean
          description: Whether the URL was served from cache rather than fetched from the provider just now.
        message:
          type: string
          description: Present instead of avatar_url when no picture could be retrieved (for example the provider reports none set).
    AutoTagRunSkipCounts:
      type: object
      description: Per-reason counts of contacts the run did not evaluate (no LLM call, no charge).
      properties:
        no_agent:
          type: integer
          examples: [0]
        no_tags:
          type: integer
          examples: [0]
        no_messages:
          type: integer
          examples: [0]
        insufficient_credits:
          type: integer
          examples: [0]
        error:
          type: integer
          examples: [0]
    AutoTagContactResult:
      type: object
      description: Outcome of evaluating a single contact against its agent's tag rules.
      properties:
        contactId:
          type: string
        skipped:
          type: ['string', 'null']
          description: Set when the contact was skipped before the evaluator ran; absent otherwise.
          enum: [no_agent, no_tags, no_messages, insufficient_credits, null]
        applied:
          type: array
          items:
            type: string
          description: Tag names actually written onto the contact.
        removed:
          type: array
          items:
            type: string
          description: Tag names removed from the contact.
        blockedRemovals:
          type: array
          items:
            type: string
          description: Removals the evaluator asked for that the tag's ai_can_remove setting refused.
        creditsCharged:
          type: number
          description: Credits charged for this contact (applied tags x the resolved rate; 0 on BYOK).
        costUsd:
          type: number
          description: Provider cost of the evaluation call itself. Never charged in credits.
        error:
          type: string
          description: Present when the evaluation or the tag write failed for this contact.
    AutoTagRunStatus:
      type: object
      description: The account's current (or most recent) on-demand auto-tag run, polled by the UI. Never includes the underlying contact id list.
      properties:
        run_id:
          type: string
        status:
          type: string
          enum: [running, done, failed, stopped_insufficient_credits]
        scope:
          type: string
          enum: [contacts, agent]
        agent_id:
          type: ['string', 'null']
        cursor:
          type: integer
          description: How many contacts of the run have been processed so far.
        total:
          type: integer
        processed:
          type: integer
        tagged_contacts:
          type: integer
          description: Number of contacts that had at least one tag applied or removed.
        tags_applied:
          type: integer
        tags_removed:
          type: integer
        skipped:
          $ref: '#/components/schemas/AutoTagRunSkipCounts'
        credits_charged:
          type: number
        started_at:
          type: ['string', 'null']
          format: date-time
        updated_at:
          type: ['string', 'null']
          format: date-time
        finished_at:
          type: ['string', 'null']
          format: date-time
        error:
          type: string
          description: Present when the run itself failed to dispatch.
    FaqSimilarMatch:
      type: object
      description: One FAQ ranked against a faq_update task's question, most relevant first.
      properties:
        faq_id:
          type: string
        question:
          type: string
        answer:
          type: string
        category:
          type: ['string', 'null']
        created_at:
          type: ['string', 'null']
          format: date-time
        bm25_score:
          type: number
          description: Raw BM25 relevance score. Mostly useful for debugging - the UI ranks/thresholds on similarity.
        similarity:
          type: number
          description: >-
            The score the UI should rank/threshold on, bounded 0-1. Equals embedding_similarity when
            semantic embeddings ran for this request, otherwise falls back to keyword_similarity.
        embedding_similarity:
          type: number
          description: Semantic cosine similarity of the task question against this FAQ, bounded 0-1. 0 when the embedding path did not run.
        keyword_similarity:
          type: number
          description: Fraction of the task question's distinct tokens that also appear in this FAQ, bounded 0-1. Always present.
        distance:
          type: number
          description: 1 - similarity. Kept for backwards compatibility with the earlier vector-search response shape.
    FaqOptimizationChange:
      type: object
      description: One user-approved change from a FAQ optimization review, to apply to the target campaign or agent.
      required:
        - action
      properties:
        action:
          type: string
          enum: [keep, remove, add_from_library, create_new, modify]
          description: >-
            Only actions in this list touch faq_refs on the target - an entry the caller does not
            send generates no write and the existing FAQ survives untouched.
        faq_id:
          type: string
          description: Existing FAQ id. Used by keep, remove, add_from_library, and modify.
        faq_ref_path:
          type: string
          description: Alternative to faq_id - the full FAQ document reference path.
        question:
          type: string
          description: Used by create_new and modify.
        answer:
          type: string
          description: Used by create_new and modify.
        edit_scope:
          type: string
          enum: [edit_all, edit_this_only]
          description: For modify - whether the edit applies everywhere the FAQ is used, or only to this campaign/agent (forks a duplicate).
    FaqUploadedFile:
      type: object
      description: A file already uploaded to Storage, to extract FAQs from.
      required:
        - storagePath
        - fileName
        - mimeType
      properties:
        storagePath:
          type: string
          description: Firebase Storage path of the uploaded file.
        fileName:
          type: string
        mimeType:
          type: string
    McpToolPolicy:
      type: object
      description: |
        Execution guard-rails for one tool: how many times it may fire, whether repeat calls with the
        same arguments are served from cache instead of re-firing, and whether it is declared safe to
        retry. Send null (instead of an object) as a tool_policies value to clear that tool's policy.
      properties:
        read_only:
          type: boolean
          description: Declares the tool a pure read / safe to retry. Feeds the AI's replay guard.
        cache_repeat_calls:
          type: boolean
          description: When true, a repeat call with the same (normalized) arguments in the same chat is served from the stored result instead of re-firing.
        max_calls_per_chat:
          type: integer
          minimum: 1
          maximum: 1000
          description: Maximum successful fires in a single chat session. Omitted or 0 means unlimited.
        max_calls_per_window:
          type: object
          description: A rolling rate cap across the whole account-contact relationship.
          required:
            - count
            - minutes
          properties:
            count:
              type: integer
              minimum: 1
              maximum: 1000
              description: Maximum successful fires allowed inside the window.
            minutes:
              type: integer
              minimum: 1
              maximum: 10080
              description: Length of the rolling window, in minutes (capped at 10080, i.e. 7 days).
    McpServerFields:
      type: object
      description: |
        Writable fields of an MCP server registration. name and url are required on create; on
        update, send only the fields you want to change - at least one is required, and any field you
        omit keeps its saved value. A change to url, auth_header_name or auth_header_value re-tests
        the connection and refreshes the cached tool list.
      properties:
        name:
          type: string
          maxLength: 60
          description: Display name for the server.
        url:
          type: string
          maxLength: 2000
          description: The MCP server's endpoint URL. Checked against an SSRF guard (private/loopback/link-local addresses are rejected) both when you save it and every time it is called.
        auth_type:
          type: string
          enum: [header, oauth2]
          description: Auth method. Omitted or "header" uses a static header credential (auth_header_name/auth_header_value). "oauth2" registers an OAuth 2.1 client - authorization itself happens separately, in the dashboard, after creation.
        auth_header_name:
          type: string
          description: Name of the header sent with every request when auth_type is "header". Defaults to "Authorization".
        auth_header_value:
          type: string
          maxLength: 4000
          description: |
            Value of the auth header, sent to the remote server verbatim. Never returned by any
            endpoint once saved - auth_header_value_set on the read view only tells you a value is on
            file. A "Basic <credential>" value is checked for well-formed base64 at save time so a
            stray quote from copy-pasting fails here with a clear message instead of as an opaque
            remote 401.
        enabled:
          type: boolean
          description: Whether the server is active. Defaults to true on create.
        enabled_tools:
          type: ['array', 'null']
          items:
            type: string
            maxLength: 200
          maxItems: 500
          description: |
            Allow-list of tool names (by their original, un-prefixed name) an attached agent may use
            from this server. Send null to clear the allow-list back to "all discovered tools on"
            (the default). A name that is not currently in the server's cached tool list is stored but
            inert until that tool is discovered.
        tool_policies:
          type: ['object', 'null']
          additionalProperties:
            oneOf:
              - $ref: '#/components/schemas/McpToolPolicy'
              - type: 'null'
          description: |
            Per-tool execution policies keyed by the tool's original (un-prefixed) name. Send null to
            clear every policy; send an object to REPLACE the whole stored map (a null value inside it
            clears just that one tool's entry). Keys are not checked against the cached tool list, so a
            policy can be saved before its tool is discovered.
    McpServerToolSummary:
      type: object
      description: A tool discovered on the server's most recent successful connection.
      properties:
        name:
          type: string
          description: The tool's name as reported by the server.
        description:
          type: string
          maxLength: 300
          description: The tool's description as reported by the server, truncated to 300 characters.
        read_only_hint:
          type: boolean
          description: Present (and true) only when the server itself declared this tool read-only in its tool annotations.
    McpServer:
      type: object
      description: |
        The scrubbed, client-safe view of a registered MCP server. Every secret (auth_header_value and
        the OAuth 2.1 access/refresh/client-secret/PKCE-verifier fields) is stripped before this is
        returned - auth_header_value_set / oauth_connected are the only signals of whether a credential
        is on file.
      properties:
        server_id:
          type: string
          description: The server's document id. Also returned as id for convenience.
        id:
          type: string
        name:
          type: string
        url:
          type: string
        enabled:
          type: boolean
        auth_type:
          type: string
          enum: [header, oauth2]
        auth_header_name:
          type: string
        auth_header_value_set:
          type: boolean
          description: True when a header credential is saved. The value itself is never returned.
        oauth_connected:
          type: boolean
          description: True once an OAuth 2.1 server has completed authorization.
        oauth_authorization_endpoint:
          type: ['string', 'null']
        oauth_token_endpoint:
          type: ['string', 'null']
        oauth_registration_endpoint:
          type: ['string', 'null']
        oauth_client_id:
          type: ['string', 'null']
        oauth_scope:
          type: ['string', 'null']
        oauth_resource:
          type: ['string', 'null']
        oauth_token_expires_at:
          type: ['string', 'null']
          format: date-time
        tools:
          type: array
          items:
            $ref: '#/components/schemas/McpServerToolSummary'
          description: Tools discovered on the last successful connection.
        enabled_tools:
          type: ['array', 'null']
          items:
            type: string
          description: The saved allow-list, or null when every discovered tool is enabled.
        tool_policies:
          type: ['object', 'null']
          additionalProperties:
            $ref: '#/components/schemas/McpToolPolicy'
        tools_cached_at:
          type: ['string', 'null']
          format: date-time
        last_connected_at:
          type: ['string', 'null']
          format: date-time
        last_error:
          type: ['string', 'null']
          description: The most recent connection error, or null when the last attempt succeeded (or none has run yet).
        created_at:
          type: ['string', 'null']
          format: date-time
        updated_at:
          type: ['string', 'null']
          format: date-time
    MetaTemplateButton:
      type: object
      description: One button on a template being created.
      required:
        - type
        - text
      properties:
        type:
          type: string
          enum: ["QUICK_REPLY", "URL", "PHONE_NUMBER"]
        text:
          type: string
          description: Button label.
        url:
          type: string
          description: Destination URL. Required when type is URL.
        phone_number:
          type: string
          description: E.164 phone number. Required when type is PHONE_NUMBER.
          examples: ["+31612345678"]
    MetaTemplateCreateInput:
      type: object
      description: |
        Creates and submits a WhatsApp template for Meta review on the
        account's own WABA. Either body or a fully-formed components
        array is required; components, when supplied, wins over
        body/header/footer/buttons.
      required:
        - name
        - language
      properties:
        name:
          type: string
          description: Template name. Meta requires lowercase letters, digits, and underscores only.
          examples: ["order_confirmation"]
        language:
          type: string
          description: Meta language code.
          examples: ["en_US"]
        body:
          type: string
          description: |
            Body text. Use Meta's native {{1}}, {{2}}, … placeholder
            syntax. Required unless components is supplied.
          examples: ["Hi {{1}}, your order {{2}} has shipped!"]
        variables:
          type: array
          description: Example values for the body placeholders, index-aligned, used for Meta's review sample.
          items:
            type: string
          examples: [["Jane", "#1234"]]
        category:
          type: string
          enum: ["MARKETING", "UTILITY", "AUTHENTICATION"]
          description: Defaults to UTILITY when omitted.
        header:
          type: string
          description: Optional plain-text header.
        footer:
          type: string
          description: Optional footer text.
        buttons:
          type: array
          items:
            $ref: '#/components/schemas/MetaTemplateButton'
        components:
          type: array
          description: |
            Escape hatch: a fully-formed Meta components array, for template
            shapes the simplified fields above cannot express (e.g. media
            headers). When present it replaces body/header/footer/buttons
            entirely.
          items: {}
      example:
        name: order_confirmation
        language: en_US
        body: "Hi {{1}}, your order {{2}} has shipped!"
        variables: ["Jane", "#1234"]
        category: UTILITY
    MetaTemplateCreated:
      type: object
      description: Meta's acknowledgement of the newly-submitted template.
      properties:
        id:
          type: string
          description: Meta's numeric template id, stringified.
          examples: ["123456789012345"]
        name:
          type: string
          examples: ["order_confirmation"]
        language:
          type: string
          examples: ["en_US"]
        status:
          type: ['string', 'null']
          description: Meta's initial review status, usually PENDING.
          examples: ["PENDING"]
        category:
          type: ['string', 'null']
          examples: ["UTILITY"]
      example:
        id: "123456789012345"
        name: order_confirmation
        language: en_US
        status: PENDING
        category: UTILITY
    MetaTemplateSummary:
      type: object
      description: One template on the account's WABA, in the same shape the Twilio-lane template UI already renders.
      properties:
        id:
          type: string
          description: Meta's numeric template id — the stable handle for delete/status lookups.
          examples: ["123456789012345"]
        name:
          type: ['string', 'null']
          examples: ["order_confirmation"]
        body:
          type: ['string', 'null']
        language:
          type: ['string', 'null']
          examples: ["en_US"]
        variables:
          type: array
          items:
            type: string
        status:
          type: ['string', 'null']
          description: Lowercased status (approved / pending / rejected, …), aligned with the Twilio lane's vocabulary. Reflects the freshest of the live Graph read and any webhook-observed status change.
          examples: ["approved"]
        sid:
          type: ['string', 'null']
          description: Meta's template id again, exposed under the Twilio lane's field name so existing "is this template sendable" checks work unchanged.
          examples: ["123456789012345"]
        type:
          type: string
        category:
          type: ['string', 'null']
          examples: ["UTILITY"]
        rejection_reason:
          type: ['string', 'null']
        meta_status:
          type: ['string', 'null']
          description: Raw Meta status (APPROVED, PENDING, PAUSED, …) — additive.
          examples: ["APPROVED"]
        components:
          type: array
          description: Raw Meta components array — additive, lets a client render headers/buttons.
          items: {}
        campaign_id:
          type: ['string', 'null']
          description: Always null on this lane. Present for shape parity with the Twilio lane.
        date_created:
          type: ['string', 'null']
        date_updated:
          type: ['string', 'null']
        submitted_at:
          type: ['string', 'null']
        approved_at:
          type: ['string', 'null']
      example:
        id: "123456789012345"
        name: order_confirmation
        body: "Hi {{1}}, your order {{2}} has shipped!"
        language: en_US
        variables: ["1", "2"]
        status: approved
        sid: "123456789012345"
        type: template
        category: UTILITY
        rejection_reason: null
        meta_status: APPROVED
        components: []
        campaign_id: null
        date_created: null
        date_updated: null
        submitted_at: null
        approved_at: null
    WebhookTestResultResponse:
      type: object
      description: Outcome of a test delivery to a webhook URL not yet (or already) saved.
      properties:
        success:
          type: boolean
        message:
          type: string
    ByoPhoneNumberRequest:
      type: object
      required:
        - phone_number
        - country_code
      properties:
        phone_number:
          type: string
          description: The number you already control, in E.164 format.
          examples: ["+14155551234"]
        country_code:
          type: string
          maxLength: 8
          description: ISO 3166-1 alpha-2 (or similar) country code.
          examples: ["US"]
        display_name:
          type: string
          description: Friendly label. Defaults to the phone number.
        category:
          type: string
          description: Free-form category label.
    ByoPhoneNumberResponse:
      type: object
      properties:
        success:
          type: boolean
          examples: [true]
        phone_number:
          type: string
        channel:
          type: string
          examples: ["whatsapp"]
        type:
          type: string
          examples: ["BYO"]
        whatsapp_status:
          type: string
          examples: ["ADDED"]
        outgoing_status:
          type: string
          examples: ["ADDED"]
        is_active:
          type: boolean
          examples: [false]
    PhoneNumberRecordDeleteResponse:
      type: object
      properties:
        success:
          type: boolean
          examples: [true]
        phone_number:
          type: string
        deleted:
          type: boolean
          examples: [true]
    ChatExportEmailResult:
      type: object
      description: Where the transcript email actually went, and how much of the conversation it contains.
      properties:
        via:
          type: string
          enum: [transactional, email_channel]
          description: The pipe actually used - transactional (a standalone email) or email_channel (a reply inside the existing email-channel thread).
        recipientEmail:
          type: string
        messageCount:
          type: integer
          description: Number of messages the transcript contains.
        omittedCount:
          type: integer
          description: Number of older messages left out to fit the length cap.
    ChatExportJsonMessage:
      type: object
      properties:
        body:
          type: ['string', 'null']
        direction:
          type: string
        timestamp:
          type: ['string', 'null']
          format: date-time
        type:
          type: ['string', 'null']
        media_url:
          type: ['string', 'null']
        media_content_type:
          type: ['string', 'null']
        name:
          type: ['string', 'null']
          description: Tool name, present only on tool_use messages.
        args:
          type: ['object', 'null']
          description: Tool call arguments, present only on tool_use messages.
    ChatExportJsonData:
      type: object
      description: Structured chat export for one contact, returned when format=json.
      properties:
        contact:
          type: object
          properties:
            id:
              type: string
            name:
              type: string
            phone:
              type: ['string', 'null']
            email:
              type: ['string', 'null']
        messages:
          type: array
          items:
            $ref: '#/components/schemas/ChatExportJsonMessage'
    MediaDownloadUrlResponse:
      type: object
      properties:
        url:
          type: string
          description: V4 signed URL, valid for 1 hour.
        expiresInMs:
          type: integer
          examples: [3600000]
    ChatMediaUploadResult:
      type: object
      properties:
        mediaUrl:
          type: string
          description: V4 signed URL, valid for 7 days.
        storagePath:
          type: string
          description: Bucket-relative storage path. Persist this on the outbound message so the URL can be re-minted after the 7-day TTL via POST /chat-exports/media/download-url.
        mediaContentType:
          type: string
    DailySummaryStats:
      type: object
      properties:
        total_conversations:
          type: integer
        total_messages_sent:
          type: integer
        total_messages_received:
          type: integer
        human_alerts:
          type: integer
        bookings:
          type: integer
        new_contacts:
          type: integer
        sales:
          type: integer
    DailySummarySectionConfig:
      type: object
      properties:
        id:
          type: string
        name:
          type: string
        enabled:
          type: boolean
        custom_prompt:
          type: string
        icon:
          type: string
        position:
          type: integer
    DailySummary:
      type: ['object', 'null']
      description: The account's AI-generated daily summary for one date, or null if it has not been generated yet.
      properties:
        date:
          type: string
          examples: ["2026-06-08"]
        status:
          type: string
          enum: [pending, generating, completed, failed]
        generated_at:
          type: ['string', 'null']
          format: date-time
        stats:
          $ref: '#/components/schemas/DailySummaryStats'
        sections:
          type: object
          description: Markdown content keyed by section id.
          additionalProperties:
            type: string
        contact_map:
          type: object
          description: Display name to contact document ID, for clickable links.
          additionalProperties:
            type: string
        auto_tasks:
          type: array
          items:
            type: object
            properties:
              title:
                type: string
              description:
                type: string
              contact_id:
                type: string
              priority:
                type: string
              type:
                type: string
        created_task_ids:
          type: array
          items:
            type: string
        error:
          type: string
    DailySummaryResponse:
      type: object
      properties:
        success:
          type: boolean
          examples: [true]
        data:
          type: object
          properties:
            summary:
              $ref: '#/components/schemas/DailySummary'
            section_configs:
              type: array
              items:
                $ref: '#/components/schemas/DailySummarySectionConfig'
    Notification:
      type: object
      description: |
        One row in the account's in-app notification feed (read over the real-time relay, not
        returned directly by any endpoint below - documented here for reference).
      properties:
        id:
          type: string
          description: The notification's document id.
        type:
          type: string
          description: Free-text category, for example "campaign_status" or "human_alert".
        title:
          type: string
          description: Short headline shown in the notification center.
        message:
          type: string
          description: Plain-text body.
        severity:
          type: string
          enum: [info, success, warning, error]
        action_url:
          type: ['string', 'null']
          description: App-relative deep link the notification opens when tapped.
        metadata:
          type: ['object', 'null']
          description: Structured context, for example { contact_id, campaign_id }.
        phone_number:
          type: ['string', 'null']
          description: Associated phone number, for channel-connection notifications.
        read:
          type: boolean
        read_at:
          type: ['string', 'null']
          format: date-time
        created_at:
          type: ['string', 'null']
          format: date-time
    ShortLinkFields:
      type: object
      description: Writable fields of a WhatsApp short link.
      properties:
        phoneNumbers:
          type: array
          minItems: 1
          items:
            type: string
          description: E.164 phone numbers eligible for routing. Each one must already exist in your account's connected numbers and be WhatsApp-reachable (WhatsApp Web, or a Twilio number with an ONLINE WhatsApp Business API sender) - an unowned or non-WhatsApp-reachable number is rejected.
        prefillMessage:
          type: string
          description: Text pre-filled into the wa.me conversation the visitor lands in.
        stickyRouting:
          type: boolean
          description: When true, a visitor's cookie pins them to the same number across sessions instead of re-routing on every click. Defaults to true.
        disabled:
          type: boolean
          description: When true, the link returns 404 from the public redirect instead of routing. Defaults to false.
    ShortLinkResult:
      type: object
      description: The saved short link's identifiers and public URL.
      properties:
        url:
          type: string
          description: The full public short-link URL visitors click.
        slug:
          type: string
          description: The link's URL slug. Always server-generated on create - never chosen by the caller.
        appNameSlug:
          type: string
          description: Tenant/branding identifier baked into the public URL's host. "dmchamp" for accounts with no white-label branding.
    SubAccountAgencyPricingData:
      type: object
      description: The caller's parent agency's credit pricing configuration, as seen by one of its sub-accounts.
      properties:
        tiers:
          type: array
          description: The agency's SaaS-mode plans, in price-list order. Empty when the agency has not configured any.
          items:
            $ref: '#/components/schemas/AgencyPricingTierFields'
        price_per_credit_cents:
          type: integer
          description: Price of one credit on a custom top-up, in the smallest unit of the currency. Absent when the agency has not set a per-credit price.
          examples: [25]
        price_per_credit_currency:
          type: string
          description: Currency the per-credit price is charged in (ISO 4217, lowercase). Absent when no price is set.
          examples: ["usd"]
        price_per_credit_note:
          type: string
          description: Short line the agency shows under the per-credit price. Absent when none is set.
        agency_display_name:
          type: string
          description: Display name of the parent agency.
    SubAccountCreditUsageSummary:
      type: object
      description: One sub-account's aggregated credit usage over the requested date range.
      properties:
        subAccountId:
          type: string
        subAccountName:
          type: string
        subAccountEmail:
          type: string
        totalCreditsUsed:
          type: number
          description: Sum of consumption rows. Balance-in rows (grants, bonuses, purchases, renewals, refunds) are excluded.
        totalCostUsd:
          type: number
          description: Sum of USD cost, from BYOK-billed rows only.
        totalInputTokens:
          type: integer
        totalOutputTokens:
          type: integer
        totalCacheReadTokens:
          type: integer
        totalCacheCreationTokens:
          type: integer
        byReason:
          type: object
          description: Credits used, keyed by usage reason.
          additionalProperties:
            type: number
        byReasonCost:
          type: object
          description: USD cost, keyed by usage reason.
          additionalProperties:
            type: number
        topCampaigns:
          type: array
          description: Campaigns that consumed the most credits, highest first.
          items:
            type: object
            properties:
              campaignName:
                type: string
              creditsUsed:
                type: number
        topCampaignsCost:
          type: array
          description: Campaigns with the highest USD cost, highest first.
          items:
            type: object
            properties:
              campaignName:
                type: string
              costUsd:
                type: number
    SubAccountCreditUsageRecord:
      type: object
      description: One raw credit usage row. Only returned in detail mode (subAccountId supplied).
      properties:
        amount:
          type: number
          description: Credits charged for this row.
        costUsd:
          type: number
        inputTokens:
          type: integer
        outputTokens:
          type: integer
        cacheReadTokens:
          type: integer
        cacheCreationTokens:
          type: integer
        timestamp:
          type: string
          description: ISO 8601 timestamp.
        reason:
          type: string
        campaignName:
          type: string
        contactName:
          type: string
        creditType:
          type: string
        customKeysUsed:
          type: boolean
        campaignId:
          type: string
        contactId:
          type: string
        description:
          type: string
        subAccountId:
          type: string
        subAccountName:
          type: string
        subAccountEmail:
          type: string
        requestId:
          type: string
          description: BYOK-only — the provider request id.
        aiModel:
          type: string
          description: BYOK-only — the model that served the request.
        aiTier:
          type: string
          enum: [standard, economy, max, mini]
          description: The pricing tier this charge was billed at. Absent on historical rows and on non-AI charges.
        isTest:
          type: boolean
          description: True for a playground/test-run record — renders as free, not as a 0-credit charge.
        isNonUsage:
          type: boolean
          description: True when this row is credits IN (grant, bonus, purchase, renewal, refund) rather than consumption.
        clientCharge:
          type: number
          description: |
            Split billing only: the client-facing charge the sub-account burned, when it differs
            from amount (the agency pool's own cost). margin = clientCharge - amount.
    SubAccountsCreditUsageData:
      type: object
      description: Credit usage across an agency's sub-accounts, or one sub-account's raw records in detail mode.
      properties:
        subAccounts:
          type: array
          items:
            $ref: '#/components/schemas/SubAccountCreditUsageSummary'
        totals:
          type: object
          properties:
            totalCreditsUsed:
              type: number
            totalCostUsd:
              type: number
            totalRecords:
              type: integer
        dateRange:
          type: object
          properties:
            from:
              type: string
            to:
              type: string
        records:
          type: array
          description: Raw usage rows. Only present in detail mode (subAccountId supplied).
          items:
            $ref: '#/components/schemas/SubAccountCreditUsageRecord'
        hasMore:
          type: boolean
        lastTimestamp:
          type: ['string', 'null']
          description: Cursor for the next page in detail mode. Null when there is no more.
        costsRedacted:
          type: boolean
          description: True when cost/token data was withheld because the sub-account spends against the agency's own BYOK key.
    SubAccountCampaignStatusEntry:
      type: object
      description: One sub-account's campaign health summary.
      properties:
        userId:
          type: string
        email:
          type: string
        displayName:
          type: string
        businessName:
          type: ['string', 'null']
          description: The sub-account's company name (settings.business_details.name). Null when never filled in.
        brandDomain:
          type: ['string', 'null']
          description: The white-label config the sub-account's surfaces are branded with. Null when the agency has no white-label config.
        uid:
          type: string
        role:
          type: string
        isAgencySubAccount:
          type: boolean
        createdTime:
          type: string
          description: ISO 8601 timestamp.
        usageLimits:
          type: ['object', 'null']
          properties:
            monthlyCredits:
              type: number
            monthlyCreditsUsed:
              type: number
            lastResetDate:
              type: string
            credits:
              type: number
            rollOverToNextMonth:
              type: boolean
            byokMonthlyLimitUsd:
              type: ['number', 'null']
            byokMonthlyUsedUsd:
              type: number
        isByok:
          type: boolean
        hasOwnByok:
          type: boolean
          description: True when the sub-account has its own BYOK key rather than inheriting the agency's.
        totalCampaigns:
          type: integer
        liveCampaigns:
          type: integer
        pausedCampaigns:
          type: integer
        draftCampaigns:
          type: integer
        completedCampaigns:
          type: integer
        hasIssues:
          type: boolean
        issueDetails:
          type: array
          items:
            type: string
        lastCampaignActivity:
          type: ['string', 'null']
          description: ISO 8601 timestamp of the most recent campaign activity, or null when there is none.
    SubAccountsCampaignStatusData:
      type: object
      description: Campaign health across an agency's sub-accounts, one page at a time.
      properties:
        totalSubAccounts:
          type: integer
        subAccountsWithIssues:
          type: integer
        totalLiveCampaigns:
          type: integer
        totalPausedCampaigns:
          type: integer
        creditPool:
          $ref: '#/components/schemas/SubAccountsCreditPoolRollup'
        subAccounts:
          type: array
          items:
            $ref: '#/components/schemas/SubAccountCampaignStatusEntry'
        hasMore:
          type: boolean
        lastDocumentId:
          type: ['string', 'null']
          description: Cursor for the next page. Use as lastDocumentId on the next call.
        pageSize:
          type: integer
    SubAccountsCreditPoolRollup:
      type: object
      description: |
        How much of the agency's credit pool its sub-accounts' spending limits
        already lay claim to. A spending limit is a cap on the shared pool, not a
        transfer out of it, so this is summed live across EVERY sub-account of the
        agency — the same numbers on every page and regardless of searchQuery.
      properties:
        agencyBalance:
          type: number
          description: The agency's own spendable balance (top-up plus recurring credits).
        allocatedCredits:
          type: number
          description: |
            Sum of every sub-account's current spending limit, minus any credits
            the client bought through the agency's own checkout (those were paid
            for already and never draw on the pool).
        unallocatedCredits:
          type: number
          description: agencyBalance minus allocatedCredits. Negative when the limits together exceed the pool.
        allocatedSubAccounts:
          type: integer
          description: Number of sub-accounts contributing to allocatedCredits.
        purchasedCreditsHeld:
          type: number
          description: Sum of client-purchased credits currently held, excluded from allocatedCredits.
    SubAccountChatContact:
      type: object
      description: One contact belonging to a sub-account, with a last-message preview.
      properties:
        contactId:
          type: string
        firstName:
          type: string
        lastName:
          type: string
        phoneNumber:
          type: string
        email:
          type: string
        channel:
          type: string
        lastActivityAt:
          type: string
          description: ISO 8601 timestamp.
        lastMessage:
          type: ['object', 'null']
          properties:
            body:
              type: string
            direction:
              type: string
            timestamp:
              type: string
        isBotActive:
          type: boolean
        markChatClosed:
          type: boolean
    SubAccountChatsData:
      type: object
      properties:
        contacts:
          type: array
          items:
            $ref: '#/components/schemas/SubAccountChatContact'
        subAccountName:
          type: string
        subAccountEmail:
          type: string
        hasMore:
          type: boolean
        lastActivityAt:
          type: ['string', 'null']
          description: Cursor for the next page. Use as lastActivityAt on the next call.
    SubAccountChatMessage:
      type: object
      properties:
        messageId:
          type: string
        body:
          type: string
        direction:
          type: string
        timestamp:
          type: string
          description: ISO 8601 timestamp.
        status:
          type: string
        channel:
          type: string
        botReply:
          type: boolean
        mediaUrl:
          type: ['string', 'null']
        mediaContentType:
          type: ['string', 'null']
        name:
          type: ['string', 'null']
        role:
          type: ['string', 'null']
    SubAccountChatMessagesData:
      type: object
      properties:
        messages:
          type: array
          items:
            $ref: '#/components/schemas/SubAccountChatMessage'
        contactInfo:
          type: object
          properties:
            firstName:
              type: string
            lastName:
              type: string
            phoneNumber:
              type: string
            channel:
              type: string
        hasMore:
          type: boolean
        oldestTimestamp:
          type: ['string', 'null']
          description: Cursor for the next (older) page. Use as beforeTimestamp on the next call.
    CopyCampaignToSubAccountResult:
      type: object
      description: Outcome of copying a campaign into a sub-account.
      properties:
        newCampaignId:
          type: string
          description: Id of the campaign created in the target sub-account.
        message:
          type: string
        counts:
          type: object
          description: How many of each referenced artifact were copied along with the campaign.
          properties:
            faqs:
              type: integer
            kbSources:
              type: integer
            customFunctions:
              type: integer
            mcpServers:
              type: integer
            mediaFiles:
              type: integer
            mediaLibraryItems:
              type: integer
    AgencyCreditTransaction:
      type: object
      description: One credit-purchase transaction by a sub-account.
      properties:
        id:
          type: string
        sub_account_id:
          type: string
        sub_account_email:
          type: string
        credits_purchased:
          type: integer
        amount_paid_cents:
          type: integer
          description: Amount paid, in the smallest unit of currency.
        currency:
          type: string
          examples: ["usd"]
        stripe_session_id:
          type: string
        stripe_payment_intent_id:
          type: string
        created_at:
          type: string
          description: ISO 8601 timestamp.
        is_new_account:
          type: boolean
          description: True when this transaction created the sub-account.
        is_renewal:
          type: boolean
          description: True when this transaction is a subscription renewal.
    AgencyCreditTransactionsData:
      type: object
      properties:
        transactions:
          type: array
          items:
            $ref: '#/components/schemas/AgencyCreditTransaction'
        count:
          type: integer
          description: Number of transactions in this response.
        summary:
          type: object
          description: Revenue summary for the returned transactions only (not the account's whole history).
          properties:
            total_credits_sold:
              type: integer
            total_revenue_cents:
              type: integer
            primary_currency:
              type: string
              description: Only meaningful when every returned transaction shares one currency.
            new_signups_count:
              type: integer
            renewals_count:
              type: integer
        has_more:
          type: boolean
          description: True when count equals the requested limitCount, meaning more may exist.
    WhiteLabelEmailFooterLink:
      type: object
      description: One custom link shown in the footer of transactional emails.
      required:
        - text
        - url
      properties:
        text:
          type: string
          description: Link label, up to 60 characters.
          examples: ["Support"]
        url:
          type: string
          examples: ["https://example.com/support"]
    WhiteLabelConfigSaveInput:
      type: object
      description: |
        Partial update of a white-label config. Send only the fields you want
        to change; anything left out keeps its current value. Most string
        fields accept an empty string "" to clear the override back to the
        platform default — login_tagline and support_email are the
        exceptions and are simply not written when empty.
      properties:
        config_id:
          type: string
          description: |
            Doc id of the config to edit (its id is its domain). THE targeting
            parameter — an account can hold several configs, so you should
            always send it. configId (camelCase) is also accepted. Optional
            only when product is supplied (resolved to the caller's sole
            config of that product) or when the caller has exactly one config.
          examples: ["app.myagency.com"]
        configId:
          type: string
          description: Alias of config_id.
          examples: ["app.myagency.com"]
        product:
          type: string
          enum: ["dmchamp", "leadfindr"]
          description: |
            Legacy target/scope selector. Ignored for TARGETING when config_id
            is supplied. As a scoping alias it also re-scopes the config's
            modules (leadfindr restricts to the Find Leads module only,
            dmchamp restores the full app) — modules wins when both are sent.
        modules:
          type: ['array', 'null']
          items:
            type: string
            enum:
              [
                "dashboard",
                "chats",
                "contacts",
                "campaigns",
                "broadcasts",
                "ai_agents",
                "appointments",
                "automations",
                "find_leads",
                "social_scheduler",
                "seo_agent",
                "deals",
                "tasks",
                "knowledge_base",
                "media_library",
              ]
          description: |
            Which app modules this storefront exposes. Omitted leaves the
            config's scoping untouched. null, or a list naming every module,
            restores the full app. Scoping to find_leads alone requires Find
            Leads access on the account. Also recomputes the derived
            root_mounted flag and the legacy product tag; root_mounted
            itself is never accepted from the request.
          examples: [["find_leads"]]
        email_provider_id:
          type: ['string', 'null']
          description: |
            Which of your tenant_email_providers sends transactional email
            branded by this config. Must be one of your own providers. "" or
            null clears it, restoring the fallback chain (agency default
            provider, then the system sender).
        domain:
          type: string
          description: Fully-qualified hostname the config is served on.
          examples: ["app.myagency.com"]
        seo_title:
          type: string
          description: Browser tab / SEO title, up to 200 characters.
        seo_description:
          type: string
          description: SEO meta description, up to 500 characters.
        favicon_url:
          type: string
          description: |
            Favicon / PWA install icon URL. Must be PNG, WebP, or SVG — JPG is
            rejected because it breaks the install-to-home-screen prompt.
            "" clears it.
        logo:
          type: string
          description: Brand logo image URL, shown on auth pages and in transactional emails. "" clears it.
        logo_dark:
          type: string
          description: Dark-mode variant of the brand logo, shown on dark auth backgrounds. "" clears it.
        logo_size:
          type: ['integer', 'string', 'null']
          minimum: 20
          maximum: 72
          description: |
            Logo height in CSS pixels used in the app sidebar; every other
            surface derives its own height from this number. 0 or ""
            restores the platform default (34).
          examples: [40]
        icon_square:
          type: string
          description: Square installable app icon URL. Same PNG/WebP/SVG requirement as favicon_url.
        terms:
          type: string
          description: Terms & Conditions URL shown on the white-label auth pages. "" clears it.
        privacy_policy:
          type: string
          description: Privacy Policy URL shown on the white-label auth pages. "" clears it.
        company_name:
          type: string
          description: Legal/company name, up to 120 characters.
        app_name:
          type: string
          description: Product name shown in the UI, up to 120 characters.
        primary_color:
          type: string
          description: "#RRGGBB hex colour. An empty string clears it."
          examples: ["#25D366"]
        accent_color:
          type: string
          description: "#RRGGBB hex colour. An empty string clears it."
        success_color:
          type: string
          description: "Success/positive accent colour, #RRGGBB. An empty string clears it."
        danger_color:
          type: string
          description: "Danger/destructive accent colour, #RRGGBB. An empty string clears it."
        heading_font:
          type: string
          description: Heading font family, from the platform's allow-listed Google Fonts list. "" clears it.
          examples: ["Inter"]
        body_font:
          type: string
          description: Body font family, from the platform's allow-listed Google Fonts list. "" clears it.
          examples: ["Inter"]
        login_bg_type:
          type: string
          enum: ["default", "gradient", "image", ""]
          description: Login-page background style. "" clears it.
        login_bg_value:
          type: string
          description: CSS gradient string or image URL, matching login_bg_type. Up to 600 characters. "" clears it.
        login_tagline:
          type: string
          description: Tagline shown on the login page, up to 200 characters. Not clearable via empty string.
        login_show_powered_by:
          type: boolean
          description: Show the "Powered by" credit on the login page.
        custom_css:
          type: string
          description: Custom CSS override injected into the branded surfaces, up to 20000 characters. "" clears it.
        density:
          type: string
          enum: ["comfortable", "compact", ""]
          description: UI density. "" clears it; "default" maps to "comfortable".
        hide_emojis:
          type: boolean
          description: Hide the decorative emoji images app-wide on this storefront.
        decoration_style:
          type: string
          enum: ["emoji", "outline", "solid", "duotone", "none"]
          description: Decoration style used across the storefront.
        bg_style:
          type: string
          enum: ["default", "orbs", "grid", "plain", "bloom", "topglow", "grain", "lines", "zigzag", "dots", "ruled"]
          description: Page background style. "" clears it.
        card_style:
          type: string
          enum: ["default", "paper", "flat", "hairline"]
          description: Card material. "" clears it.
        corner_style:
          type: string
          enum: ["default", "soft", "sharp"]
          description: Corner sharpness. "" clears it.
        bg_animation:
          type: boolean
          description: Whether the background pattern drifts (true, default) or holds still.
        show_name_on_sign_in:
          type: boolean
          description: Show the app name above the logo on auth pages.
        show_help_menu:
          type: boolean
        show_api_reference:
          type: boolean
        show_changelog:
          type: boolean
        support_email:
          type: string
          description: Support contact email shown to your account's users.
        email_footer_text:
          type: string
          description: Footer tagline shown under the brand name in transactional emails, up to 200 characters. "" clears it.
        email_footer_links:
          type: array
          maxItems: 4
          description: Custom footer links for transactional emails, up to 4. An empty array clears them.
          items:
            $ref: '#/components/schemas/WhiteLabelEmailFooterLink'
        default_language:
          type: ['string', 'null']
          description: |
            Default locale for transactional emails sent on this storefront's
            behalf to recipients with no account yet. An account-holder's own
            language setting always wins. "" or null clears it.
          examples: ["en"]
    WhiteLabelConfigSaveResponse:
      type: object
      properties:
        success:
          type: boolean
          examples: [true]
        config_id:
          type: string
          examples: ["app.myagency.com"]
        updated:
          type: array
          description: The stored field names that were written. Empty when nothing changed.
          items:
            type: string
      example:
        success: true
        config_id: app.myagency.com
        updated: ["primary_color", "app_name"]
    WhiteLabelConfigCreateInput:
      type: object
      description: |
        Creates a new white-label config, or (with replace_config_id) swaps
        an existing config onto a new domain. All fields other than domain
        are optional at creation and can be filled in afterwards with
        PUT /white-label-config.
      required:
        - domain
      properties:
        domain:
          type: string
          description: Fully-qualified hostname; becomes the config's id. Must not already be in use by any account.
          examples: ["app.myagency.com"]
        seo_title:
          type: string
        seo_description:
          type: string
        favicon_url:
          type: string
          description: Must be PNG, WebP, or SVG — JPG is rejected.
        company_name:
          type: string
        app_name:
          type: string
        logo:
          type: string
        terms:
          type: string
        privacy_policy:
          type: string
        description:
          type: string
        replace_config_id:
          type: string
          description: |
            Id of one of the caller's EXISTING configs to re-key onto the new
            domain in one atomic operation, carrying every other field
            forward. Must name a config the caller owns and must differ from
            domain.
          examples: ["old-app.myagency.com"]
        product:
          type: string
          enum: ["dmchamp", "leadfindr"]
          description: Back-compat alias for modules. Omitted / "dmchamp" is the full app; "leadfindr" scopes to Find Leads only (requires Find Leads access).
        modules:
          type: ['array', 'null']
          items:
            type: string
            enum:
              [
                "dashboard",
                "chats",
                "contacts",
                "campaigns",
                "broadcasts",
                "ai_agents",
                "appointments",
                "automations",
                "find_leads",
                "social_scheduler",
                "seo_agent",
                "deals",
                "tasks",
                "knowledge_base",
                "media_library",
              ]
          description: Which app modules the new storefront exposes. Omitted falls back to product; null (or every module) means the full app.
      example:
        domain: app.myagency.com
        company_name: My Agency
        app_name: My Agency Portal
    WhiteLabelConfigCreateResponse:
      type: object
      properties:
        success:
          type: boolean
          examples: [true]
        configId:
          type: string
          examples: ["app.myagency.com"]
        uid:
          type: string
          examples: ["user_abc123"]
        routing_provider:
          type: string
          enum: ["firebase", "caddy"]
          description: Which infrastructure serves this domain.
          examples: ["caddy"]
        product:
          type: string
          enum: ["dmchamp", "leadfindr"]
          examples: ["dmchamp"]
        modules:
          type: ['array', 'null']
          items:
            type: string
          description: Null means the full app.
        root_mounted:
          type: boolean
          description: Whether this storefront is selected for root-mounted (non-DM-Champ) hosting.
          examples: [false]
        is_primary:
          type: boolean
          description: True when this config was written as the account's primary config.
          examples: [true]
      example:
        success: true
        configId: app.myagency.com
        uid: user_abc123
        routing_provider: caddy
        product: dmchamp
        modules: null
        root_mounted: false
        is_primary: true
    WhiteLabelConfigDeleteResponse:
      type: object
      properties:
        success:
          type: boolean
          examples: [true]
        configId:
          type: string
          description: The deleted config's id, or an empty string when the account had no matching config (idempotent).
          examples: ["app.myagency.com"]
      example:
        success: true
        configId: app.myagency.com
  parameters:
    ApiKeyParam:
      name: apiKey
      in: query
      description: Your DM Champ API key. Alternatively send it as the X-API-Key header or as Authorization Bearer <key>.
      required: false
      schema:
        type: string
      example: "your_api_key"
    UserIdParam:
      name: userId
      in: query
      description: Legacy widget-session identifier. Only accepted by the few endpoints that list it explicitly; use your API key everywhere else.
      required: false
      schema:
        type: string
      example: "your_user_id"

  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: query
      name: apiKey
      description: Your DM Champ API key. Obtain from Settings → API Keys.
    UserIdAuth:
      type: apiKey
      in: query
      name: userId
      description: Legacy widget-session authentication (your DM Champ User ID). Only the few endpoints that list it explicitly accept it; it is not a general alternative to the API key.

  responses:
    SnapshotsForbidden:
      description: |
        Not an agency account, or the account is not on the Agents workspace yet.
        Snapshots package AI Agents, so both are required.
      content:
        application/json:
          schema:
            type: object
            properties:
              success:
                type: boolean
                examples: [false]
              error_code:
                type: integer
                enum: [403]
              error:
                type: string
                enum:
                  - "Snapshots are available to agency accounts only."
                  - "Snapshots require the new Agents workspace. Your account hasn't been migrated yet."
                  - "Target is not part of your agency."
    EntryPointNotFound:
      description: |
        No entry point with that ID on this account. The same answer is given
        for one that belongs to someone else, so entry point IDs cannot be
        guessed.
      content:
        application/json:
          schema:
            type: object
            properties:
              success:
                type: boolean
                examples: [false]
              error:
                type: string
                examples: ["Entry point not found"]
    AgentNotFound:
      description: |
        No agent with that ID on this account. The same answer is given for an
        agent that belongs to someone else, so agent IDs cannot be guessed.
      content:
        application/json:
          schema:
            type: object
            properties:
              success:
                type: boolean
                examples: [false]
              error:
                type: string
                examples: ["Agent not found"]
    CustomFunctionNotFound:
      description: |
        No custom function with that ID on this account. The same answer is
        given for a function that belongs to someone else, so IDs cannot be
        guessed.
      content:
        application/json:
          schema:
            type: object
            properties:
              success:
                type: boolean
                examples: [false]
              error:
                type: string
                examples: ["Custom function not found"]
    CustomFunctionBadRequest:
      description: A field was rejected. The message names the problem.
      content:
        application/json:
          schema:
            type: object
            properties:
              success:
                type: boolean
                examples: [false]
              error:
                type: string
                examples:
                  - "method must be one of GET, POST, PUT, DELETE, PATCH"
                  - "skip_system_data must be a boolean"
                  - "each header must have a string key and a string value"
                  - "input parameter type must be one of string, number, boolean, object, array, query_param"
                  - "headers cannot exceed 200 entries"
    CustomFunctionManagedByAutomation:
      description: |
        This function was created and is kept up to date by one of your
        automations, so it cannot be edited or deleted on its own - change the
        automation instead. The response names the automation.
      content:
        application/json:
          schema:
            type: object
            properties:
              success:
                type: boolean
                examples: [false]
              error:
                type: string
                enum: [managed_by_automation]
              message:
                type: string
                examples: ["This function is managed by an automation. Edit or delete the automation instead."]
              automation_id:
                type: string
                description: The automation that owns this function

paths:
  /health:
    get:
      summary: Health Check
      description: Check if the server is running
      tags:
        - System
      responses:
        '200':
          description: Server is healthy
          content:
            text/plain:
              schema:
                type: string
                examples: ["The server is running healthy! 🥳"]
              example: "The server is running healthy! 🥳"

  /error-log:
    get:
      summary: List Error Log Entries
      description: |
        Every failed action on the account for a time window, newest first:
        failed or undelivered messages, replies the AI skipped, the
        failure-flavoured notifications (channel disconnects, billing, send
        guards, knowledge base imports) and webhooks that are failing or were
        switched off.

        Each entry carries a stable code that the help documentation explains
        along with the fix, so a code can be quoted in a support request.
        Automation run failures are served separately by the automations API and
        merged by the app.

        The endpoint is read-only and scoped to the signed-in account. Provider
        text in the detail field is sanitized before it is returned. Summary counts are
        computed over the whole filtered range, not just the returned page.
      tags:
        - Error Log
      parameters:
        - name: since
          in: query
          required: false
          schema:
            type: string
            format: date-time
          description: Window start (ISO 8601). Defaults to 7 days before until; clamped to at most 90 days before until.
        - name: until
          in: query
          required: false
          schema:
            type: string
            format: date-time
          description: Window end (ISO 8601). Defaults to now.
        - name: category
          in: query
          required: false
          schema:
            type: string
          description: |
            Comma-separated categories to keep. One or more of delivery, ai,
            channel, billing, knowledge_base, broadcast, automation,
            integration, appointments, other.
        - name: channel
          in: query
          required: false
          schema:
            type: string
          description: Comma-separated channels to keep, for example whatsapp,sms,instagram.
        - name: severity
          in: query
          required: false
          schema:
            type: string
          description: Comma-separated severities to keep. One or more of error, warning, info.
        - name: q
          in: query
          required: false
          schema:
            type: string
            maxLength: 64
          description: Free text matched against the contact name, the contact identifier and the code. Longer input is truncated.
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 50
          description: Page size.
        - name: cursor
          in: query
          required: false
          schema:
            type: string
          description: |
            Opaque cursor from a previous response's next_cursor. It is an
            offset into the merged list for the same window, so keep every other
            parameter identical while paging.
      responses:
        '200':
          description: The matching entries plus a summary over the whole filtered range
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  range:
                    type: object
                    properties:
                      since:
                        type: string
                        format: date-time
                      until:
                        type: string
                        format: date-time
                  summary:
                    type: object
                    properties:
                      total:
                        type: integer
                        description: Entries matching the filters across the whole range
                      by_category:
                        type: object
                        additionalProperties:
                          type: integer
                      by_severity:
                        type: object
                        additionalProperties:
                          type: integer
                      truncated:
                        type: boolean
                        description: True when a source hit its per-window row cap, so the totals are a floor
                  entries:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: string
                          description: Stable row id, for example msg:<contactId>:<messageId>, ntf:<id> or whk:<id>
                        occurred_at:
                          type: string
                          format: date-time
                        source:
                          type: string
                          enum: [message, ai_skip, notification, webhook, automation_run]
                        category:
                          type: string
                          enum: [delivery, ai, channel, billing, knowledge_base, broadcast, automation, integration, appointments, other]
                        severity:
                          type: string
                          enum: [error, warning, info]
                        code:
                          type: string
                          examples: ["TW-63016"]
                        provider:
                          type: string
                          enum: [twilio, meta, whatsapp_web, telegram, email, line, viber, linkedin, dmchamp, other]
                        provider_code:
                          type: [integer, "null"]
                          description: The raw numeric provider code, when there was one
                        channel:
                          type: [string, "null"]
                        contact:
                          type: [object, "null"]
                          properties:
                            id:
                              type: string
                            name:
                              type: [string, "null"]
                            identifier:
                              type: [string, "null"]
                        detail:
                          type: [string, "null"]
                          description: Provider or system text, sanitized
                        action_url:
                          type: [string, "null"]
                          description: In-app path that opens the place to fix it
                        meta:
                          type: object
                          additionalProperties: true
                  next_cursor:
                    type: [string, "null"]
              example:
                success: true
                range:
                  since: "2026-08-24T09:00:00.000Z"
                  until: "2026-08-31T09:00:00.000Z"
                summary:
                  total: 2
                  by_category:
                    delivery: 1
                    channel: 1
                  by_severity:
                    error: 2
                  truncated: false
                entries:
                  - id: "msg:abc123:msg987"
                    occurred_at: "2026-08-30T14:12:03.000Z"
                    source: message
                    category: delivery
                    severity: error
                    code: "TW-63016"
                    provider: twilio
                    provider_code: 63016
                    channel: whatsapp
                    contact:
                      id: abc123
                      name: Jane Doe
                      identifier: "+14155551234"
                    detail: null
                    action_url: "/chats?contactRef=abc123"
                    meta:
                      message_id: msg987
                  - id: "ntf:n555"
                    occurred_at: "2026-08-29T08:01:00.000Z"
                    source: notification
                    category: channel
                    severity: error
                    code: "APP-1111"
                    provider: whatsapp_web
                    provider_code: null
                    channel: whatsapp_web
                    contact: null
                    detail: "WhatsApp needs a quick reconnect"
                    action_url: "/settings/channels"
                    meta:
                      notification_type: whatsapp_web_reconnect_required
                next_cursor: null
        '400':
          description: A query parameter was invalid
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    enum: [invalid_since, invalid_until, invalid_range, invalid_limit, invalid_cursor, invalid_category, invalid_channel, invalid_severity]
        '503':
          description: The error log store is not reachable from this runtime
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    enum: [not_available]

  /contacts:
    post:
      summary: Create New Contact
      description: |
        Create a new contact with phone number and optional information.
        
        **New Feature**: You can now optionally add the contact to one or more lists upon creation.
        
        **List Assignment Options**:
        - Use `listId` for adding to a single list (simpler for single list case)
        - Use `listIds` for adding to multiple lists (array format)
        - If both are provided, `listIds` takes precedence
        
        The API will:
        - Validate that all lists exist
        - Verify you have permission to add contacts to each list
        - Create the contact and add it to the specified lists atomically
        
        If any list validation fails, the contact will not be created.
      tags:
        - Contacts
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Contact'
            examples:
              create_contact:
                summary: Create contact without lists
                value:
                  phoneNumber: "+1234567890"
                  firstName: "John"
                  lastName: "Doe"
                  email: "john@example.com"
                  is_bot_active: true
              create_contact_single_list:
                summary: Create contact with single list (using listId)
                value:
                  phoneNumber: "+1234567890"
                  firstName: "Alice"
                  lastName: "Johnson"
                  email: "alice@example.com"
                  is_bot_active: true
                  listId: "list123"
              create_contact_multiple_lists:
                summary: Create contact with multiple lists (using listIds)
                value:
                  phoneNumber: "+1234567890"
                  firstName: "Jane"
                  lastName: "Smith"
                  email: "jane@example.com"
                  is_bot_active: true
                  listIds: ["list123", "list456"]
              create_contact_custom_fields_flat:
                summary: Create contact with custom fields (flat / Make-Zapier friendly)
                value:
                  phoneNumber: "+972555046655"
                  firstName: "Anna"
                  email: "anna@example.com"
                  webinar_date_nh: "Thursday, May 14, 2026 at 09:00"
                  webinar_url_nh: "https://webinardo.com/abc/unique-link"
              create_contact_custom_fields_nested:
                summary: Create contact with custom fields (explicit nested object)
                value:
                  phoneNumber: "+972555046655"
                  firstName: "Anna"
                  email: "anna@example.com"
                  custom_fields:
                    webinar_date_nh: "Thursday, May 14, 2026 at 09:00"
                    webinar_url_nh: "https://webinardo.com/abc/unique-link"
      responses:
        '200':
          description: Contact created successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  data:
                    type: object
                    properties:
                      message:
                        type: string
                        examples: ["Successfully created new contact"]
                      contactId:
                        type: string
                        examples: ["abc123"]
                      listsAdded:
                        type: array
                        items:
                          type: string
                        description: List IDs the contact was added to
                        examples: [["list123", "list456"]]
              example:
                success: true
                data:
                  message: Successfully created new contact
                  contactId: contact_abc123
                  listsAdded:
                    - list_xyz789
        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["Invalid phone number"]
                  error_code:
                    type: integer
                    examples: [400]
              example:
                success: false
                error: Invalid phone number. Ensure it includes a valid country code and is in the correct format.
                error_code: 400
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["Invalid API key"]
        '403':
          description: Permission denied
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["You don't have permission to add contacts to list: list123"]
                  error_code:
                    type: integer
                    examples: [403]
        '404':
          description: List not found
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["List not found: list123"]
                  error_code:
                    type: integer
                    examples: [404]
        '409':
          description: Contact already exists
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["A contact with this phone number already exists"]
                  error_code:
                    type: integer
                    examples: [409]
              example:
                success: false
                error: A contact with this phone number already exists
                error_code: 409
    get:
      summary: Get Contact or List Contacts
      description: |
        Two modes in one endpoint:

        - Lookup mode: pass phoneNumber or email to fetch a single contact. The
          response includes the contact with its lists, tags, and campaigns
          resolved to id and name pairs.
        - List mode: pass neither phoneNumber nor email to page through all
          contacts on the account, newest first. Use the limit and cursor query
          parameters to paginate, and optionally filter by listId, tag and
          excludeTag, or by a full rules object (see the rules parameter). Each
          page returns compact contact summaries plus a
          next_cursor value; pass it as the cursor on the next call, and stop when
          next_cursor is null.
      tags:
        - Contacts
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
        - name: phoneNumber
          in: query
          required: false
          schema:
            type: string
          description: Phone number to look up, in international format. Triggers lookup mode.
        - name: email
          in: query
          required: false
          schema:
            type: string
          description: Email address to look up. Triggers lookup mode.
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 50
          description: Page size in list mode. Defaults to 50, maximum 100.
        - name: cursor
          in: query
          required: false
          schema:
            type: string
          description: Pagination cursor in list mode. Use the next_cursor value from the previous page.
        - name: listId
          in: query
          required: false
          schema:
            type: string
          description: In list mode, only return contacts that belong to this list.
        - name: tag
          in: query
          required: false
          schema:
            type: string
          description: >-
            In list mode, only return contacts carrying this tag. Pass the tag NAME
            (matched ignoring case). A name that does not exist on the account returns
            404 rather than an empty page, so a typo is easy to spot.
        - name: excludeTag
          in: query
          required: false
          schema:
            type: string
          description: >-
            In list mode, only return contacts that do NOT carry this tag. Pass the tag
            NAME (matched ignoring case). Combinable with tag, listId and limit/cursor.
            A name that does not exist on the account returns 404.
        - name: search
          in: query
          required: false
          schema:
            type: string
          description: >-
            In list mode, free-text search. Matches contacts whose first name, last
            name, "first last" full name, phone number, email, WhatsApp name,
            Instagram handle or Telegram username CONTAINS the term, ignoring case
            (it is a contains match, not a starts-with one). Returns the top matches
            ordered by most recent activity with next_cursor null - a picker shows
            best matches, it does not scroll. listId, tag, excludeTag and
            updated_after are ignored while searching.
        - name: updated_after
          in: query
          required: false
          schema:
            type: string
          description: >-
            In list mode, incremental sync. Returns only contacts modified at or
            after this moment, oldest change first, so you can pull just what
            changed since your last sync instead of re-paging the whole account.
            Paginate it with limit and cursor exactly like normal list mode. Accepts
            an ISO 8601 timestamp or epoch milliseconds; an unparseable value is a
            400. updatedAfter is accepted as an alias. Cannot be combined with
            rules, and is ignored while searching.
          example: "2026-05-01T00:00:00Z"
        - name: rules
          in: query
          required: false
          schema:
            type: string
          description: >-
            In list mode, filter contacts with a rules object, URL-encoded as JSON:
            {"match":"all","conditions":[{"field":"status","op":"is_any","value":["Lead"]}]}.
            match is all or any. Each condition names a field and an operator: tags
            (has_any, has_all, has_none) and lists (in_any, not_in_any) take IDs;
            channel and status take values (is_any, is_none); the date fields
            created_at, last_activity_at, last_incoming_message_at,
            last_outgoing_message_at, first_ai_interaction_at and
            last_ai_interaction_at take within_last or not_within_last with
            {"amount":30,"unit":"days"}, or before/after with an ISO date, or
            is_set/not_set; the yes-no fields has_interacted_with_ai,
            is_bot_active, do_not_disturb,
            is_private and has_ever_responded take is with true or false; the text
            fields email, phone_number, first_name and last_name take contains,
            not_contains, is_set or not_set; current_campaign_id and assigned_agent
            take is_any, is_none, is_set or not_set; deal_stage takes pipeline stage
            IDs with is_any, is_none, is_set or not_set; custom_field takes a key plus
            eq, neq, contains, not_contains, is_set or not_set. Up to 20 conditions.
            rules cannot be combined with search, listId, tag, excludeTag or
            updated_after, and it returns the top matches with next_cursor null (no
            paging).
      responses:
        '200':
          description: A single contact (lookup mode) or a page of contact summaries (list mode)
          content:
            application/json:
              schema:
                oneOf:
                  - type: object
                    description: Lookup mode response.
                    properties:
                      success:
                        type: boolean
                      contactId:
                        type: string
                        description: ID of the matched contact.
                      contact:
                        $ref: '#/components/schemas/Contact'
                  - type: object
                    description: List mode response.
                    properties:
                      success:
                        type: boolean
                      contacts:
                        type: array
                        items:
                          $ref: '#/components/schemas/ContactSummary'
                      next_cursor:
                        type: ['string', 'null']
                        description: Cursor for the next page, or null when there are no more results.
        '400':
          description: >-
            Invalid query parameters, for example a malformed phone number, a rules object that is
            not valid JSON or breaks a rule of the DSL, or rules combined with search, listId, tag,
            excludeTag or updated_after.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContactsApiError'
        '404':
          description: No contact matched the phone number or email (lookup mode), or the listId does not exist (list mode)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContactsApiError'
    delete:
      summary: Delete Contacts
      security:
        - ApiKeyAuth: []
      description: |
        Permanently delete one or more contacts by ID in a single call. IDs that
        do not exist on your account are skipped (never deleted) and counted in
        the skipped total. Up to 500 IDs per request. This cannot be undone.
      tags:
        - Contacts
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - contactIds
              properties:
                contactIds:
                  type: array
                  items:
                    type: string
                  maxItems: 500
                  description: IDs of the contacts to delete.
            examples:
              batch:
                summary: Delete two contacts
                value:
                  contactIds: ["contactId1", "contactId2"]
      responses:
        '200':
          description: Deletion result
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  deleted:
                    type: integer
                    description: Number of contacts deleted.
                  skipped:
                    type: integer
                    description: Number of IDs skipped because they were not found on this account.
        '400':
          description: contactIds missing, empty, or over 500 entries
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContactsApiError'
  /contacts/count:
    get:
      summary: Count Contacts
      security:
        - ApiKeyAuth: []
      description: |
        How many contacts match a filter, plus a per-channel split - without paging
        through them. This is the call for ANY "how many" question: a listing shows
        one page and can never be read as a total.

        All filters are optional and combine with AND. Pass agentId to count the
        contacts assigned to one AI agent (or agentId=none for contacts with no
        assigned agent - those are answered by the channel's default agent), channel
        for one channel, tag for a tag NAME, listId for a list, botActive true or
        false for the AI assistant on or off, status for a contact status. Or pass a
        rules object (the smart-list vocabulary, URL-encoded JSON) on its own - rules
        cannot be combined with the other filters. No filter at all counts the whole
        account.
      tags:
        - Contacts
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: agentId
          in: query
          required: false
          schema:
            type: string
          description: >-
            Only contacts assigned to this AI agent. Pass the value none for contacts
            with NO assigned agent.
        - name: channel
          in: query
          required: false
          schema:
            type: string
          description: Only contacts on this channel, for example whatsapp, messenger, instagram, sms, email, chat_widget.
        - name: tag
          in: query
          required: false
          schema:
            type: string
          description: Only contacts carrying this tag. Pass the tag NAME (matched ignoring case); an unknown name is a 404.
        - name: listId
          in: query
          required: false
          schema:
            type: string
          description: Only contacts on this list.
        - name: botActive
          in: query
          required: false
          schema:
            type: boolean
          description: true for contacts whose AI assistant is on, false for off.
        - name: status
          in: query
          required: false
          schema:
            type: string
          description: Only contacts with this status, for example Lead.
        - name: rules
          in: query
          required: false
          schema:
            type: string
          description: >-
            A URL-encoded JSON rules object, same shape as smart_rules on lists. Cannot
            be combined with agentId, channel, tag, listId, botActive or status.
      responses:
        '200':
          description: The count
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  total:
                    type: integer
                    description: Contacts matching the filter.
                  by_channel:
                    type: object
                    additionalProperties:
                      type: integer
                    description: The same contacts split by channel; contacts without a channel are under none.
                  filters:
                    type: object
                    description: The filters that were applied.
              example:
                success: true
                total: 3423
                by_channel:
                  messenger: 2744
                  instagram: 667
                  none: 12
                filters:
                  agentId: agent_xyz789
        '400':
          description: rules combined with another filter, malformed rules, or a non-boolean botActive
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContactsApiError'
        '404':
          description: The tag name or listId does not exist on this account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContactsApiError'
  /contacts/{contactId}:
    put:
      summary: Update Contact
      description: |
        Update an existing contact. Only the fields you send are written; everything
        else is left alone.

        Anything you send that is NOT one of the fields below is stored as a custom
        field (custom_fields.<key>) rather than changing the contact - so a
        misspelled field name is saved, but has no effect. The one exception: user,
        user_ref, id and created_at are dropped entirely, because they decide who
        owns the contact.
      tags:
        - Contacts
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
        - name: contactId
          in: path
          required: true
          schema:
            type: string
          description: ID of the contact to update
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                firstName:
                  type: string
                  description: Contact's first name
                lastName:
                  type: string
                  description: Contact's last name
                email:
                  type: string
                  description: Contact's email address
                  format: email
                phone_number:
                  type: string
                  description: >-
                    New phone number, in international format (a missing + is added
                    for you). An invalid number is a 400.
                channel:
                  type: string
                  description: >-
                    Which phone-based channel the contact is reached on. One of
                    whatsapp, whatsapp_web or sms; anything else is a 400.
                status:
                  type: string
                  description: >-
                    Contact status, for example Lead or Customer. Must be a non-empty
                    string. Setting it to spam is what marks the contact as spam.
                is_spam:
                  type: boolean
                  description: Mark or unmark the contact as spam.
                is_bot_active:
                  type: boolean
                  description: Whether the AI replies to this contact.
                is_private:
                  type: boolean
                  description: Whether the conversation is private (human only).
                do_not_disturb:
                  type: boolean
                  description: >-
                    Stop the AI from engaging this contact at all. To stop only the
                    automated follow-ups while the AI keeps answering what they send,
                    use follow_ups_disabled instead.
                do_not_disturb_reason:
                  type: string
                  description: Why the contact was set to do-not-disturb. Non-empty string.
                lead_profile:
                  type: string
                  description: Free-text lead profile note.
                tag_ids:
                  type: array
                  items:
                    type: string
                  description: Replaces the contact's tags with exactly these tag IDs. Send an empty array to clear them.
                list_ids:
                  type: array
                  items:
                    type: string
                  description: Replaces the contact's list memberships with exactly these list IDs. Send an empty array to clear them.
                bot_message_count:
                  type: integer
                  minimum: 0
                  description: The AI-reply counter shown on the contact. Send 0 to reset it.
                unread_count:
                  type: integer
                  minimum: 0
                  description: Unread badge count for this conversation. Send 0 to clear the badge.
                ignore_counter:
                  type: integer
                  minimum: 0
                  description: How many further inbound messages the AI should skip.
                is_important:
                  type: boolean
                  description: Star the conversation (the inbox "important" filter reads this).
                follow_ups_disabled:
                  type: boolean
                  description: >-
                    Stop every automated follow-up for this contact - for example once
                    they have bought - while the AI keeps replying to anything they
                    send. This is the softer twin of do_not_disturb.
                pinned_at:
                  type: ["string", "null"]
                  description: >-
                    Pin the conversation to the top of the inbox. Send an ISO 8601
                    timestamp (pin order is pin time, newest first), or null to unpin.
                mark_chat_closed:
                  type: boolean
                  description: Mark the conversation closed.
                human_alerted:
                  type: boolean
                  description: Whether a human has been alerted about this conversation.
                is_chat_window_open:
                  type: boolean
                  description: >-
                    Whether the 24 hour WhatsApp customer-care window is open. Only
                    meaningful on the WhatsApp Business API channel.
                skip_resume_follow_up:
                  type: boolean
                  description: Skip the next resume follow-up for this contact.
                sync_to_phone:
                  type: boolean
                  description: Whether to sync this contact to the connected phone.
                bot_waiting_for_contact_to_finish_responding:
                  type: boolean
                  description: Internal chat state - whether the AI is waiting for the contact to finish typing.
                last_user_read_at:
                  type: string
                  description: >-
                    When you last opened this conversation, as an ISO 8601 timestamp or
                    epoch milliseconds. Written alongside unread_count when marking a
                    chat read.
                last_updated_by:
                  type: string
                  description: Who made this change, for the audit trail. Non-empty string.
                pending_paste:
                  type: ["boolean", "null"]
                  description: Whether a pasted attachment is waiting to be sent. Send null to clear it.
                pending_paste_url:
                  type: string
                  description: URL of the pending pasted attachment.
                pending_paste_mime:
                  type: string
                  description: MIME type of the pending pasted attachment.
                current_response_id:
                  type: ["string", "null"]
                  description: The in-flight AI response id. Send null to clear it.
                incomplete_reply:
                  type: ["string", "null"]
                  description: A partially written AI reply kept after an interruption. Send null to clear it.
                ai_model_override:
                  type: ["string", "null"]
                  description: Force a specific AI model for this contact. Send null to go back to the account default.
                custom_fields:
                  type: object
                  additionalProperties: true
                  description: |
                    Per-key merge: only the keys you send are written, the rest of the
                    existing custom_fields map is preserved. You can also pass custom
                    field keys at the top level — anything not in the standard schema
                    is auto-routed into custom_fields.
            examples:
              update_contact:
                value:
                  firstName: "John"
                  lastName: "Doe"
                  email: "john@example.com"
                  is_bot_active: false
                  do_not_disturb: true
              update_contact_custom_fields:
                summary: Update only custom fields (nested or flat both work)
                value:
                  custom_fields:
                    webinar_date_nh: "Thursday, May 21, 2026 at 09:00"
                    webinar_url_nh: "https://webinardo.com/abc/new-link"
      responses:
        '200':
          description: Contact updated successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  message:
                    type: string
                    examples: ["Contact updated successfully"]
              example:
                success: true
                message: Contact updated successfully
        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  message:
                    type: string
                    examples: ["No fields to update"]
              example:
                success: false
                message: No fields to update
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                type: string
                examples: ["Invalid API key"]
        '404':
          description: Contact not found
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  message:
                    type: string
                    examples: ["Contact not found"]
              example:
                success: false
                message: Contact not found
        '500':
          description: Server error
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["Internal Server Error"]
                  error_code:
                    type: integer
                    examples: [500]
              example:
                success: false
                error: Internal Server Error
                error_code: 500
    get:
      summary: Get Contact By ID
      security:
        - ApiKeyAuth: []
      description: |
        Fetch a single contact by its ID. Returns the same enriched contact
        object as the lookup mode of the contact search endpoint, including
        lists, tags, and campaigns resolved to id and name pairs, plus the last
        message exchanged. Returns 404 when the contact does not exist on your
        account.
      tags:
        - Contacts
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
        - name: contactId
          in: path
          required: true
          schema:
            type: string
          description: ID of the contact to fetch.
      responses:
        '200':
          description: The requested contact
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  contactId:
                    type: string
                    description: ID of the contact.
                  contact:
                    $ref: '#/components/schemas/Contact'
        '404':
          description: Contact not found on this account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContactsApiError'
    delete:
      summary: Delete Contact
      security:
        - ApiKeyAuth: []
      description: |
        Permanently delete one contact and everything attached to it - its whole
        message history included. This cannot be undone.

        To remove several contacts at once, use the batch delete on /contacts
        instead of calling this repeatedly.
      tags:
        - Contacts
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
        - name: contactId
          in: path
          required: true
          schema:
            type: string
          description: ID of the contact to delete.
      responses:
        '200':
          description: The contact was deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
        '404':
          description: Contact not found on this account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContactsApiError'
  /tags:
    post:
      summary: Tag a Contact
      description: >
        Assign a tag to a contact. If the tag does not exist yet, it is created automatically.
        If the tag already exists, the contact is added to it.
        Identify the contact by phone number, email, or contactId — useful for Instagram, Messenger
        and chat widget contacts that do not have a phone number.
      tags:
        - Tags
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
        - name: tag
          in: query
          required: true
          schema:
            type: string
          description: The tag name to assign (created automatically if it does not exist)
        - name: phoneNumber
          in: query
          required: false
          schema:
            type: string
          description: Phone number of the contact (include country code, e.g. +15551234567). Provide one of phoneNumber, email, or contactId.
        - name: email
          in: query
          required: false
          schema:
            type: string
          description: Email address of the contact. Provide one of phoneNumber, email, or contactId.
        - name: contactId
          in: query
          required: false
          schema:
            type: string
          description: Contact ID (as returned in webhook payloads). Provide one of phoneNumber, email, or contactId.
      responses:
        '201':
          description: Tag assigned to contact successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    $ref: '#/components/schemas/Tag'
              example:
                success: true
                data:
                  name: vip
                  description: High-value customer
        '400':
          description: Missing required parameters (tag and one of phoneNumber, email, or contactId)
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  error:
                    type: string
              example:
                success: false
                error: 'Missing required parameters: tag and one of phoneNumber, email, or contactId'
        '401':
          description: Invalid API key
        '402':
          description: Insufficient credits
        '404':
          description: Contact not found
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  error:
                    type: string
              example:
                success: false
                error: Contact not found
    get:
      summary: Get All Tags
      description: Retrieve all tags
      tags:
        - Tags
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
      responses:
        '200':
          description: List of tags
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Tag'
              example:
                - name: vip
                  description: High-value customer
                - name: lead
                  description: Inbound prospect
                - name: support
                  description: Open support ticket
    delete:
      summary: Delete Tags in Bulk
      security:
        - ApiKeyAuth: []
      description: |
        Deletes up to 1000 tags by ID from the caller's tag collection in one call. Tags that no
        longer exist are silently skipped (anti-enumeration - a missing-or-foreign id cannot be
        distinguished), so the deleted count can be less than the number of IDs sent. To delete a
        single tag, use DELETE /tags/{tagId} (see leftover #2 below - note the ID, not the name).
      tags:
        - Tags
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - tagIds
              properties:
                tagIds:
                  type: array
                  description: Non-empty array of tag document IDs (max 1000).
                  items:
                    type: string
            examples:
              delete_batch:
                value:
                  tagIds: ["tagId1", "tagId2"]
      responses:
        '200':
          description: Tags deleted (or already gone)
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  deleted:
                    type: integer
                    description: Number of tags actually deleted. Can be less than tagIds.length when some were already gone.
        '400':
          description: tagIds missing, empty, contains non-string entries, or exceeds 1000 entries
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string

  /tags/{name}:
    get:
      summary: Get Tag by Name
      description: Retrieve a specific tag by name
      tags:
        - Tags
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
        - name: name
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Tag found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Tag'
              example:
                name: vip
                description: High-value customer
        '404':
          description: Tag not found
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  error:
                    type: string
              example:
                success: false
                error: Tag not found
    put:
      summary: Update Tag
      security:
        - ApiKeyAuth: []
      description: |
        Partially updates a tag. Despite the {name} path template, this identifies the tag by its
        document ID — pass
        the id field from GET /tags or POST /tags, not the tag's display name. user and id in the
        body are silently dropped; a renamed name is trimmed the same way tag creation trims it.
      tags:
        - Tags
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
        - name: name
          in: path
          required: true
          description: The tag's document ID (NOT its display name, despite the path template — see the mismatch note).
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              description: At least one field. Only name is validated (trimmed, rejected if blank); other fields you send are stored as-is.
              properties:
                name:
                  type: string
                description:
                  type: string
            examples:
              rename:
                value:
                  name: "vip-renewed"
      responses:
        '200':
          description: The tag was updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  tag_id:
                    type: string
        '400':
          description: tagId missing, body empty, or the new name is blank after trimming
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
        '404':
          description: Tag not found (also returned for a tag belonging to another account — anti-enumeration)
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["Tag not found"]
    delete:
      summary: Delete Tag
      security:
        - ApiKeyAuth: []
      description: |
        Deletes a single tag by ID. Despite the {name} path template, this identifies the tag by its
        document ID. An
        unknown/foreign id under this account returns 200 with deleted:0 rather than 404 — there is
        nothing to enumerate. For bulk delete, use DELETE /tags (leftover #1 above).
      tags:
        - Tags
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
        - name: name
          in: path
          required: true
          description: The tag's document ID (NOT its display name, despite the path template — see the mismatch note).
          schema:
            type: string
      responses:
        '200':
          description: Delete attempted — check deleted for whether a tag actually existed
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  deleted:
                    type: integer
                    description: 1 if a tag was deleted, 0 if the id did not exist under this account.
        '400':
          description: tagId missing
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string

  /contacts/lists:
    post:
      summary: Add Contact To List
      description: Add a specific contact to a list
      tags:
        - Contacts
        - Lists
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - contactId
                - listId
              properties:
                contactId:
                  type: string
                  description: ID of the contact to add to the list
                listId:
                  type: string
                  description: ID of the list to add the contact to
            examples:
              add_contact_to_list:
                value:
                  contactId: "contact123"
                  listId: "list456"
      responses:
        '200':
          description: Contact added to list successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  message:
                    type: string
                    examples: ["Contact successfully added to list My List"]
              example:
                success: true
                message: Contact successfully added to list My List
        '400':
          description: Missing required parameters
          content:
            application/json:
              schema:
                type: string
                examples: ["Missing required parameters"]
              example: Missing required parameters
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                type: string
                examples: ["Invalid API key"]
        '403':
          description: Permission denied
          content:
            application/json:
              schema:
                type: string
                examples: ["You don't have permission to add this contact to a list"]
              example: You don't have permission to add this contact to a list
        '500':
          description: Server error
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  message:
                    type: string
                    examples: ["Error occurred while adding contact to list."]
              example:
                success: false
                message: Error occurred while adding contact to list.
    delete:
      summary: Remove Contact From List
      description: Remove a specific contact from a list
      tags:
        - Contacts
        - Lists
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: contactId
          in: query
          required: true
          schema:
            type: string
          description: ID of the contact to remove from the list
          example: "contact123"
        - name: listId
          in: query
          required: true
          schema:
            type: string
          description: ID of the list to remove the contact from
          example: "list456"
      responses:
        '200':
          description: Contact removed from list successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  message:
                    type: string
                    examples: ["Contact successfully removed from list My List"]
              example:
                success: true
                message: Contact successfully removed from list My List
        '400':
          description: Missing required parameters
          content:
            application/json:
              schema:
                type: string
                examples: ["Missing required parameters"]
              example: Missing required parameters
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                type: string
                examples: ["Invalid API key"]
        '403':
          description: Permission denied
          content:
            application/json:
              schema:
                type: string
                examples: ["You don't have permission to remove this contact from a list"]
              example: You don't have permission to remove this contact from a list
        '500':
          description: Server error
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  message:
                    type: string
                    examples: ["Error occurred while adding contact to list."]
              example:
                success: false
                message: Error occurred while removing contact from list.

  /contacts/tags/{contactId}:
    get:
      summary: Get Contact Tags
      description: Get all tags associated with a contact
      tags:
        - Contacts
        - Tags
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
        - name: contactId
          in: path
          required: true
          schema:
            type: string
          description: ID of the contact to get tags for
      responses:
        '200':
          description: List of contact tags
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Tag'
              example:
                - name: vip
                  description: High-value customer
                - name: lead
                  description: Inbound prospect
        '400':
          description: Missing contact ID
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  message:
                    type: string
                    examples: ["Missing required parameters. Please provide a contactId"]
              example:
                success: false
                message: 'Missing required parameters. Please provide a contactId'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["Invalid API key"]
        '404':
          description: Contact not found
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["Contact not found"]
              example:
                success: false
                error: Contact not found

  /contacts/messages/{contactId}:
    get:
      summary: Get Contact Messages
      description: Retrieve messages for a specific contact
      tags:
        - Contacts
        - Messages
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
        - name: contactId
          in: path
          required: true
          schema:
            type: string
          description: ID of the contact to get messages for
      responses:
        '200':
          description: List of messages
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Message'
              example:
                success: true
                data:
                  - messageSid: msg_01H8X9Y2Z3
                    body: Hi, I have a question about my order
                    direction: inbound
                    status: received
                    timestamp: '2026-04-29T08:15:00Z'
                  - messageSid: msg_01H8X9Y2Z4
                    body: Sure, what's your order number?
                    direction: outbound
                    status: delivered
                    timestamp: '2026-04-29T08:16:12Z'
        '400':
          description: Missing contact ID
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  message:
                    type: string
                    examples: ["Missing required parameters. Please provide a contactId"]
              example:
                success: false
                message: 'Missing required parameters. Please provide a contactId'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["Invalid API key"]
        '404':
          description: Contact not found
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["Contact not found"]
              example:
                success: false
                error: Contact not found

  /summaries:
    post:
      summary: Generate Chat Summary
      description: Generate a chat summary for a specific contact based on their conversation history
      tags:
        - Summaries
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                phoneNumber:
                  type: string
                  description: Contact's phone number (must include valid country code)
                email:
                  type: string
                  format: email
                  description: Contact's email address
              oneOf:
                - required: [phoneNumber]
                - required: [email]
            examples:
              withPhoneNumber:
                value:
                  phoneNumber: "+1234567890"
              withEmail:
                value:
                  email: "contact@example.com"
      responses:
        '200':
          description: Summary generation initiated successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  message:
                    type: string
                    examples: ["Chat summary generated successfully"]
              example:
                success: true
                message: Chat summary generated successfully
        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                type: string
                examples: ["Missing required parameters. Please provide a phone number or email."]
              example: Missing required parameters. Please provide a phone number or email.
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                type: string
                examples: ["Invalid API key"]
        '404':
          description: Contact not found
          content:
            application/json:
              schema:
                type: string
                examples: ["Contact not found"]
              example: Contact not found
        '500':
          description: Server error
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                    examples: ["Internal Server Error"]

  /scripts/chat-widget.js:
    get:
      summary: Get Chat Widget Script
      description: Returns the JavaScript code needed to embed the chat widget on a website
      tags:
        - Chat Widget
      responses:
        '200':
          description: JavaScript code for chat widget
          content:
            application/javascript:
              schema:
                type: string
                description: JavaScript code
              example: |
                (function () {
                  var iframe = document.createElement('iframe');
                  iframe.src = 'https://api.dmchamp.com/v1/chat-widget/' + window.DMChampConfigId;
                  iframe.style.cssText = 'position:fixed;bottom:20px;right:20px;width:380px;height:560px;border:0;z-index:2147483647;';
                  document.body.appendChild(iframe);
                })();

  /chat-widget-config:
    get:
      summary: Get Chat Widget Configuration
      description: Returns the configuration for the chat widget
      tags:
        - Chat Widget
      parameters:
        - name: configId
          in: query
          required: true
          schema:
            type: string
          description: ID of the chat widget configuration
      responses:
        '200':
          description: Chat widget configuration
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  data:
                    type: object
                    description: Widget configuration data
              example:
                success: true
                data:
                  configId: widget_abc123
                  name: Support Widget
                  primaryColor: '#1F6FEB'
                  welcomeMessage: 'Hi! How can we help today?'
                  position: bottom-right
                  avatarUrl: 'https://cdn.example.com/avatar.png'
        '404':
          description: Configuration not found
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["Configuration not found"]
              example:
                success: false
                error: Configuration not found

  /chat-widget-configs/{configId}:
    patch:
      summary: Update Chat Widget Configuration
      security:
        - ApiKeyAuth: []
      description: |
        Partially updates your chat widget configuration. Only the fields you
        include in the request body are changed; everything else is left as
        is. Use this to toggle widget features (for example the attachment /
        upload button, emoji button, lead capture, privacy consent) or to
        change appearance without opening the dashboard.

        The configId is the same identifier used in your embed snippet
        (window.DMChampConfigId) and by GET /chat-widget-config. The
        widget picks up changes on its next page load.

        Requires "edit" permission on the Integrations area (account owners
        always have it).
      tags:
        - Chat Widget
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
        - name: configId
          in: path
          required: true
          schema:
            type: string
          description: ID of the chat widget configuration to update
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              description: Any subset of the fields below. Unknown fields are ignored.
              properties:
                name:
                  type: string
                  maxLength: 120
                  description: Display name shown in the widget header
                description:
                  type: string
                  maxLength: 2000
                  description: Short description / subtitle shown under the name
                opening_message:
                  type: string
                  maxLength: 2000
                  description: First message the widget shows to a visitor
                logo_url:
                  type: string
                  format: uri
                  description: Public https URL of the logo / avatar image
                launcher_icon:
                  type: string
                  enum: [chat, chat-dots, send, help, bolt, smile, avatar, custom]
                  description: >-
                    Floating launcher button icon. avatar reuses logo_url;
                    custom uses launcher_icon_url.
                launcher_icon_url:
                  type: string
                  format: uri
                  description: Public https URL of a custom launcher icon (used when launcher_icon is custom)
                colors:
                  type: object
                  properties:
                    primary:
                      type: string
                      example: '#1F6FEB'
                    primary_foreground:
                      type: string
                      example: '#FFFFFF'
                    secondary:
                      type: string
                    secondary_foreground:
                      type: string
                    background:
                      type: string
                allowed_domains:
                  type: array
                  maxItems: 50
                  items:
                    type: string
                  description: Hostnames the widget may be embedded on (empty = any)
                lead_capture_enabled:
                  type: boolean
                  description: Ask visitors for their contact details before chatting
                lead_capture_title:
                  type: string
                  maxLength: 120
                lead_capture_subtitle:
                  type: string
                  maxLength: 2000
                privacy_policy_enabled:
                  type: boolean
                  description: Show the privacy consent checkbox / notice
                privacy_policy_url:
                  type: string
                  format: uri
                show_upload_button:
                  type: boolean
                  description: Show or hide the attachment (file upload) button in the composer
                show_emoji_button:
                  type: boolean
                  description: Show or hide the emoji picker button in the composer
                theme:
                  type: string
                  enum: [classic, glass]
                corner_style:
                  type: string
                  enum: [default, soft, sharp]
                font_style:
                  type: string
                  enum: [default, serif, rounded, mono]
            example:
              show_upload_button: false
              launcher_icon: chat-dots
              colors:
                primary: '#d60808'
      responses:
        '200':
          description: Configuration updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  config_id:
                    type: string
              example:
                success: true
                config_id: widget_abc123
        '400':
          description: Validation error (unknown enum value, non-boolean toggle, invalid URL or hostname)
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
              example:
                success: false
                error: "show_upload_button must be a boolean"
        '401':
          description: Missing or invalid API key
        '404':
          description: Configuration not found (or not owned by this account)
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
              example:
                success: false
                error: Chat widget config not found

  /chat-widget-demo/{configId}:
    get:
      summary: Get Hosted Client Demo Page
      description: >-
        Returns a standalone HTML page that renders the given client website
        inside a full-viewport iframe with the chat widget floating on top.
        Intended to be opened directly in a browser (a shareable demo link),
        not consumed programmatically. When the target site forbids being
        framed, a neutral fallback page is served and the widget still loads.
      tags:
        - Chat Widget
      parameters:
        - name: configId
          in: path
          required: true
          schema:
            type: string
          description: ID of the chat widget configuration to overlay
        - name: site
          in: query
          required: true
          schema:
            type: string
          description: >-
            The client website URL to preview. May be a bare hostname
            (https:// is assumed) or a full http(s) URL. URL-encode the value.
      responses:
        '200':
          description: HTML demo page
          content:
            text/html:
              schema:
                type: string
                description: HTML content
        '400':
          description: Missing/invalid site parameter or config id
          content:
            text/plain:
              schema:
                type: string
              example: "Invalid demo link: the site parameter must be a valid http(s) website URL."
        '404':
          description: Widget configuration not found
          content:
            text/plain:
              schema:
                type: string
              example: This demo link does not exist (widget not found).

  /chat-widget/{configId}:
    get:
      summary: Get Chat Widget
      description: Returns the HTML for the chat widget with the specified configuration
      tags:
        - Chat Widget
      parameters:
        - name: configId
          in: path
          required: true
          schema:
            type: string
          description: ID of the chat widget configuration
      responses:
        '200':
          description: HTML content for the chat widget
          content:
            text/html:
              schema:
                type: string
                description: HTML content
              example: |
                <!DOCTYPE html>
                <html>
                  <head><meta charset="utf-8"><title>Chat Widget</title></head>
                  <body>
                    <div id="dmchamp-chat" data-config-id="widget_abc123"></div>
                    <script src="/scripts/chat-widget.js"></script>
                  </body>
                </html>
        '404':
          description: Widget not found
          content:
            text/plain:
              schema:
                type: string
                examples: ["Widget not found"]
              example: Widget not found

  /chat-widget/{configId}/{fromId}:
    get:
      summary: Get Chat Widget with From ID
      description: Returns the HTML for the chat widget with the specified configuration and from ID
      tags:
        - Chat Widget
      parameters:
        - name: configId
          in: path
          required: true
          schema:
            type: string
          description: ID of the chat widget configuration
        - name: fromId
          in: path
          required: true
          schema:
            type: string
          description: ID of the source (for tracking purposes)
      responses:
        '200':
          description: HTML content for the chat widget
          content:
            text/html:
              schema:
                type: string
                description: HTML content
              example: |
                <!DOCTYPE html>
                <html>
                  <head><meta charset="utf-8"><title>Chat Widget</title></head>
                  <body>
                    <div id="dmchamp-chat" data-config-id="widget_abc123" data-from-id="visitor_xyz789"></div>
                    <script src="/scripts/chat-widget.js"></script>
                  </body>
                </html>
        '404':
          description: Widget not found
          content:
            text/plain:
              schema:
                type: string
                examples: ["Widget not found"]
              example: Widget not found

  /incoming-chat-widget-message:
    post:
      summary: Handle Incoming Chat Widget Message
      description: Processes an incoming message from the chat widget and publishes it to a PubSub topic
      tags:
        - Chat Widget
        - Messages
      parameters:
        - name: userId
          in: query
          required: true
          schema:
            type: string
          description: ID of the user/owner to whom the chat widget belongs
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - customData
                - messageType
              properties:
                customData:
                  type: object
                  required:
                    - body
                  properties:
                    messageSid:
                      type: string
                      description: Unique identifier for the message
                    id:
                      type: string
                      description: Alternative unique identifier for the message
                    fromId:
                      type: string
                      description: ID of the sender (visitor)
                    toId:
                      type: string
                      description: ID of the chat widget
                    body:
                      type: string
                      description: Message content
                    status:
                      type: string
                      description: Message status
                      default: "received"
                    mediaUrl:
                      type: ["string", "null"]
                      description: URL to media content (if any)
                    mediaContentType:
                      type: ["string", "null"]
                      description: MIME type of media content
                messageType:
                  type: string
                  description: Type of message (e.g., text, reaction)
                  default: "text"
            examples:
              textMessage:
                summary: Simple text message
                value: {
                  "customData": {
                    "messageSid": "msg123",
                    "fromId": "visitor123",
                    "toId": "widget456",
                    "body": "Hello from chat widget",
                    "status": "received"
                  },
                  "messageType": "text"
                }
              mediaMessage:
                summary: Message with media
                value: {
                  "customData": {
                    "messageSid": "msg123",
                    "fromId": "visitor123",
                    "toId": "widget456",
                    "body": "Check out this image",
                    "status": "received",
                    "mediaUrl": "https://example.com/image.jpg",
                    "mediaContentType": "image/jpeg"
                  },
                  "messageType": "text"
                }
      responses:
        '200':
          description: Message processed successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  messageId:
                    type: string
                    description: PubSub message ID
                    examples: ["1234567890"]
              example:
                success: true
                messageId: '5638579871234567'
        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                    examples: ["Message body cannot be empty"]
                  details:
                    type: string
                    description: Additional error details
              example:
                error: Message body cannot be empty
        '405':
          description: Method not allowed
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                    examples: ["Method not allowed"]
              example:
                error: Method not allowed
        '500':
          description: Server error
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                    examples: ["Internal server error"]
              example:
                error: Internal server error

  /chat-widget-messages:
    get:
      summary: Get Chat Widget Messages
      description: Retrieves messages for a specific chat widget conversation
      tags:
        - Chat Widget
        - Messages
      parameters:
        - name: fromId
          in: query
          required: true
          schema:
            type: string
          description: ID of the user/owner of the contact
        - name: toId
          in: query
          required: true
          schema:
            type: string
          description: ID of the chat widget that the contact is using
        - name: after
          in: query
          required: false
          schema:
            type: string
            format: int64
          description: Optional timestamp (in milliseconds) to fetch only messages after this time
      responses:
        '200':
          description: List of messages with bot typing status
          content:
            application/json:
              schema:
                type: object
                properties:
                  messages:
                    type: array
                    items:
                      type: object
                      properties:
                        messageSid:
                          type: string
                          description: Unique message identifier
                        body:
                          type: string
                          description: Message content
                        mediaUrl:
                          type: ["string", "null"]
                          description: URL to media content (if any)
                        mediaContentType:
                          type: ["string", "null"]
                          description: MIME type of media content
                        timestamp:
                          type: string
                          format: date-time
                          description: Message timestamp
                        type:
                          type: string
                          enum: ["text", "image"]
                          description: Message type
                        direction:
                          type: string
                          enum: ["inbound", "outbound", "outbound-api"]
                          description: Message direction (inbound = from user, outbound = from system)
                        status:
                          type: string
                          description: Message delivery status
                  isBotTyping:
                    type: boolean
                    description: Whether the bot is currently typing a response
                    examples: [false]
              examples:
                success:
                  value: {
                    "messages": [
                      {
                        "messageSid": "msg123abc",
                        "body": "Hello there, how can I help you?",
                        "mediaUrl": null,
                        "mediaContentType": null,
                        "timestamp": "2023-05-15T14:30:45.123Z",
                        "type": "text",
                        "direction": "outbound",
                        "status": "delivered"
                      },
                      {
                        "messageSid": "msg456def",
                        "body": "I have a question about your services",
                        "mediaUrl": null,
                        "mediaContentType": null,
                        "timestamp": "2023-05-15T14:30:15.456Z",
                        "type": "text",
                        "direction": "inbound",
                        "status": "delivered"
                      }
                    ],
                    "isBotTyping": false
                  }
        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                    examples: ["Missing required parameters"]
        '404':
          description: Contact not found
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                    examples: ["Contact not found"]
        '500':
          description: Server error
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                    examples: ["Internal server error"]
                  details:
                    type: string
                    description: Error details (only provided in development environment)

  /update-message-read-status:
    post:
      summary: Mark Chat Widget Messages as Read
      description: Updates the read status of all unread messages for a specific chat widget conversation
      tags:
        - Chat Widget
        - Messages
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - fromId
                - toId
              properties:
                fromId:
                  type: string
                  description: ID of the user/owner of the contact
                toId:
                  type: string
                  description: ID of the chat widget that the contact is using
            examples:
              basic:
                value:
                  fromId: "user123"
                  toId: "widget456"
      responses:
        '200':
          description: Messages marked as read successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
              example:
                success: true
        '400':
          description: Missing required parameters
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                    examples: ["Missing required parameters"]
              example:
                error: Missing required parameters
        '404':
          description: Contact not found
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                    examples: ["Contact not found"]
              example:
                error: Contact not found
        '500':
          description: Server error
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                    examples: ["Internal server error"]
                  details:
                    type: string
                    description: Error details (only provided in development environment)
              example:
                error: Internal server error

  /whatsapp-widget/{phone}:
    get:
      summary: Get WhatsApp Widget
      description: Returns the HTML for the WhatsApp widget for the specified phone number
      tags:
        - WhatsApp Widget
      parameters:
        - name: phone
          in: path
          required: true
          schema:
            type: string
          description: Phone number with country code
      responses:
        '200':
          description: HTML content for the WhatsApp widget
          content:
            text/html:
              schema:
                type: string
                description: HTML content
              example: |
                <!DOCTYPE html>
                <html>
                  <body>
                    <a href="https://wa.me/15551234567" class="wa-button" style="background:#25D366;color:#fff;padding:12px 18px;border-radius:24px;">
                      Chat on WhatsApp
                    </a>
                  </body>
                </html>

  /whatsapp-widget/{phone}/{message}:
    get:
      summary: Get WhatsApp Widget with Message
      description: Returns the HTML for the WhatsApp widget for the specified phone number and pre-filled message
      tags:
        - WhatsApp Widget
      parameters:
        - name: phone
          in: path
          required: true
          schema:
            type: string
          description: Phone number with country code
        - name: message
          in: path
          required: true
          schema:
            type: string
          description: Pre-filled message
      responses:
        '200':
          description: HTML content for the WhatsApp widget
          content:
            text/html:
              schema:
                type: string
                description: HTML content
              example: |
                <!DOCTYPE html>
                <html>
                  <body>
                    <a href="https://wa.me/15551234567?text=Hello%2C%20I%27d%20like%20to%20book" class="wa-button" style="background:#25D366;color:#fff;padding:12px 18px;border-radius:24px;">
                      Chat on WhatsApp
                    </a>
                  </body>
                </html>

  /whatsapp-templates/send:
    post:
      summary: Send WhatsApp Template to Contact
      description: |
        Send a WhatsApp template to a contact, even if the chat is closed. The API will also reopen the chat session.
        
        **Template Variable Processing:**
        Templates support advanced variable syntax with the following features:
        
        1. **Basic Variables**: {{first_name}}, {{email}}, {{company}}
        2. **Default Values**: {{field|Default Text}} - Shows default if field is empty
        3. **Text Transformations**: 
           - {{field|uppercase}} - Converts to UPPERCASE
           - {{field|lowercase}} - Converts to lowercase
           - {{field|capitalize}} - Capitalizes first letter of each word
        4. **Combined Syntax**: {{field|Default Value|transformation}}
           Example: {{company|Your Company|uppercase}}
        
        **Note:** WhatsApp templates require Meta approval. The system automatically converts named variables to numbered placeholders ({{1}}, {{2}}, etc.) for WhatsApp API compatibility.
      tags:
        - WhatsApp Templates
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: Optional. Send the template from a managed account you administer. May also be sent as a field in the JSON body.
          schema:
            type:
              - string
              - "null"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - contactId
                - whatsappTemplateId
              properties:
                contactId:
                  type: string
                  description: ID of the contact to send the template to
                whatsappTemplateId:
                  type: string
                  description: ID of the WhatsApp template to send
                variables:
                  type: object
                  additionalProperties:
                    type: string
                  description: |
                    Optional. Explicit values for the template's variables, keyed by variable NAME.
                    A name listed here overrides the value that would be read from the contact;
                    names you leave out keep resolving from the contact's fields as before.
                    Use this for values the contact record cannot hold — most importantly the code
                    of an authentication (one-time password) template, which must be shorter than
                    15 characters.
                  example:
                    code: "482913"
            examples:
              withApiKey:
                summary: Send WhatsApp template using API key
                value: {
                  "contactId": "contact123",
                  "whatsappTemplateId": "template456"
                }
              withAdvancedTemplate:
                summary: Template with advanced variable syntax
                value: {
                  "contactId": "contact123",
                  "whatsappTemplateId": "template789"
                }
                description: |
                  Template body example: "Hi {{first_name|there}}, welcome to {{company|Our Store|uppercase}}!"
                  Result: "Hi John, welcome to OUR STORE!" (or defaults if fields are empty)
              withExplicitVariables:
                summary: Authentication template with an explicit OTP code
                value: {
                  "contactId": "contact123",
                  "whatsappTemplateId": "template999",
                  "variables": {
                    "code": "482913"
                  }
                }
                description: |
                  Template variable "code" is sent as 482913 instead of being looked up on the
                  contact. Any variable you do not list still resolves from the contact's fields.
      responses:
        '200':
          description: Template sent successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  data:
                    type: string
                    examples: ["WhatsApp template message sent successfully"]
              example:
                success: true
                data: WhatsApp template message sent successfully
        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error_code:
                    type: integer
                    examples: [400]
                  error:
                    type: string
              example:
                success: false
                error_code: 400
                error: 'Missing required fields: contactId and whatsappTemplateId are required'
        '403':
          description: Twilio credentials not found
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error_code:
                    type: integer
                    examples: [403]
                  error:
                    type: string
        '500':
          description: Server error
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error_code:
                    type: integer
                    examples: [500]

  /whatsapp-templates:
    get:
      summary: Get WhatsApp Templates
      description: Retrieve all WhatsApp templates for the authenticated user
      tags:
        - WhatsApp Templates
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: Optional. List the WhatsApp templates of a managed account you administer.
          schema:
            type:
              - string
              - "null"
      responses:
        '200':
          description: Successfully retrieved WhatsApp templates
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  data:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: string
                          description: Template ID
                        name:
                          type: string
                          description: Template name
                        status:
                          type: string
                          description: Template approval status
                        language:
                          type: string
                          description: Template language
                        category:
                          type: string
                          description: Template category
                        components:
                          type: array
                          description: Template components
                        createdAt:
                          type: string
                          format: date-time
                          description: Template creation timestamp
                        template:
                          type: object
                          description: Full template data
              example:
                success: true
                data:
                  - id: template_abc123
                    name: welcome_message
                    status: approved
                    language: en_US
                    category: MARKETING
                    createdAt: '2026-04-29T08:15:00Z'
                  - id: template_def456
                    name: appointment_reminder
                    status: approved
                    language: en_US
                    category: UTILITY
                    createdAt: '2026-04-29T09:00:00Z'
        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error_code:
                    type: integer
                    examples: [400]
                  error:
                    type: string
                    examples: ["Authentication failed. Please provide a valid API key."]
              example:
                success: false
                error_code: 400
                error: 'Authentication failed. Please provide a valid API key.'
        '500':
          description: Server error
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error_code:
                    type: integer
                    examples: [500]
                  error:
                    type: string
                    examples: ["Error occurred while retrieving WhatsApp templates"]
              example:
                success: false
                error_code: 500
                error: Error occurred while retrieving WhatsApp templates
    post:
      summary: Create WhatsApp Template for Campaign
      security:
        - ApiKeyAuth: []
      description: |
        Create a WhatsApp template for a campaign's opening message and submit it for approval.

        The template is written to the campaign identified by campaign_id. Behaviour depends on
        the campaign's channels:

        - Campaigns using the WhatsApp Business API channel: the template content is submitted
          for WhatsApp review. The response includes the review status (usually received or
          pending) and the content ID.
        - Campaigns that only use channels without an external review step: the template is
          stored and auto-approved immediately, with no content ID.
        - Campaigns with no WhatsApp channel at all: nothing is created and the response
          reports campaign_status not_applicable.

        Variable placeholders may be written as double-curly, single-curly, or square-bracket
        names; they are normalized to the canonical double-curly form. The body is limited to
        1024 characters.
      tags:
        - WhatsApp Templates
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: Optional. Create the campaign template on a managed account you administer. May also be sent as a field in the JSON body.
          schema:
            type:
              - string
              - "null"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - campaign_id
                - name
                - language
                - body
              properties:
                campaign_id:
                  type: string
                  description: ID of the campaign the template belongs to
                name:
                  type: string
                  description: Template name
                language:
                  type: string
                  description: Template language code (for example en, es, de, pt_BR, zh_CN)
                body:
                  type: string
                  maxLength: 1024
                  description: Template body text, up to 1024 characters
                variables:
                  type: array
                  description: Ordered list of variable names used in the body. Optional - variable names are also derived from the body itself.
                  items:
                    type: string
            example:
              campaign_id: campaign123
              name: welcome_message
              language: en
              body: Hi {{first_name}}, thanks for reaching out!
              variables:
                - first_name
      responses:
        '201':
          description: Template created and submitted (or auto-approved)
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  campaign_status:
                    type: string
                    description: 'Resulting template status on the campaign: received, pending, approved, or rejected'
                  template_sid:
                    type: ['string', 'null']
                    description: Content ID when the template was submitted for review, null when auto-approved
                  message:
                    type: string
              example:
                success: true
                campaign_status: pending
                template_sid: HXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
        '400':
          description: Validation error (missing fields, unsupported language, body too long, or the content was rejected)
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  error:
                    type: string
              example:
                success: false
                error: 'Missing required fields: campaign_id, name, language, body'
        '200':
          description: Not applicable — the campaign's channel does not use Meta-approved templates, so nothing was submitted
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  campaign_status:
                    type: string
                    examples: [not_applicable]
                  message:
                    type: string
                    examples: ["This channel does not require WhatsApp template approval"]
        '404':
          description: Campaign not found
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["Campaign not found"]
  /whatsapp-templates/docs:
    post:
      summary: Create WhatsApp Template
      security:
        - ApiKeyAuth: []
      description: |
        Create a standalone template in your account's template library. This is the create
        step of the template lifecycle the other template endpoints work with: create here,
        edit with PUT /whatsapp-templates/{templateId}, send for review with
        POST /whatsapp-templates/{templateId}/submit, follow the outcome with
        GET /whatsapp-templates/{templateId}/status, and remove with
        DELETE /whatsapp-templates/{templateId}.

        This is not the same as POST /whatsapp-templates, which requires campaign_id and
        writes the template straight into that campaign's opening-message slot. Here
        campaign_id is optional and only links the new template to a campaign.

        The template is stored as a draft by default and is not sent anywhere. Create it with
        status submitted to queue it for WhatsApp review immediately - the review is handled in
        the background, so poll the status endpoint for the outcome. Only draft and submitted
        may be set at creation time; every other status (approved, rejected and the review
        states) is assigned by the review process.

        Variable placeholders may be written as double-curly, single-curly, or square-bracket
        names; they are normalized to the canonical double-curly form. The body is limited to
        1024 characters.
      tags:
        - WhatsApp Templates
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: Optional. Create the template on a managed account you administer. May also be sent as a field in the JSON body.
          schema:
            type:
              - string
              - "null"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - name
                - language
                - body
              properties:
                name:
                  type: string
                  description: Template name
                language:
                  type: string
                  description: Template language code (for example en, es, de, pt_BR, zh_CN)
                body:
                  type: string
                  maxLength: 1024
                  description: Template body text, up to 1024 characters
                variables:
                  type: array
                  description: Ordered list of variable names used in the body. Optional.
                  items:
                    type: string
                status:
                  type: string
                  enum: [draft, submitted]
                  default: draft
                  description: draft stores the template without submitting it; submitted queues it for WhatsApp review. Defaults to draft when omitted.
                type:
                  type: string
                  enum: [general, smart_followup]
                  default: general
                  description: Template kind. Defaults to general.
                category:
                  type: string
                  enum: [marketing, utility, authentication, authentication-international]
                  description: Pricing category of the template. Optional.
                campaign_id:
                  type: string
                  description: Optional ID of a campaign to link the template to. The campaign must belong to the same account.
            example:
              name: welcome_message
              language: en
              body: Hi {{first_name}}, thanks for reaching out!
              variables:
                - first_name
              status: draft
              category: marketing
      responses:
        '201':
          description: Template created
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  template_id:
                    type: string
                    description: ID of the newly created template, used by the other template endpoints
                  status:
                    type: string
                    description: 'The status the template was stored with: draft or submitted'
              example:
                success: true
                template_id: template123
                status: draft
        '400':
          description: Validation error (missing name, language or body, unsupported language, invalid status, type or category, body over 1024 characters, or the content was rejected)
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
              example:
                success: false
                error: 'Missing required field: name'
        '403':
          description: WhatsApp templates are not enabled for this account
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["WhatsApp templates are not enabled for this account"]
        '404':
          description: The campaign referenced by campaign_id was not found on this account
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["Campaign not found"]
  /whatsapp-web/send:
    post:
      summary: Send WhatsApp Message via WhatsApp Web
      description: |
        Send a WhatsApp message directly to a phone number via WhatsApp Web.

        **Key Features:**
        - Send to any phone number (E.164 format required, e.g., +1234567890)
        - Automatic contact creation if contact doesn't exist
        - Message chunking: Messages are automatically split by double newlines (\n\n) into separate WhatsApp messages
        - Natural typing simulation: Delays between chunks simulate human typing speed (500 chars/min)
        - Typing indicators: Shows "typing..." status during message sending
        - Free to send: no per-message credit cost (the channel uses your own paired number)

        **Requirements:**
        - Active WhatsApp Web connection required
        - Phone number must be in E.164 format (e.g., +1234567890)

        **Message Chunking Example:**

        Input message: "Hey Sohaib! 👋\n\nGreat news about your feature request!\n\nI'll let you know when it's live!"

        Results in 3 separate WhatsApp messages:
        1. Hey Sohaib! 👋
        2. Great news about your feature request!
        3. I'll let you know when it's live!

        **Note:** This endpoint uses WhatsApp Web, not the Meta Business API, so no template approval is required.
      tags:
        - WhatsApp Web
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - phoneNumber
                - message
              properties:
                phoneNumber:
                  type: string
                  description: Recipient's phone number in E.164 format (e.g., +1234567890)
                  example: "+1234567890"
                  pattern: "^[+][1-9][0-9]{1,14}$"
                message:
                  type: string
                  description: Message text. Will be automatically split by double newlines into separate messages.
                  example: |-
                    Hey Sohaib! 👋

                    Great news about your feature request!

                    I'll let you know when it's live!
            examples:
              singleMessage:
                summary: Send single message
                value:
                  phoneNumber: "+1234567890"
                  message: "Hello! This is a test message."
              chunkedMessage:
                summary: Send chunked message (will split into 3 messages)
                value:
                  phoneNumber: "+1234567890"
                  message: |-
                    Hey there! 👋

                    Great news about your feature request!

                    I'll let you know when it's live!
      responses:
        '202':
          description: |
            Send accepted and queued. The actual WhatsApp Web dispatch (with
            humanlike typing delays and chunking) runs asynchronously, so the
            HTTP response returns immediately. If scheduledFor is present,
            the send was queued for the next active-hours window; otherwise
            it will fire as soon as the worker picks it up.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  queued:
                    type: boolean
                    examples: [true]
                  contactId:
                    type: string
                    description: ID of the contact (created or existing)
                    examples: ["contact123"]
                  scheduledFor:
                    type: string
                    format: date-time
                    description: ISO timestamp when the send is scheduled to fire (only present when queued for outside active hours).
                    examples: ["2026-05-06T08:00:00.000Z"]
              example:
                success: true
                queued: true
                contactId: contact_abc123
        '400':
          description: Bad request - Invalid input
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                  code:
                    type: string
              examples:
                missingFields:
                  summary: Missing required fields
                  value: {
                    "success": false,
                    "error": "Missing required fields: phoneNumber and message are required",
                    "code": "MISSING_REQUIRED_FIELDS"
                  }
                invalidPhone:
                  summary: Invalid phone number format
                  value: {
                    "success": false,
                    "error": "Invalid phone number format. Must be E.164 format (e.g., +1234567890)",
                    "code": "INVALID_PHONE_FORMAT"
                  }
                contactDND:
                  summary: Contact on Do Not Disturb list
                  value: {
                    "success": false,
                    "error": "Contact is on Do Not Disturb list. Reason: Previous message failed",
                    "code": "CONTACT_DND"
                  }
        '401':
          description: Unauthorized - Invalid API key
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["Invalid API key"]
                  code:
                    type: string
                    examples: ["INVALID_API_KEY"]
        '402':
          description: Payment required - Insufficient credits
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["Insufficient credits. Required: 3, Available: 1"]
                  code:
                    type: string
                    examples: ["INSUFFICIENT_CREDITS"]
        '500':
          description: Internal server error
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                  code:
                    type: string
                    examples: ["INTERNAL_ERROR"]
              examples:
                noConnection:
                  summary: No WhatsApp Web connection
                  value: {
                    "success": false,
                    "error": "User has no WhatsApp Web connection",
                    "code": "SEND_FAILED"
                  }
                sendFailed:
                  summary: Message send failed
                  value: {
                    "success": false,
                    "error": "Failed to send message",
                    "code": "SEND_FAILED"
                  }

  /incoming-custom-channel-message:
    post:
      summary: Handle Custom Channel Message
      description: Handles incoming messages from custom channels. Campaign Assignment Priority - 1. Explicit campaignId (highest priority) 2. Keyword matching (existing logic) 3. Default campaign (fallback). Backwards Compatibility - All new fields are optional. Existing integrations continue to work unchanged.
      tags:
        - Messages
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - customData
                - messageType
              properties:
                customData:
                  type: object
                  required:
                    - body
                  properties:
                    messageSid:
                      type: string
                      description: Unique identifier for the message
                    id:
                      type: string
                      description: Alternative unique identifier for the message
                    fromId:
                      type: string
                      description: ID of the sender
                    toId:
                      type: string
                      description: ID of the receiver
                    body:
                      type: string
                      description: Message content
                    status:
                      type: string
                      description: Message status
                      default: "received"
                    mediaUrl:
                      type: ["string", "null"]
                      description: URL to media content (if any)
                    mediaContentType:
                      type: ["string", "null"]
                      description: MIME type of media content
                    channel:
                      type: ["string", "null"]
                      description: Custom channel identifier
                    campaignId:
                      type: ["string", "null"]
                      description: Explicit campaign ID to assign contact to (highest priority)
                    firstName:
                      type: ["string", "null"]
                      description: Contact's first name (for contact creation/update)
                    lastName:
                      type: ["string", "null"]
                      description: Contact's last name (for contact creation/update)
                    email:
                      type: ["string", "null"]
                      description: Contact's email address (for contact creation/update)
                messageType:
                  type: string
                  description: Type of message (e.g., text, reaction)
                  default: "text"
            examples:
              textMessage:
                summary: Simple text message
                value: {
                  "customData": {
                    "messageSid": "msg123",
                    "fromId": "user123",
                    "toId": "business456",
                    "body": "Hello from custom channel",
                    "status": "received",
                    "channel": "custom"
                  },
                  "messageType": "text"
                }
              messageWithCampaign:
                summary: Message with explicit campaign assignment
                value: {
                  "customData": {
                    "messageSid": "msg123",
                    "fromId": "user123",
                    "toId": "business456",
                    "body": "Hello from custom channel",
                    "status": "received",
                    "channel": "telegram",
                    "campaignId": "camp_456",
                    "firstName": "John",
                    "lastName": "Doe",
                    "email": "john@example.com"
                  },
                  "messageType": "text"
                }
              mediaMessage:
                summary: Message with media
                value: {
                  "customData": {
                    "messageSid": "msg123",
                    "fromId": "user123",
                    "toId": "business456",
                    "body": "Check out this image",
                    "status": "received",
                    "mediaUrl": "https://example.com/image.jpg",
                    "mediaContentType": "image/jpeg",
                    "channel": "custom"
                  },
                  "messageType": "text"
                }
      responses:
        '200':
          description: Message processed successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  messageId:
                    type: string
                    examples: ["1234567890"]
              example:
                success: true
                messageId: '5638579871234567'
        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                    examples: ["Message body cannot be empty"]
                  details:
                    type: string
                    description: Additional error details
              example:
                error: Message body cannot be empty
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                    examples: ["Missing API key or userId in query parameters"]
              example:
                error: Missing API key or userId in query parameters
        '500':
          description: Server error
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                    examples: ["Internal server error"]
              example:
                error: Internal server error

  /incoming_custom_channel_message:
    post:
      summary: Handle Custom Channel Message (Alternative)
      description: Alternative endpoint for handling incoming messages from custom channels. Same functionality as /incoming-custom-channel-message. Campaign Assignment Priority - 1. Explicit campaignId (highest priority) 2. Keyword matching (existing logic) 3. Default campaign (fallback). Backwards Compatibility - All new fields are optional. Existing integrations continue to work unchanged.
      tags:
        - Messages
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - customData
                - messageType
              properties:
                customData:
                  type: object
                  required:
                    - body
                  properties:
                    messageSid:
                      type: string
                      description: Unique identifier for the message
                    id:
                      type: string
                      description: Alternative unique identifier for the message
                    fromId:
                      type: string
                      description: ID of the sender
                    toId:
                      type: string
                      description: ID of the receiver
                    body:
                      type: string
                      description: Message content
                    status:
                      type: string
                      description: Message status
                      default: "received"
                    mediaUrl:
                      type: ["string", "null"]
                      description: URL to media content (if any)
                    mediaContentType:
                      type: ["string", "null"]
                      description: MIME type of media content
                    channel:
                      type: ["string", "null"]
                      description: Custom channel identifier
                    campaignId:
                      type: ["string", "null"]
                      description: Explicit campaign ID to assign contact to (highest priority)
                    firstName:
                      type: ["string", "null"]
                      description: Contact's first name (for contact creation/update)
                    lastName:
                      type: ["string", "null"]
                      description: Contact's last name (for contact creation/update)
                    email:
                      type: ["string", "null"]
                      description: Contact's email address (for contact creation/update)
                messageType:
                  type: string
                  description: Type of message (e.g., text, reaction)
                  default: "text"
            examples:
              textMessage:
                summary: Simple text message
                value: {
                  "customData": {
                    "messageSid": "msg123",
                    "fromId": "user123",
                    "toId": "business456",
                    "body": "Hello from custom channel",
                    "status": "received",
                    "channel": "custom"
                  },
                  "messageType": "text"
                }
      responses:
        '200':
          description: Message processed successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  messageId:
                    type: string
                    examples: ["1234567890"]
              example:
                success: true
                messageId: '5638579871234567'
        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                    examples: ["Message body cannot be empty"]
                  details:
                    type: string
                    description: Additional error details
              example:
                error: Message body cannot be empty
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                    examples: ["Missing API key or userId in query parameters"]
              example:
                error: Missing API key or userId in query parameters
        '500':
          description: Server error
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                    examples: ["Internal server error"]
              example:
                error: Internal server error

  /send-custom-channel-message:
    post:
      summary: Send Custom Channel Message
      description: Send an outbound message to a custom channel contact. Creates contact if it doesn't exist.
      tags:
        - Messages
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - customData
              properties:
                customData:
                  type: object
                  required:
                    - fromId
                    - customChannel
                    - body
                  properties:
                    fromId:
                      type: string
                      description: External ID of the contact
                    customChannel:
                      type: string
                      description: Custom channel identifier
                    body:
                      type: string
                      description: Message content to send
                    campaignId:
                      type: ["string", "null"]
                      description: Campaign ID to assign contact to (if creating new contact)
                    firstName:
                      type: ["string", "null"]
                      description: Contact's first name (for contact creation/update)
                    lastName:
                      type: ["string", "null"]
                      description: Contact's last name (for contact creation/update)
                    email:
                      type: ["string", "null"]
                      description: Contact's email address (for contact creation/update)
            examples:
              simpleOutbound:
                summary: Simple outbound message
                value: {
                  "customData": {
                    "fromId": "user_123",
                    "customChannel": "telegram",
                    "body": "Hello! How can I help you today?"
                  }
                }
              outboundWithCampaign:
                summary: Outbound message with campaign assignment
                value: {
                  "customData": {
                    "fromId": "user_123",
                    "customChannel": "telegram",
                    "body": "Hello! How can I help you today?",
                    "campaignId": "camp_456",
                    "firstName": "Jane",
                    "lastName": "Smith",
                    "email": "jane@example.com"
                  }
                }
      responses:
        '200':
          description: Message sent successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  messageId:
                    type: string
                    description: ID of the created message
                  contactId:
                    type: string
                    description: ID of the contact
                  message:
                    type: string
                    examples: ["Message sent successfully"]
              example:
                success: true
                messageId: msg_01H8X9Y2Z3
                contactId: contact_abc123
                message: Message sent successfully
        '400':
          description: Bad request
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                    examples: ["Missing required fields: fromId, customChannel, and body are required"]
              example:
                error: 'Missing required fields: fromId, customChannel, and body are required'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                    examples: ["Invalid API key"]
        '500':
          description: Server error
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["Internal server error"]
                  message:
                    type: string
                    description: Detailed error message
              example:
                success: false
                error: Internal server error
                message: Failed to publish message to custom channel

  /send_custom_channel_message:
    post:
      summary: Send Custom Channel Message (Alternative)
      description: Alternative endpoint for sending outbound messages to custom channel contacts. Same functionality as /send-custom-channel-message. Creates contact if it doesn't exist.
      tags:
        - Messages
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - customData
              properties:
                customData:
                  type: object
                  required:
                    - fromId
                    - customChannel
                    - body
                  properties:
                    fromId:
                      type: string
                      description: External ID of the contact
                    customChannel:
                      type: string
                      description: Custom channel identifier
                    body:
                      type: string
                      description: Message content to send
                    campaignId:
                      type: ["string", "null"]
                      description: Campaign ID to assign contact to (if creating new contact)
                    firstName:
                      type: ["string", "null"]
                      description: Contact's first name (for contact creation/update)
                    lastName:
                      type: ["string", "null"]
                      description: Contact's last name (for contact creation/update)
                    email:
                      type: ["string", "null"]
                      description: Contact's email address (for contact creation/update)
            examples:
              simpleOutbound:
                summary: Simple outbound message
                value: {
                  "customData": {
                    "fromId": "user_123",
                    "customChannel": "telegram",
                    "body": "Hello! How can I help you today?"
                  }
                }
      responses:
        '200':
          description: Message sent successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  messageId:
                    type: string
                    description: ID of the created message
                  contactId:
                    type: string
                    description: ID of the contact
                  message:
                    type: string
                    examples: ["Message sent successfully"]
              example:
                success: true
                messageId: msg_01H8X9Y2Z3
                contactId: contact_abc123
                message: Message sent successfully
        '400':
          description: Bad request
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                    examples: ["Missing required fields: fromId, customChannel, and body are required"]
              example:
                error: 'Missing required fields: fromId, customChannel, and body are required'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                    examples: ["Invalid API key"]
        '500':
          description: Server error
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["Internal server error"]
                  message:
                    type: string
                    description: Detailed error message
              example:
                success: false
                error: Internal server error
                message: Failed to publish message to custom channel

  /sync-custom-channel-message:
    post:
      summary: Sync Custom Channel Message
      description: |
        Syncs an externally-sent message (e.g., from GoHighLevel) back into DM Champ without re-sending it via the custom channel webhook.

        Use cases:
        1. A user manually sends a message from GHL — the AI gets context of what was sent.
        2. A GHL workflow sends an automated outbound message to a lead — the AI knows what was said before the lead ever replies.

        The message is stored with synced_from_history: true and sent_manually: true, which prevents the outbound trigger from re-sending it to the webhook.

        If the contact doesn't exist yet and customChannel is provided, the contact is auto-created.
        Optionally pauses the AI agent on the contact (default: true).
      tags:
        - Messages
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - customData
              properties:
                customData:
                  type: object
                  required:
                    - fromId
                    - body
                  properties:
                    fromId:
                      type: string
                      description: External contact identifier (e.g., GHL contact ID)
                    body:
                      type: string
                      description: Message body text
                    customChannel:
                      type: string
                      description: Custom channel name (e.g., "GoHighLevel"). Required if contact doesn't exist yet (enables auto-creation).
                    messageSid:
                      type: string
                      description: External message ID for deduplication. If a message with this ID already exists, sync is skipped.
                    pauseAi:
                      type: boolean
                      description: Whether to pause the AI agent on this contact. Defaults to true.
                      default: true
                    mediaUrl:
                      type: string
                      description: Optional media URL
                    mediaContentType:
                      type: string
                      description: Optional media content type (e.g., "image/jpeg")
                    timestamp:
                      type: integer
                      description: Optional timestamp in Unix milliseconds. Defaults to current time if not provided.
                    firstName:
                      type: string
                      description: Contact's first name (used for contact creation if contact doesn't exist yet)
                    lastName:
                      type: string
                      description: Contact's last name (used for contact creation if contact doesn't exist yet)
                    email:
                      type: string
                      description: Contact's email address (used for contact creation if contact doesn't exist yet)
                    campaignId:
                      type: string
                      description: Campaign ID to assign (used during contact creation if contact doesn't exist yet)
            examples:
              syncFromGHL:
                summary: Sync a message sent from GoHighLevel
                value: {
                  "customData": {
                    "fromId": "ghl-contact-id-123",
                    "body": "Hey, I just replied from GHL!",
                    "customChannel": "GoHighLevel",
                    "messageSid": "ghl-msg-456",
                    "pauseAi": true
                  }
                }
              syncWithMedia:
                summary: Sync a message with media attachment
                value: {
                  "customData": {
                    "fromId": "ghl-contact-id-123",
                    "body": "Check out this image",
                    "customChannel": "GoHighLevel",
                    "mediaUrl": "https://example.com/image.jpg",
                    "mediaContentType": "image/jpeg"
                  }
                }
              syncAutomatedWorkflow:
                summary: Sync an automated GHL workflow message (keep AI active)
                value: {
                  "customData": {
                    "fromId": "ghl-contact-id-789",
                    "body": "Hi! Thanks for signing up. We will be in touch shortly.",
                    "customChannel": "GoHighLevel",
                    "pauseAi": false,
                    "timestamp": 1710331200000
                  }
                }
      responses:
        '200':
          description: Message synced successfully (or duplicate detected)
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  messageId:
                    type: string
                    description: ID of the created (or existing duplicate) message
                  contactId:
                    type: string
                    description: ID of the contact
                  aiPaused:
                    type: boolean
                    description: Whether the AI was paused on this contact
                  contactCreated:
                    type: boolean
                    description: Whether a new contact was created
                  message:
                    type: string
                    examples: ["Message synced successfully"]
              example:
                success: true
                messageId: msg_01H8X9Y2Z3
                contactId: contact_abc123
                aiPaused: true
                contactCreated: false
                message: Message synced successfully
        '400':
          description: Bad request - missing required fields or API key
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                    examples: ["Missing required fields: fromId and body are required"]
              example:
                error: 'Missing required fields: fromId and body are required'
        '401':
          description: Unauthorized - invalid API key
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                    examples: ["Invalid API key"]
        '404':
          description: Contact not found (and no customChannel provided for auto-creation)
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                    examples: ["Contact not found"]
                  message:
                    type: string
                    description: Detailed explanation with guidance
              example:
                error: Contact not found
                message: Provide a customChannel field to enable contact auto-creation.
        '500':
          description: Server error
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["Internal server error"]
                  message:
                    type: string
                    description: Detailed error message
              example:
                success: false
                error: Internal server error
                message: Failed to write synced message to Firestore

  /sync_custom_channel_message:
    post:
      summary: Sync Custom Channel Message (Alternative)
      description: Alternative endpoint for syncing externally-sent messages. Same functionality as /sync-custom-channel-message.
      tags:
        - Messages
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - customData
              properties:
                customData:
                  type: object
                  required:
                    - fromId
                    - body
                  properties:
                    fromId:
                      type: string
                      description: External contact identifier (e.g., GHL contact ID)
                    body:
                      type: string
                      description: Message body text
                    customChannel:
                      type: string
                      description: Custom channel name (e.g., "GoHighLevel"). Required if contact doesn't exist yet.
                    messageSid:
                      type: string
                      description: External message ID for deduplication
                    pauseAi:
                      type: boolean
                      description: Whether to pause the AI agent on this contact
                      default: true
                    mediaUrl:
                      type: string
                      description: Optional media URL
                    mediaContentType:
                      type: string
                      description: Optional media content type
                    timestamp:
                      type: integer
                      description: Optional timestamp in Unix milliseconds
                    firstName:
                      type: string
                      description: Contact's first name (for contact creation)
                    lastName:
                      type: string
                      description: Contact's last name (for contact creation)
                    email:
                      type: string
                      description: Contact's email (for contact creation)
                    campaignId:
                      type: string
                      description: Campaign ID (for contact creation)
            examples:
              syncFromGHL:
                summary: Sync a message sent from GoHighLevel
                value: {
                  "customData": {
                    "fromId": "ghl-contact-id-123",
                    "body": "Hey, I just replied from GHL!",
                    "customChannel": "GoHighLevel"
                  }
                }
      responses:
        '200':
          description: Message synced successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  messageId:
                    type: string
                    description: ID of the created message
                  contactId:
                    type: string
                    description: ID of the contact
                  aiPaused:
                    type: boolean
                    description: Whether the AI was paused
                  contactCreated:
                    type: boolean
                    description: Whether a new contact was created
                  message:
                    type: string
                    examples: ["Message synced successfully"]
              example:
                success: true
                messageId: msg_01H8X9Y2Z3
                contactId: contact_abc123
                aiPaused: true
                contactCreated: false
                message: Message synced successfully
        '400':
          description: Bad request
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                    examples: ["Missing required fields: fromId and body are required"]
              example:
                error: 'Missing required fields: fromId and body are required'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                    examples: ["Invalid API key"]
        '404':
          description: Contact not found
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                    examples: ["Contact not found"]
                  message:
                    type: string
              example:
                error: Contact not found
                message: Provide a customChannel field to enable contact auto-creation.
        '500':
          description: Server error
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["Internal server error"]
                  message:
                    type: string
                    description: Detailed error message
              example:
                success: false
                error: Internal server error
                message: Failed to write synced message to Firestore

  /subaccounts:
    post:
      summary: Create SubAccount
      description: |
        Create a new sub-account within your agency. Only available for agency and developer accounts.

        **Credits are an allowance, not a prepayment.** The `usage_limits.credits` and
        `usage_limits.monthly_credits` you set are spending caps for the sub-account; they are
        **not** debited from your agency credit pool at creation time. Your pool is charged only
        as the sub-account actually consumes credits. Consequently there is no upfront pool check —
        creation succeeds even if your pool is currently low (usage simply stops once the pool is
        empty), and lowering or removing a sub-account's allowance later does not move credits back
        to your pool because none were moved out of it in the first place.
      tags:
        - SubAccounts
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - email
                - first_name
                - last_name
                - usage_limits
              properties:
                email:
                  type: string
                  format: email
                  description: Sub-account user's email address
                first_name:
                  type: string
                  description: Sub-account user's first name
                last_name:
                  type: string
                  description: Sub-account user's last name
                business_name:
                  type: string
                  description: Business name for the sub-account
                description:
                  type: string
                  description: Business description
                address_line:
                  type: string
                  description: Business address line
                city:
                  type: string
                  description: Business city
                country:
                  type: string
                  description: Business country
                postal_code:
                  type: string
                  description: Business postal code
                state:
                  type: string
                  description: Business state
                time_zone_id:
                  type: string
                  description: Time zone identifier
                language:
                  type: string
                  description: Preferred language
                usage_limits:
                  type: object
                  required:
                    - monthly_credits
                  properties:
                    monthly_credits:
                      type: integer
                      minimum: 1
                      description: Total monthly credits allocated
                    monthly_credits_used:
                      type: integer
                      default: 0
                      description: Number of monthly credits used
                    credits:
                      type: integer
                      description: Initial credit balance (defaults to monthly_credits if not specified)
                    roll_over_to_next_month:
                      type: boolean
                      default: false
                      description: Whether unused credits roll over to next month
                    team_seats_limit:
                      type: integer
                      minimum: -1
                      nullable: true
                      description: |
                        Explicit numeric team-seat cap for the sub-account: how many team
                        members its owner may invite. `0` = team members not included,
                        `-1` = unlimited, any positive integer = that exact cap.

                        Wins over the `team_seats_*` feature presets, so you can grant any
                        number (2, 7, 15, ...) instead of only 3/5/10/unlimited. Omit (or set
                        `null` later) to fall back to the presets. Change it after creation
                        via `PUT /subaccounts/{subAccountUid}/limits`.

                        A cap set here counts as a MANUAL grant: if the sub-account later
                        subscribes to one of your SaaS plans, buying a plan replaces it with
                        the plan's own seat cap (when the plan defines one), but unattended
                        plan RENEWALS never overwrite a manual cap.
                    agent_limit:
                      type: integer
                      minimum: -1
                      nullable: true
                      description: |
                        Explicit numeric AI-agent cap for the sub-account: how many AI agents
                        it may have. `0` = it cannot create AI agents itself (you can still
                        push one down from your agency), `-1` = unlimited, any positive
                        integer = that exact cap. Omit to leave the count unmanaged.

                        Enforced when the sub-account creates or duplicates an AI agent.
                        Same MANUAL-grant provenance rules as `team_seats_limit` above, and
                        changeable later via `PUT /subaccounts/{subAccountUid}/limits`.
                    channel_limit:
                      type: integer
                      minimum: -1
                      nullable: true
                      description: |
                        Explicit numeric CONNECTED-CHANNEL cap for the sub-account: how many
                        messaging channels it may have connected at once. `0` = it cannot
                        connect any, `-1` = unlimited, any positive integer = that exact
                        cap. Omit to leave the count unmanaged (the sub-account then falls
                        back to its `channels_1`/`channels_3`/`channels_unlimited`
                        feature, which remains the presence gate).

                        Enforced on every connect path (WhatsApp, WhatsApp Web, Telegram,
                        LINE, Viber, TikTok, Instagram, email, iMessage, number purchase).
                        Instagram and Messenger share ONE page connection and count as ONE
                        channel. RE-connecting a channel already on the account is never
                        blocked. Same MANUAL-grant provenance rules as `team_seats_limit`
                        above, and changeable later via
                        `PUT /subaccounts/{subAccountUid}/limits`.
                send_account_info_to_sub_account:
                  type: boolean
                  default: false
                  description: |
                    When true, emails the new sub-account's login credentials (email + temporary
                    password) and a sign-in link to `email` immediately after creation. When false
                    or omitted, no email is sent — you're responsible for delivering the temporary
                    password returned in the response to your client yourself.
                guided_onboarding:
                  type: boolean
                  default: true
                  description: |
                    Whether the sub-account owner is taken through the guided Setup Wizard the first
                    time they sign in. Defaults to `true`.

                    Set `false` for done-for-you clients you configure yourself: they land straight
                    on the dashboard, and the **Setup Wizard** entry is hidden from their side nav.
                    You can re-show that entry later via `PUT /subaccounts/{uid}/menu-visibility`
                    (the two are independent — menu visibility controls reachability, this flag
                    controls the forced first-sign-in redirect).
                features:
                  type: array
                  description: |
                    Optional list of feature IDs to grant the sub-account. When omitted, a sensible default
                    feature set is applied (unlimited channels, AI understanding modalities, contact tagging,
                    unlimited contacts/campaigns, webhooks, API access, follow-ups, advanced mode, etc.).

                    Tiered features within the same group (e.g. `channels_1` / `channels_3` /
                    `channels_unlimited`, or the `ai_responses_per_chat_*` / `campaign_context_*` /
                    `team_seats_*` ladders) are mutually exclusive — the highest tier you include wins.

                    The `channel_*` IDs are a separate, independent allow-list of WHICH channel types
                    the sub-account may connect — orthogonal to the `channels_1` / `channels_3` /
                    `channels_unlimited` COUNT tiers, and both gates apply. Include NONE of them and
                    the sub-account has no channel-type restriction (the historical behaviour, which
                    is why existing sub-accounts are unaffected). Include one or more and the account
                    may only connect exactly those channel types; the rest appear locked on its
                    Channels page with an upgrade note. Use `PUT /subaccounts/{subAccountUid}/features`
                    to change the list later.

                    Unknown IDs are rejected with `400 Bad Request`.
                  items:
                    type: string
                    enum:
                      - channels_1
                      - channels_3
                      - channels_unlimited
                      - channel_chat_widget
                      - channel_whatsapp_api
                      - channel_whatsapp_web
                      - channel_instagram
                      - channel_messenger
                      - channel_telegram
                      - channel_line
                      - channel_email
                      - channel_sms
                      - channel_imessage
                      - reply_to_comments
                      - image_understanding
                      - voice_note_understanding
                      - video_understanding
                      - document_understanding
                      - ai_appointment_booking
                      - real_time_web_search
                      - advanced_mode
                      - ai_responses_per_chat_20
                      - ai_responses_per_chat_50
                      - ai_responses_per_chat_100
                      - ai_responses_per_chat_unlimited
                      - contact_tagging
                      - contacts_unlimited
                      - automatic_follow_ups
                      - webhooks
                      - api_access
                      - custom_functions
                      - byok
                      - campaign_context_20k
                      - campaign_context_50k
                      - campaign_context_75k
                      - campaign_context_100k
                      - sub_account_management
                      - team_seats_3
                      - team_seats_5
                      - team_seats_10
                      - team_seats_unlimited
                      - tasks
                      - daily_summaries
                      - ai_media_library
                      - mcp_servers
            examples:
              createSubAccount:
                value: {
                  "email": "subaccount@example.com",
                  "first_name": "John",
                  "last_name": "Doe",
                  "business_name": "Sample Business",
                  "description": "Business description",
                  "address_line": "123 Main St",
                  "city": "Sample City",
                  "country": "US",
                  "postal_code": "12345",
                  "state": "CA",
                  "time_zone_id": "America/Los_Angeles",
                  "language": "en",
                  "usage_limits": {
                    "monthly_credits": 1000,
                    "credits": 1000,
                    "roll_over_to_next_month": false
                  },
                  "features": [
                    "channels_unlimited",
                    "image_understanding",
                    "voice_note_understanding",
                    "ai_appointment_booking",
                    "webhooks",
                    "api_access",
                    "campaign_context_100k"
                  ]
                }
      responses:
        '201':
          description: Sub-account created successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  data:
                    type: object
                    properties:
                      email:
                        type: string
                        format: email
                        description: Sub-account email
                      first_name:
                        type: string
                        description: First name
                      last_name:
                        type: string
                        description: Last name
                      role:
                        type: string
                        examples: ["Sub Account"]
                        description: User role
                      is_agency_sub_account:
                        type: boolean
                        examples: [true]
                        description: Indicates this is a sub-account
                      uid:
                        type: string
                        description: Firebase UID for the sub-account
                      created_time:
                        type: string
                        format: date-time
                        description: Account creation timestamp
                      temporary_password:
                        type: string
                        description: Temporary password for the sub-account user
                      usage_limits:
                        type: object
                        properties:
                          monthly_credits:
                            type: integer
                            description: Total monthly credits allocated
                          monthly_credits_used:
                            type: integer
                            description: Number of monthly credits used
                          last_reset_date:
                            type: string
                            format: date-time
                            description: Date when credits were last reset
                          credits:
                            type: integer
                            description: Current credit balance
                          roll_over_to_next_month:
                            type: boolean
                            description: Whether unused credits roll over to next month
              example:
                success: true
                data:
                  email: subaccount@example.com
                  first_name: John
                  last_name: Doe
                  role: Sub Account
                  is_agency_sub_account: true
                  uid: subacct_abc123def456
                  created_time: '2026-04-29T10:15:00Z'
                  temporary_password: Tmp!9xQ2vL
                  usage_limits:
                    monthly_credits: 1000
                    monthly_credits_used: 0
                    last_reset_date: '2026-04-29T10:15:00Z'
                    credits: 1000
                    roll_over_to_next_month: false
        '400':
          description: Invalid input or not an agency account
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error_code:
                    type: integer
                    enum: [400]
                  error:
                    type: string
                    enum:
                      - "Missing required fields including usage limits"
                      - "Monthly credits must be greater than 0"
                      - "You are not an agency account"
              example:
                success: false
                error_code: 400
                error: Missing required fields including usage limits
        '500':
          description: Server error
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error_code:
                    type: integer
                    enum: [500]
                  error:
                    type: string
                    examples: ["An error occurred while creating the agency account"]
    get:
      summary: List or Get SubAccounts
      description: |
        Retrieve sub-accounts under your agency. Only available for agency and developer accounts.

        - **List mode** (default): omit the `email` query parameter to receive an array of every non-deleted sub-account under your agency.
        - **Single mode**: pass `email` to fetch one specific sub-account.

        The response `data` field is an **array** in list mode and a **single object** in single mode.
      tags:
        - SubAccounts
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: email
          in: query
          required: false
          schema:
            type: string
            format: email
          description: When provided, returns the single matching sub-account. When omitted, returns the full list of sub-accounts under your agency.
      responses:
        '200':
          description: Sub-account(s) returned. `data` is a single object when `email` is provided, otherwise an array.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  data:
                    oneOf:
                      - $ref: '#/components/schemas/SubAccount'
                      - type: array
                        items:
                          $ref: '#/components/schemas/SubAccount'
              examples:
                single:
                  summary: Single sub-account (email query provided)
                  value:
                    success: true
                    data:
                      user_id: subacct_abc123def456
                      email: subaccount@example.com
                      first_name: John
                      last_name: Doe
                      business_name: Sample Business
                      country: US
                      language: en
                      is_team_member: false
                      usage_limits:
                        monthly_credits: 1000
                        monthly_credits_used: 142
                        last_reset_date: '2026-04-01T00:00:00Z'
                        credits: 858
                        roll_over_to_next_month: false
                list:
                  summary: All sub-accounts (no email query)
                  value:
                    success: true
                    data:
                      - user_id: subacct_abc123def456
                        email: subaccount@example.com
                        first_name: John
                        last_name: Doe
                        business_name: Sample Business
                        country: US
                        language: en
                        usage_limits:
                          monthly_credits: 1000
                          monthly_credits_used: 142
                          credits: 858
                          roll_over_to_next_month: false
                      - user_id: subacct_xyz789
                        email: other@example.com
                        first_name: Jane
                        last_name: Smith
                        business_name: Another Business
                        country: NL
                        language: en
                        usage_limits:
                          monthly_credits: 500
                          monthly_credits_used: 12
                          credits: 488
                          roll_over_to_next_month: true
        '400':
          description: Invalid request or not an agency account
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error_code:
                    type: integer
                    enum: [400]
                  error:
                    type: string
                    examples: ["You are not an agency account"]
        '404':
          description: Sub-account not found
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error_code:
                    type: integer
                    enum: [404]
                  error:
                    type: string
                    examples: ["Agency Sub Account not found"]
        '500':
          description: Server error
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error_code:
                    type: integer
                    enum: [500]
                  error:
                    type: string

    delete:
      summary: Delete SubAccount
      description: Delete a specific sub-account. Only available for agency and developer accounts.
      tags:
        - SubAccounts
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: email
          in: query
          required: true
          schema:
            type: string
            format: email
          description: Email of the sub-account to delete
      responses:
        '200':
          description: Sub-account deleted successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  message:
                    type: string
                    examples: ["Sub-account deleted successfully"]
              example:
                success: true
                message: Sub-account deleted successfully
        '400':
          description: Invalid request or not an agency account
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error_code:
                    type: integer
                    enum: [400]
                  error:
                    type: string
                    examples: ["You are not an agency account"]
        '404':
          description: Sub-account not found
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error_code:
                    type: integer
                    enum: [404]
                  error:
                    type: string
                    examples: ["Agency Sub Account not found"]
        '500':
          description: Server error
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error_code:
                    type: integer
                    enum: [500]
                  error:
                    type: string
                    examples: ["Error occurred while deleting sub-account"]

  /billing/switch-plan:
    post:
      summary: Switch Subscription Plan
      description: |
        Move your EXISTING subscription onto a different DM Champ plan, in place.

        Use this instead of starting a new checkout when you already pay for a plan:
        starting a checkout would leave you with two live subscriptions and two charges.
        This updates the plan item on your current subscription, so you keep one
        subscription and one renewal date, and Stripe prorates the difference
        automatically (you are credited when you move down, charged pro-rata when you
        move up).

        **Which prices are accepted**: only the current DM Champ plan prices
        (Business, Agency and Agency Unlimited, monthly or annual). Any other price id
        is rejected with 400.

        **Who can call it**: the account owner, or a team member with full billing
        permission. Not available to client sub-accounts (your agency manages your plan)
        or to lifetime/AppSumo accounts.

        **Requirements**: an existing subscription that is active, trialing, or past due.
        If the card is declined while charging the difference, the call fails with 402 and
        your plan is unchanged.
      tags:
        - Billing
      security:
        - ApiKeyAuth: []
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - priceId
              properties:
                priceId:
                  type: string
                  description: The Stripe price id of the plan to switch to (must be a current DM Champ plan price).
            examples:
              switchToAgencyAnnual:
                summary: Move to the Agency plan, billed yearly
                value:
                  priceId: price_1U4j3VAZI3FnPor0HWEEUik9
      responses:
        '200':
          description: Plan switched (or already on that plan)
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    type: object
                    properties:
                      ok:
                        type: boolean
                      productId:
                        type: string
                      priceId:
                        type: string
                      plan:
                        type: string
                        enum: [business, agency, unlimited]
                      interval:
                        type: string
                        enum: [month, year]
                      subscriptionId:
                        type: string
        '400':
          description: The price is not one of the current DM Champ plan prices
        '402':
          description: The card was declined while charging the difference
        '403':
          description: Sub-accounts cannot change their own plan
        '409':
          description: No changeable subscription (none, inactive, or a lifetime plan)
  /subaccounts/credits:
    post:
      summary: Adjust SubAccount Credits
      description: |
        Add or deduct credits from a sub-account. Only available for agency and developer accounts.

        This endpoint is the **callback target** for the webhook auto-recharge flow.
        When DM Champ fires an auto-recharge webhook to the agency's server
        (see the `WebhookAutoRechargePayload` schema and the Webhooks section below),
        the agency processes payment externally and then calls this endpoint with a
        positive `amount` to grant credits to the sub-account.

        **Usage**:
        - Positive `amount`: adds credits (e.g., auto-recharge callback, manual top-up)
        - Negative `amount`: deducts credits (cannot exceed available balance)

        **Idempotency**: Use the `idempotency_key` from the webhook payload in the
        `description` field to track which recharge request this credit adjustment fulfills.
      tags:
        - SubAccounts
      security:
        - ApiKeyAuth: []
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - email
                - amount
              properties:
                email:
                  type: string
                  format: email
                  description: Email of the sub-account to adjust credits for
                amount:
                  type: integer
                  description: Number of credits to add (positive) or deduct (negative). Must be non-zero.
                description:
                  type: string
                  description: Optional description for the credit adjustment (appears in audit trail)
            examples:
              addCredits:
                summary: Add credits (auto-recharge callback)
                value: {
                  "email": "sub@example.com",
                  "amount": 500,
                  "description": "Auto-recharge callback for idempotency key auto_recharge_abc123_1712836800000"
                }
              deductCredits:
                summary: Deduct credits
                value: {
                  "email": "sub@example.com",
                  "amount": -100,
                  "description": "Usage overage adjustment"
                }
      responses:
        '200':
          description: Credits adjusted successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  data:
                    type: object
                    properties:
                      email:
                        type: string
                        format: email
                        description: Sub-account email
                      previous_balance:
                        type: integer
                        description: Credit balance before the adjustment
                      adjustment:
                        type: integer
                        description: The amount that was applied
                      new_balance:
                        type: integer
                        description: Credit balance after the adjustment
              examples:
                success:
                  value: {
                    "success": true,
                    "data": {
                      "email": "sub@example.com",
                      "previous_balance": 42,
                      "adjustment": 500,
                      "new_balance": 542
                    }
                  }
        '400':
          description: Invalid input, insufficient credits, or not an agency account
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error_code:
                    type: integer
                    enum: [400]
                  error:
                    type: string
                    enum:
                      - "Missing required field: email"
                      - "Amount must be a non-zero number. Positive to add, negative to deduct."
                      - "You are not an agency account"
                      - "Insufficient credits. Available: 42, attempted to deduct: 100"
        '401':
          description: Invalid API key
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error_code:
                    type: integer
                    enum: [401]
                  error:
                    type: string
                    examples: ["Invalid API key. Please check your API key and try again."]
        '404':
          description: Sub-account not found under this agency
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error_code:
                    type: integer
                    enum: [404]
                  error:
                    type: string
                    examples: ["Sub-account not found with this email under your agency"]
        '500':
          description: Server error
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error_code:
                    type: integer
                    enum: [500]
                  error:
                    type: string
                    examples: ["An error occurred while adjusting credits"]

  /subaccounts/{subAccountUid}/limits:
    put:
      summary: Update SubAccount Limits
      description: |
        Update a sub-account's usage limits and credit-management mode after creation.
        All fields are optional — only the keys you send are changed.

        `usageLimits.team_seats_limit` sets the numeric team-seat cap: `0` = team
        members not included, `-1` = unlimited, any positive integer = that exact cap,
        `null` = clear the numeric cap and fall back to the `team_seats_*` feature
        presets. The cap is enforced server-side on every invite, direct add and
        invite acceptance; existing members are never removed by lowering it.

        A cap set through this endpoint is a MANUAL grant. SaaS plans can carry their
        own per-plan seat cap (set in the dashboard's plan editor): buying a plan
        applies the plan's cap, but unattended plan renewals never overwrite a cap
        you set manually here — only a plan-written cap is refreshed on renewal.
        Clearing with `null` hands the field back to the plan on its next renewal.
      tags:
        - SubAccounts
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: subAccountUid
          in: path
          required: true
          schema:
            type: string
          description: The sub-account's user id
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                usageLimits:
                  type: object
                  properties:
                    credits:
                      type: number
                      minimum: 0
                      description: Current credit balance / allowance
                    monthly_credits:
                      type: number
                      minimum: 0
                      description: Recurring monthly credit allowance
                    roll_over_to_next_month:
                      type: boolean
                      description: Whether unused monthly credits roll over
                    byok_monthly_limit_usd:
                      type: number
                      minimum: 0
                      nullable: true
                      description: Monthly BYOK USD spending limit (null = unlimited)
                    team_seats_limit:
                      type: integer
                      minimum: -1
                      nullable: true
                      description: Numeric team-seat cap (0 = not included, -1 = unlimited, null = use presets)
                    agent_limit:
                      type: integer
                      minimum: -1
                      nullable: true
                      description: Numeric AI-agent cap (0 = cannot create AI agents, -1 = unlimited, null = unmanaged)
                    channel_limit:
                      type: integer
                      minimum: -1
                      nullable: true
                      description: Numeric connected-channel cap (0 = cannot connect any channel, -1 = unlimited, null = unmanaged)
                creditManagementMode:
                  type: string
                  enum: [manual, reselling]
                  description: How the sub-account's credits are managed
      responses:
        '200':
          description: Limits updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  data:
                    type: object
                    properties:
                      subAccountUid:
                        type: string
                      updated:
                        type: array
                        items:
                          type: string
                        description: The field keys that were changed
        '400':
          description: Invalid input
        '403':
          description: Not your sub-account, or caller is not an agency
        '404':
          description: Sub-account not found

  /subaccounts/{subAccountUid}/features:
    put:
      summary: Update SubAccount Features
      description: |
        Replace the sub-account's feature list. Same feature IDs and tier-ladder
        semantics as the `features` array on **Create SubAccount** (the highest tier
        per group wins; unknown IDs are rejected with `400`).

        The three default-ON toggles (`tasks`, `daily_summaries`, `ai_media_library`)
        are mirrored change-driven: simply omitting them from `features` never turns
        them off — only removing one that was previously present does, or an explicit
        boolean in `feature_settings`.
      tags:
        - SubAccounts
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: subAccountUid
          in: path
          required: true
          schema:
            type: string
          description: The sub-account's user id
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - features
              properties:
                features:
                  type: array
                  items:
                    type: string
                  description: The full desired feature list (same IDs as Create SubAccount)
                feature_settings:
                  type: object
                  properties:
                    tasks:
                      type: boolean
                    daily_summaries:
                      type: boolean
                    ai_media_library:
                      type: boolean
                  description: Explicit on/off intent for the default-ON toggles (optional)
      responses:
        '200':
          description: Features updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  data:
                    type: object
                    properties:
                      subAccountUid:
                        type: string
                      features:
                        type: array
                        items:
                          type: string
                        description: The normalized (tier-collapsed) list that was stored
        '400':
          description: Invalid or unknown feature ID
        '403':
          description: Not your sub-account, or caller is not an agency
        '404':
          description: Sub-account not found

  /subaccounts/{subAccountUid}/ai-tiers:
    put:
      summary: Update SubAccount AI Tiers
      description: |
        Restrict which AI model tiers the sub-account is allowed to pick.

        The list REPLACES whatever was stored. Send `null` (or omit every valid tier)
        to clear the restriction so the sub-account can use any tier again.

        Valid tier IDs: `standard`, `economy`, `max`, `mini`.

        The restriction is enforced in two places, so it is not just a UI hint:

        - **On campaign save** — a campaign that selects a tier outside the list is
          rejected.
        - **At message time** — a campaign already carrying a disallowed tier (set
          before the restriction, or by a snapshot install) is downgraded to an allowed
          tier when the AI replies, so a sub-account can never keep spending on a tier
          you took away.

        Duplicate entries are removed and unknown tier IDs are rejected with `400`.
      tags:
        - SubAccounts
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: subAccountUid
          in: path
          required: true
          schema:
            type: string
          description: The sub-account's user id
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - allowed_ai_tiers
              properties:
                allowed_ai_tiers:
                  type: array
                  nullable: true
                  items:
                    type: string
                    enum: [standard, economy, max, mini]
                  description: >-
                    The tiers the sub-account may use. null clears the restriction
                    (any tier allowed). The key must be present — omitting it is a 400,
                    so an incomplete body can never silently unlock every tier.
                  examples:
                    - [economy, standard]
      responses:
        '200':
          description: Allowed AI tiers updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  data:
                    type: object
                    properties:
                      subAccountUid:
                        type: string
                      allowed_ai_tiers:
                        type: array
                        nullable: true
                        items:
                          type: string
                          enum: [standard, economy, max, mini]
                        description: The normalized list that was stored (null = no restriction)
        '400':
          description: Missing allowed_ai_tiers, or an unknown tier ID
        '403':
          description: Not your sub-account, or caller is not an agency
        '404':
          description: Sub-account not found

  /subaccounts/{subAccountUid}/locked-bot-fields:
    put:
      summary: Update SubAccount Locked Bot Fields
      description: |
        Freeze parts of the AI playbook so the sub-account cannot change them.

        The list REPLACES whatever was stored. Send `null` (or an empty list) to unlock
        everything again.

        Valid field IDs:

        - `instructions` — the main bot instructions
        - `goal` — the conversation goal
        - `rules` — the behaviour rules
        - `personality` — tone / persona
        - `conclude_unless` — the conclude conditions ("conclude the chat unless…")

        A locked field is read-only for the sub-account: their save is rejected and the
        agency's stored value is kept, so an agency-authored playbook cannot be edited
        away by the client. Unlocked fields stay fully editable, and the agency itself
        can always change any field.

        Duplicate entries are removed and unknown field IDs are rejected with `400`.
      tags:
        - SubAccounts
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: subAccountUid
          in: path
          required: true
          schema:
            type: string
          description: The sub-account's user id
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - locked_bot_fields
              properties:
                locked_bot_fields:
                  type: array
                  nullable: true
                  items:
                    type: string
                    enum: [instructions, goal, rules, personality, conclude_unless]
                  description: >-
                    The playbook fields to freeze. null (or an empty list) unlocks
                    everything. The key must be present — omitting it is a 400.
                  examples:
                    - [instructions, rules]
      responses:
        '200':
          description: Locked bot fields updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  data:
                    type: object
                    properties:
                      subAccountUid:
                        type: string
                      locked_bot_fields:
                        type: array
                        nullable: true
                        items:
                          type: string
                          enum: [instructions, goal, rules, personality, conclude_unless]
                        description: The normalized list that was stored (null = nothing locked)
        '400':
          description: Missing locked_bot_fields, or an unknown field ID
        '403':
          description: Not your sub-account, or caller is not an agency
        '404':
          description: Sub-account not found

  /subaccounts/{subAccountUid}/pause:
    post:
      summary: Pause SubAccount
      description: |
        Pause a client account. Everything the account SENDS stops immediately: no
        outbound messages, no broadcasts, no AI replies on any channel. The client can
        still sign in, but instead of the app they see a full-screen "Account paused"
        lock showing the optional `message` you pass here (only billing and
        login/logout stay reachable).

        Nothing is deleted or disconnected. Agents, campaigns, connected channels,
        contacts, chat history and settings are all preserved, so unpausing puts the
        client back exactly where they left off.

        This is the same state as the **Hard blocked** toggle in the dashboard's
        sub-account panel — it writes `agency_block.level: "hard_blocked"` — so a
        client paused here shows as blocked in the dashboard and vice versa. The
        current state is readable from the `agency_block` field on
        **List SubAccounts** (`GET /subaccounts`).

        Idempotent: pausing an already-paused client just refreshes the message,
        reason and timestamp. Use **Unpause SubAccount** to lift it.
      tags:
        - SubAccounts
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: subAccountUid
          in: path
          required: true
          schema:
            type: string
          description: The sub-account's user id
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                message:
                  type: string
                  maxLength: 500
                  nullable: true
                  description: >-
                    Shown to the client on their "Account paused" screen. Leave it out
                    for the default wording.
                  examples:
                    - Your account is on hold — please contact us to reactivate it.
                reason:
                  type: string
                  maxLength: 500
                  nullable: true
                  description: >-
                    Agency-internal note stored with the pause (and in the audit log).
                    Never shown to the client.
                  examples:
                    - Invoice 1042 unpaid
      responses:
        '200':
          description: Sub-account paused
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  data:
                    type: object
                    properties:
                      subAccountUid:
                        type: string
                      paused:
                        type: boolean
                        examples: [true]
                      level:
                        type: string
                        enum: [none, soft_blocked, hard_blocked]
                        description: The stored agency_block level (hard_blocked when paused)
        '400':
          description: Invalid input, or the target user is not one of your sub-accounts
        '403':
          description: Not your sub-account, or caller is not an agency
        '404':
          description: Sub-account not found

  /subaccounts/{subAccountUid}/unpause:
    post:
      summary: Unpause SubAccount
      description: |
        Lift the pause set by **Pause SubAccount** — clears `agency_block` back to
        `level: "none"` (the dashboard's **Hard blocked** toggle switched off).

        Sending and AI replies resume straight away and the lock screen disappears.
        Because a pause never removes anything, the client returns to exactly the
        configuration, channels, contacts and chat history they had before.

        Idempotent: unpausing a client that isn't paused changes nothing. The current
        state is readable from the `agency_block` field on **List SubAccounts**
        (`GET /subaccounts`).
      tags:
        - SubAccounts
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: subAccountUid
          in: path
          required: true
          schema:
            type: string
          description: The sub-account's user id
      responses:
        '200':
          description: Sub-account unpaused
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  data:
                    type: object
                    properties:
                      subAccountUid:
                        type: string
                      paused:
                        type: boolean
                        examples: [false]
                      level:
                        type: string
                        enum: [none, soft_blocked, hard_blocked]
                        description: The stored agency_block level (none once unpaused)
        '400':
          description: Invalid input, or the target user is not one of your sub-accounts
        '403':
          description: Not your sub-account, or caller is not an agency
        '404':
          description: Sub-account not found

  /snapshots:
    get:
      summary: List Snapshots
      description: |
        Lists your agency's snapshots, newest first.

        A **snapshot** is a frozen bundle of one or more AI Agents plus their knowledge
        (FAQs, knowledge-base sources and their files), their tools (custom functions,
        MCP servers) and their media library. You capture one from your own account in the
        dashboard (Snapshots), then install it onto client accounts.

        The list omits the captured bundles themselves (they can be large) — you get the id,
        name, agent names, per-artifact counts, and which snapshot is the current default.

        Agency accounts only.
      tags:
        - Snapshots
      security:
        - ApiKeyAuth: []
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
      responses:
        '200':
          description: Snapshots retrieved successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  data:
                    type: object
                    properties:
                      snapshots:
                        type: array
                        items:
                          type: object
                          properties:
                            id:
                              type: string
                            name:
                              type: string
                            agent_names:
                              type: array
                              items:
                                type: string
                            counts:
                              $ref: '#/components/schemas/SnapshotCounts'
                            created_at:
                              type: string
                              format: date-time
                              nullable: true
                            is_default:
                              type: boolean
                              description: Whether new sub-accounts are born with this snapshot installed
                      default_snapshot_id:
                        type: string
                        nullable: true
              examples:
                success:
                  value: {
                    "success": true,
                    "data": {
                      "snapshots": [
                        {
                          "id": "Qk3xR2mLp0aBc9dE",
                          "name": "Real-estate starter kit",
                          "agent_names": ["Listing assistant"],
                          "counts": { "agents": 1, "faqs": 34, "kbSources": 2, "customFunctions": 1, "mcpServers": 0, "mediaItems": 5 },
                          "created_at": "2026-07-28T09:12:44.000Z",
                          "is_default": true
                        }
                      ],
                      "default_snapshot_id": "Qk3xR2mLp0aBc9dE"
                    }
                  }
        '403':
          $ref: '#/components/responses/SnapshotsForbidden'
    post:
      summary: Create Snapshot
      description: |
        Captures a new snapshot from your OWN agency account: pick the agents to
        freeze and the platform copies them together with their knowledge (FAQs,
        knowledge-base sources and their files), their tools (custom functions,
        MCP servers) and their media library.

        Leave the three include flags out to capture everything, or set one to
        false to leave that part behind - useful when a template should carry the
        agent's behaviour but not, say, its media.

        The snapshot is a frozen copy: editing the agents afterwards does not
        change it. Install it with POST /snapshots/{snapshotId}/apply, or make it
        the default for new client accounts with PUT /snapshots/default.

        Agency accounts only.
      tags:
        - Snapshots
      security:
        - ApiKeyAuth: []
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - name
                - agent_ids
              properties:
                name:
                  type: string
                  description: Name for the new snapshot
                  examples: ["Real-estate starter kit"]
                agent_ids:
                  type: array
                  minItems: 1
                  description: The agents to capture, by ID
                  items:
                    type: string
                include_knowledge:
                  type: boolean
                  default: true
                  description: Capture each agent's FAQs and knowledge-base sources (files included)
                include_tools:
                  type: boolean
                  default: true
                  description: Capture each agent's custom functions and MCP servers
                include_media:
                  type: boolean
                  default: true
                  description: Capture each agent's media library
            examples:
              everything:
                summary: Capture one agent with everything it has
                value: { "name": "Real-estate starter kit", "agent_ids": ["ag7HkQ2ZpLxR3mNb"] }
              behaviourOnly:
                summary: Capture the agents without their media
                value: {
                  "name": "Behaviour only",
                  "agent_ids": ["ag7HkQ2ZpLxR3mNb"],
                  "include_media": false
                }
      responses:
        '201':
          description: Snapshot captured
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  data:
                    type: object
                    properties:
                      snapshot_id:
                        type: string
                      counts:
                        $ref: '#/components/schemas/SnapshotCounts'
                      snapshot:
                        description: The new snapshot in the same shape GET /snapshots returns, or null if it could not be re-read
                        anyOf:
                          - $ref: '#/components/schemas/SnapshotListEntry'
                          - type: 'null'
              examples:
                success:
                  value: {
                    "success": true,
                    "data": {
                      "snapshot_id": "Qk3xR2mLp0aBc9dE",
                      "counts": { "agents": 1, "faqs": 34, "kbSources": 2, "customFunctions": 1, "mcpServers": 0, "mediaItems": 5 },
                      "snapshot": {
                        "id": "Qk3xR2mLp0aBc9dE",
                        "name": "Real-estate starter kit",
                        "agent_names": ["Listing assistant"],
                        "counts": { "agents": 1, "faqs": 34, "kbSources": 2, "customFunctions": 1, "mcpServers": 0, "mediaItems": 5 },
                        "created_at": "2026-07-28T09:12:44.000Z",
                        "is_default": false
                      }
                    }
                  }
        '400':
          description: Missing name, empty or malformed agent_ids, or a non-boolean include flag
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error_code:
                    type: integer
                    enum: [400]
                  error:
                    type: string
                    examples: ["agent_ids must be a non-empty array of agent ids."]
        '403':
          $ref: '#/components/responses/SnapshotsForbidden'
        '404':
          description: One of the agent IDs does not exist on your account
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error_code:
                    type: integer
                    enum: [404]
                  error:
                    type: string

  /snapshots/default:
    get:
      summary: Get Default Snapshot
      description: |
        Returns the snapshot every NEW sub-account of your agency is born with,
        or `null` when none is set.

        A default pointing at a since-deleted snapshot reports as `null`.
      tags:
        - Snapshots
      security:
        - ApiKeyAuth: []
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
      responses:
        '200':
          description: Current default returned
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  data:
                    type: object
                    properties:
                      default_snapshot_id:
                        type: string
                        nullable: true
        '403':
          $ref: '#/components/responses/SnapshotsForbidden'
    put:
      summary: Set Default Snapshot
      description: |
        Nominates the snapshot every NEW sub-account of your agency is born with. Pass
        `null` to clear it.

        This is what makes fully automated onboarding possible: with a default set, an account
        created by **any** provisioning path — `POST /subaccounts`, the dashboard, or your own
        Stripe checkout link — arrives already carrying that snapshot's agent, knowledge base,
        tools and media instead of empty. Nothing else to call.

        Notes:
        - Installed agents start **paused**, exactly as a manual apply does, so you can wire
          channels before they go live.
        - The install runs right after provisioning and never blocks it: if it fails, the client
          account is still created and usable (it just arrives empty), and the failure is logged.
        - Only one snapshot can be the default — setting a new one replaces the previous.
      tags:
        - Snapshots
      security:
        - ApiKeyAuth: []
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - snapshot_id
              properties:
                snapshot_id:
                  type: string
                  nullable: true
                  description: The snapshot to install on every new sub-account, or null to clear.
            examples:
              set:
                summary: Set a default
                value: { "snapshot_id": "Qk3xR2mLp0aBc9dE" }
              clear:
                summary: Clear the default
                value: { "snapshot_id": null }
      responses:
        '200':
          description: Default updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  data:
                    type: object
                    properties:
                      default_snapshot_id:
                        type: string
                        nullable: true
        '400':
          description: Missing or malformed snapshot_id
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error_code:
                    type: integer
                    enum: [400]
                  error:
                    type: string
                    enum:
                      - "snapshot_id is required (pass null to clear the default)."
                      - "snapshot_id must be a string or null."
        '403':
          $ref: '#/components/responses/SnapshotsForbidden'
        '404':
          description: No such snapshot on this agency
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error_code:
                    type: integer
                    enum: [404]
                  error:
                    type: string
                    examples: ["Snapshot not found."]

  /snapshots/{snapshotId}/apply:
    post:
      summary: Apply Snapshot to a SubAccount
      description: |
        Installs a fresh copy of everything in the snapshot into one of your accounts:
        the agents, their FAQs and knowledge-base sources (files included), their custom
        functions and MCP servers, and their media library.

        Pass `sub_account_id` to target a client account; omit it to install into your own
        agency account.

        What this does **not** carry, by design — each account owns its own:
        - Channel connections and WhatsApp templates
        - Calendars, and OAuth-based integrations (the target reconnects them)

        Installed agents are created **paused** so you can connect channels and review them
        before they start replying.

        Use `PUT /snapshots/default` instead if you want this to happen automatically for
        every new client account.
      tags:
        - Snapshots
      security:
        - ApiKeyAuth: []
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: snapshotId
          in: path
          required: true
          schema:
            type: string
          description: The snapshot to install
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                sub_account_id:
                  type: string
                  description: UID of the sub-account to install into. Omit to install into your own agency account.
            examples:
              toSubAccount:
                summary: Install onto a client account
                value: { "sub_account_id": "9fQ2v1MzYhVv2S8kRq0bXn3TgH2" }
      responses:
        '200':
          description: Snapshot installed
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  data:
                    type: object
                    properties:
                      snapshot_id:
                        type: string
                      sub_account_id:
                        type: string
                      agent_ids:
                        type: array
                        items:
                          type: string
                        description: Ids of the agents created on the target account
                      counts:
                        $ref: '#/components/schemas/SnapshotCounts'
              examples:
                success:
                  value: {
                    "success": true,
                    "data": {
                      "snapshot_id": "Qk3xR2mLp0aBc9dE",
                      "sub_account_id": "9fQ2v1MzYhVv2S8kRq0bXn3TgH2",
                      "agent_ids": ["ag_7HkQ2ZpL"],
                      "counts": { "agents": 1, "faqs": 34, "kbSources": 2, "customFunctions": 1, "mcpServers": 0, "mediaItems": 5 }
                    }
                  }
        '400':
          description: Malformed target
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error_code:
                    type: integer
                    enum: [400]
                  error:
                    type: string
                    examples: ["sub_account_id must be a non-empty string."]
        '403':
          $ref: '#/components/responses/SnapshotsForbidden'
        '404':
          description: No such snapshot on this agency
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error_code:
                    type: integer
                    enum: [404]
                  error:
                    type: string
                    examples: ["Snapshot not found."]

  /team/departments:
    get:
      summary: List Departments
      description: |
        Your account's departments - the named groups your team is organised into
        (Sales, Customer support, HR). A department can own leads, can hand new
        conversations to its own members automatically, and can be used to limit what
        a team member sees.

        Every /team endpoint is signed in as a PERSON, not with an API key: send
        Authorization: Bearer <Firebase ID token>. An API key gets a 401 here,
        because these routes decide what to do based on who you are signed in as.
      tags:
        - Team
      parameters:
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
      responses:
        '200':
          description: The account's departments
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  departments:
                    type: array
                    items:
                      $ref: '#/components/schemas/Department'
    post:
      summary: Create Department
      description: |
        Creates a department. Give it a name, the members who belong to it, and
        optionally the incoming conversations it should claim: new chats on the agents
        or channels you list are filed under this department automatically and handed
        to whoever on it is least busy.

        Every /team endpoint is signed in as a PERSON, not with an API key: send
        Authorization: Bearer <Firebase ID token>. An API key gets a 401 here,
        because these routes decide what to do based on who you are signed in as.
      tags:
        - Team
      parameters:
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DepartmentInput'
            examples:
              sales:
                summary: A Sales department that claims WhatsApp
                value:
                  name: "Sales"
                  color: "#2f6fed"
                  member_uids: ["uid_alice", "uid_bob"]
                  routing_channels: ["whatsapp"]
      responses:
        '201':
          description: The department was created
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  department:
                    $ref: '#/components/schemas/Department'
        '409':
          description: A department with that name already exists
  /team/departments/{departmentId}:
    patch:
      summary: Update Department
      description: |
        Changes a department. Only the fields you send are changed; anything you leave
        out is kept as it is.

        Every /team endpoint is signed in as a PERSON, not with an API key: send
        Authorization: Bearer <Firebase ID token>. An API key gets a 401 here,
        because these routes decide what to do based on who you are signed in as.
      tags:
        - Team
      parameters:
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
        - name: departmentId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DepartmentInput'
      responses:
        '200':
          description: The department was updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  department:
                    $ref: '#/components/schemas/Department'
        '404':
          description: Department not found
    delete:
      summary: Delete Department
      description: |
        Deletes a department. Any team member who was limited to it stops being limited
        to it, so nobody is left with an empty inbox. Contacts that were filed under it
        simply stop showing a department.

        Every /team endpoint is signed in as a PERSON, not with an API key: send
        Authorization: Bearer <Firebase ID token>. An API key gets a 401 here,
        because these routes decide what to do based on who you are signed in as.
      tags:
        - Team
      parameters:
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
        - name: departmentId
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: The department was deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  deleted:
                    type: string
                  memberships_rescoped:
                    type: integer
                    description: How many team members were un-limited from this department.
        '404':
          description: Department not found
  /team/members:
    get:
      summary: List Team Members
      description: |
        Lists everyone on your team, plus how many seats your plan includes and how
        many are in use, so you can show "3 of 5 seats" and know when inviting is
        going to be refused.

        Every /team endpoint is signed in as a PERSON, not with an API key: send
        Authorization: Bearer <Firebase ID token>. An API key gets a 401 here,
        because these routes decide what to do based on who you are signed in as.
      tags:
        - Team
      responses:
        '200':
          description: The team roster
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  members:
                    type: array
                    description: One entry per team member.
                    items:
                      type: object
                      additionalProperties: true
                  seat_limit:
                    type: ["integer", "null"]
                    description: Seats your plan allows, or null for unlimited.
                  seats_used:
                    type: integer
                    description: Active members counted against the limit.
        '401':
          description: This endpoint needs a Firebase ID token, not an API key
        '403':
          description: You do not have permission to view the team
    post:
      summary: Create Team Member
      description: |
        Adds a teammate to your account directly, without sending an invite - use
        this when you want them on the team immediately. To let them accept in their
        own time instead, send an invite.

        Pick their role: admin (everything except billing-level owner actions),
        editor (create and edit) or viewer (read only). Adjust individual areas with
        permission_overrides when the role is nearly right but not quite.

        Fails when your plan's seats are already full.

        Sign in as a person: Authorization: Bearer <Firebase ID token>.
      tags:
        - Team
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TeamMemberInput'
            examples:
              editor:
                summary: Add an editor
                value:
                  email: "sam@example.com"
                  display_name: "Sam Rivera"
                  role: "editor"
              scoped_editor:
                summary: An editor who only sees their own contacts
                value:
                  email: "sam@example.com"
                  display_name: "Sam Rivera"
                  role: "editor"
                  contact_scope: "assigned"
                  contact_scope_unassigned: true
      responses:
        '201':
          description: The team member was created
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  team_member_id:
                    type: string
                  member_uid:
                    type: string
                  message:
                    type: string
        '400':
          description: Missing email, display_name or role, or an invalid role
        '401':
          description: This endpoint needs a Firebase ID token, not an API key
        '403':
          description: You do not have permission to manage the team
        '409':
          description: Your plan's team seats are full
  /team/members/{memberUid}:
    patch:
      summary: Update Team Member
      description: |
        Changes a teammate's role, their per-area permissions, which contacts they
        can see, which client sub-accounts they can reach, or whether they take part
        in automatic contact assignment. Send only what you want to change.

        Send status "active" to bring a suspended member back. It is the only status
        this endpoint accepts - to suspend someone, use the suspend endpoint.

        Sign in as a person: Authorization: Bearer <Firebase ID token>.
      tags:
        - Team
      parameters:
        - name: memberUid
          in: path
          required: true
          schema:
            type: string
          description: UID of the team member to update.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                role:
                  type: string
                  enum: ['admin', 'editor', 'viewer']
                permission_overrides:
                  type: array
                  items:
                    $ref: '#/components/schemas/TeamPermissionOverride'
                status:
                  type: string
                  enum: ['active']
                  description: Only "active" is accepted, to reactivate a suspended member.
                auto_assign_enabled:
                  type: boolean
                  description: Whether new contacts can be auto-assigned to this member.
                contact_scope:
                  type: string
                  enum: ['all', 'assigned']
                contact_scope_unassigned:
                  type: boolean
                sub_account_access:
                  $ref: '#/components/schemas/TeamSubAccountAccess'
      responses:
        '200':
          description: The team member was updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  message:
                    type: string
        '400':
          description: Invalid role, status or scope value
        '401':
          description: This endpoint needs a Firebase ID token, not an API key
        '403':
          description: You do not have permission to manage the team
        '404':
          description: No such team member
    delete:
      summary: Remove Team Member
      description: |
        Removes someone from your team and frees their seat. They immediately lose
        access to your account; their own login is not deleted.

        Sign in as a person: Authorization: Bearer <Firebase ID token>.
      tags:
        - Team
      parameters:
        - name: memberUid
          in: path
          required: true
          schema:
            type: string
          description: UID of the team member to remove.
      responses:
        '200':
          description: The team member was removed
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  message:
                    type: string
        '401':
          description: This endpoint needs a Firebase ID token, not an API key
        '403':
          description: You do not have permission to manage the team
        '404':
          description: No such team member
  /team/members/{memberUid}/suspend:
    post:
      summary: Suspend Team Member
      description: |
        Suspends a teammate: they keep their place on the team but lose access until
        you reactivate them by updating them with status "active". Use this instead
        of removing someone when the pause is temporary.

        Sign in as a person: Authorization: Bearer <Firebase ID token>.
      tags:
        - Team
      parameters:
        - name: memberUid
          in: path
          required: true
          schema:
            type: string
          description: UID of the team member to suspend.
      responses:
        '200':
          description: The team member was suspended
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  message:
                    type: string
        '401':
          description: This endpoint needs a Firebase ID token, not an API key
        '403':
          description: You do not have permission to manage the team
        '404':
          description: No such team member
  /team/invites:
    get:
      summary: List Team Invites
      description: |
        Lists the invitations you have sent, so you can see who has not accepted yet.
        Filter by status to see only the pending ones.

        Sign in as a person: Authorization: Bearer <Firebase ID token>.
      tags:
        - Team
      parameters:
        - name: status
          in: query
          required: false
          schema:
            type: string
          description: Only return invites in this state, for example pending or accepted.
      responses:
        '200':
          description: The invites you have sent
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  invites:
                    type: array
                    items:
                      type: object
                      additionalProperties: true
        '401':
          description: This endpoint needs a Firebase ID token, not an API key
        '403':
          description: You do not have permission to view the team
    post:
      summary: Invite Team Member
      description: |
        Emails someone an invitation to join your team. This is the usual way to add
        a teammate: they click the link, sign in as themselves, and accept. The
        invite expires after 7 days.

        Set their role and, if you want, their permissions and contact visibility up
        front - those are copied onto their membership the moment they accept, so you
        do not have to edit them afterwards.

        Fails when your plan's seats are already full.

        Sign in as a person: Authorization: Bearer <Firebase ID token>.
      tags:
        - Team
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TeamInviteInput'
            examples:
              invite_editor:
                value:
                  email: "sam@example.com"
                  role: "editor"
      responses:
        '201':
          description: The invitation was sent
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  invite_id:
                    type: string
                  message:
                    type: string
        '400':
          description: Missing email or an invalid role
        '401':
          description: This endpoint needs a Firebase ID token, not an API key
        '403':
          description: You do not have permission to manage the team
        '409':
          description: Your plan's team seats are full, or that person is already invited
  /team/invites/accept:
    post:
      summary: Accept Team Invite
      description: |
        Accepts an invitation using the token from the invite email, joining you to
        that account's team.

        This is an act of your OWN identity: sign in as yourself with
        Authorization: Bearer <Firebase ID token>. It is deliberately refused while
        you are working inside somebody else's account in team mode.
      tags:
        - Team
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - invite_token
              properties:
                invite_token:
                  type: string
                  description: The token from the invitation email link.
      responses:
        '200':
          description: You joined the team
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  team_member_id:
                    type: string
                  account_owner_uid:
                    type: string
                    description: The account you now have access to.
                  message:
                    type: string
        '400':
          description: invite_token missing
        '401':
          description: This endpoint needs a Firebase ID token, not an API key
        '403':
          description: Refused because the session is impersonating another account
        '404':
          description: The invitation does not exist, has expired, or was already used
  /team/invites/decline:
    post:
      summary: Decline Team Invite
      description: |
        Declines an invitation using the token from the invite email.

        Like accepting, this is an act of your own identity: sign in as yourself with
        Authorization: Bearer <Firebase ID token>, and it is refused while you are
        working inside somebody else's account in team mode.
      tags:
        - Team
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - invite_token
              properties:
                invite_token:
                  type: string
                  description: The token from the invitation email link.
      responses:
        '200':
          description: The invitation was declined
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  message:
                    type: string
        '400':
          description: invite_token missing
        '401':
          description: This endpoint needs a Firebase ID token, not an API key
        '403':
          description: Refused because the session is impersonating another account
        '404':
          description: The invitation does not exist, has expired, or was already used
  /team/invites/{inviteId}:
    delete:
      summary: Cancel Team Invite
      description: |
        Withdraws an invitation you sent before it was accepted. The link in the
        email stops working.

        Sign in as a person: Authorization: Bearer <Firebase ID token>.
      tags:
        - Team
      parameters:
        - name: inviteId
          in: path
          required: true
          schema:
            type: string
          description: ID of the invite to cancel (from the invite list).
      responses:
        '200':
          description: The invitation was cancelled
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  message:
                    type: string
        '401':
          description: This endpoint needs a Firebase ID token, not an API key
        '403':
          description: You do not have permission to manage the team
        '404':
          description: No such invite
  /team/invites/{inviteId}/resend:
    post:
      summary: Resend Team Invite
      description: |
        Sends the invitation email again - for when it was missed or went to spam.

        Sign in as a person: Authorization: Bearer <Firebase ID token>.
      tags:
        - Team
      parameters:
        - name: inviteId
          in: path
          required: true
          schema:
            type: string
          description: ID of the invite to resend.
      responses:
        '200':
          description: The invitation email was sent again
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  message:
                    type: string
        '401':
          description: This endpoint needs a Firebase ID token, not an API key
        '403':
          description: You do not have permission to manage the team
        '404':
          description: No such invite
  /team/permissions:
    get:
      summary: Get My Team Permissions
      description: |
        Returns what YOU are allowed to do in the account you are currently working
        in - your role and your level for each area (none, view, edit or full). Use
        it to hide buttons a member cannot use instead of letting them discover the
        limit through an error.

        Sign in as a person: Authorization: Bearer <Firebase ID token>.
      tags:
        - Team
      responses:
        '200':
          description: Your effective permissions
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  permissions:
                    type: object
                    description: Your level for each area.
                    additionalProperties: true
                additionalProperties: true
        '401':
          description: This endpoint needs a Firebase ID token, not an API key
  /team/users/{targetUid}/role:
    post:
      summary: Assign Platform Role
      description: |
        Sets a user's PLATFORM role - User, Agency, Support or Dev. This is not team
        membership: it is what kind of DM Champ account somebody has.

        Restricted to DM Champ staff (Dev role), and the last remaining Dev cannot be
        demoted. Refused while impersonating another account.

        Sign in as a person: Authorization: Bearer <Firebase ID token>.
      tags:
        - Team
      parameters:
        - name: targetUid
          in: path
          required: true
          schema:
            type: string
          description: UID of the user whose platform role to set.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - role
              properties:
                role:
                  type: string
                  enum: ['User', 'Dev', 'Support', 'Agency']
      responses:
        '200':
          description: The role was assigned
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  targetUid:
                    type: string
                  role:
                    type: string
                  claimUpdated:
                    type: boolean
        '400':
          description: Missing or invalid role, or this would remove the last Dev
        '401':
          description: This endpoint needs a Firebase ID token, not an API key
        '403':
          description: Only DM Champ staff can assign platform roles
        '404':
          description: No such user
  /team/tokens/team-member:
    post:
      summary: Create Team Mode Session Token
      description: |
        Mints a sign-in token that lets a team member start working INSIDE an account
        they belong to. Sign in with the returned token to switch into that account;
        come back out with the return-from-team token.

        Sign in as a person: Authorization: Bearer <Firebase ID token>.
      tags:
        - Team
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - account_owner_uid
              properties:
                account_owner_uid:
                  type: string
                  description: UID of the account to work inside.
      responses:
        '200':
          description: The session token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TeamSessionToken'
        '400':
          description: account_owner_uid missing
        '401':
          description: This endpoint needs a Firebase ID token, not an API key
        '403':
          description: You are not a member of that account
  /team/tokens/return-from-team:
    post:
      summary: Create Return From Team Session Token
      description: |
        Mints a sign-in token that takes a team member back out of the account they
        were working inside, to their own account.

        Sign in as a person: Authorization: Bearer <Firebase ID token>.
      tags:
        - Team
      responses:
        '200':
          description: The session token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TeamSessionToken'
        '401':
          description: This endpoint needs a Firebase ID token, not an API key
        '403':
          description: The current session is not inside another account
  /team/tokens/return-to-admin:
    post:
      summary: Create Return To Admin Session Token
      description: |
        Mints a sign-in token that ends an assist session and returns DM Champ staff
        to their own admin account.

        Sign in as a person: Authorization: Bearer <Firebase ID token>.
      tags:
        - Team
      responses:
        '200':
          description: The session token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TeamSessionToken'
        '401':
          description: This endpoint needs a Firebase ID token, not an API key
        '403':
          description: The current session is not an assist session
  /team/tokens/assist:
    post:
      summary: Create Assist Session Token
      description: |
        Mints a sign-in token that lets DM Champ staff open a customer's account to
        help them. Staff only, and deliberately refused to team members working
        inside a staff account.

        Sign in as a person: Authorization: Bearer <Firebase ID token>.
      tags:
        - Team
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                targetUid:
                  type: string
                  description: UID of the account to assist.
              additionalProperties: true
      responses:
        '200':
          description: The session token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TeamSessionToken'
        '401':
          description: This endpoint needs a Firebase ID token, not an API key
        '403':
          description: Not DM Champ staff, or refused because the session is impersonating another account
  /team/tokens/agency-assist:
    post:
      summary: Create Agency Assist Session Token
      description: |
        Mints a sign-in token that lets an agency open one of its client
        sub-accounts, and (called without a sub-account) take the agency back out
        again.

        Only sub-accounts belonging to your agency can be opened. A team member of
        the agency can do this only when they have team management permission and
        have been granted that client.

        Sign in as a person: Authorization: Bearer <Firebase ID token>.
      tags:
        - Team
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                subAccountUid:
                  type: string
                  description: >-
                    UID of the client sub-account to open. Omit it to return to the
                    agency account.
      responses:
        '200':
          description: The session token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TeamSessionToken'
        '401':
          description: This endpoint needs a Firebase ID token, not an API key
        '403':
          description: That sub-account is not on your agency, or you have not been granted it
        '404':
          description: No such sub-account
  /users/lists:
    get:
      summary: Get User Lists
      description: Retrieve all lists associated with the authenticated user
      tags:
        - Users
        - Lists
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
      responses:
        '200':
          description: Lists retrieved successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  lists:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: string
                          description: Unique identifier for the list
                          examples: ["list123"]
                        name:
                          type: string
                          description: Name of the list
                          examples: ["My Contacts"]
              example:
                success: true
                lists:
                  - id: list_abc123
                    name: VIP Customers
                  - id: list_def456
                    name: Trial Signups
                  - id: list_ghi789
                    name: Newsletter Subscribers
        '400':
          description: Missing API key
          content:
            application/json:
              schema:
                type: string
                examples: ["Missing required parameter: apiKey"]
              example: 'Missing required parameter: apiKey'
        '401':
          description: Invalid API key
          content:
            application/json:
              schema:
                type: string
                examples: ["Invalid API key"]
        '500':
          description: Server error
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  message:
                    type: string
                    examples: ["Error occurred while retrieving user lists."]
              example:
                success: false
                message: Error occurred while retrieving user lists.

  /lists:
    post:
      summary: Create List
      security:
        - ApiKeyAuth: []
      description: |
        Create a contact list.

        A list is either STATIC (you decide who is in it) or SMART (membership is computed from
        rules and kept up to date for you). Send type "smart" together with smart_rules to create a
        smart list; the rules are evaluated immediately and the response tells you how many contacts
        matched. A smart list cannot be given hand-picked members.
      tags:
        - Lists
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ListCreateRequest'
            examples:
              static_list:
                summary: A plain list with three contacts in it
                value:
                  name: "VIP Customers"
                  contact_ids: ["contact_abc123", "contact_def456", "contact_ghi789"]
              smart_list:
                summary: A smart list of everyone tagged vip who replied in the last 30 days
                value:
                  name: "Active VIPs"
                  type: "smart"
                  smart_rules:
                    match: "all"
                    conditions:
                      - field: "tags"
                        op: "has_any"
                        value: ["tag_vip"]
                      - field: "last_incoming_message_at"
                        op: "within_last"
                        value:
                          amount: 30
                          unit: "days"
      responses:
        '201':
          description: List created
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  list_id:
                    type: string
                    examples: ["list_abc123"]
                  evaluation:
                    oneOf:
                      - $ref: '#/components/schemas/SmartListEvaluation'
                      - type: 'null'
                    description: Result of the first rules evaluation. null for a static list.
        '400':
          description: Missing name, an invalid status, or invalid smart-list rules
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["Missing required field: name"]
        '409':
          description: Hand-picked members were supplied for a smart list
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["This is a smart list - its members are computed from its rules."]
    delete:
      summary: Delete Lists
      security:
        - ApiKeyAuth: []
      description: |
        Delete one or more lists in a single call. Lists that do not exist, or that belong to another
        account, are skipped silently - the response reports how many were actually deleted. Deleting
        a list does not delete the contacts in it.
      tags:
        - Lists
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - listIds
              properties:
                listIds:
                  type: array
                  description: IDs of the lists to delete. At least 1, at most 1000.
                  minItems: 1
                  maxItems: 1000
                  items:
                    type: string
                  examples: [["list_abc123", "list_def456"]]
            examples:
              delete_two:
                summary: Delete two lists
                value:
                  listIds: ["list_abc123", "list_def456"]
      responses:
        '200':
          description: Deletion completed
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  deleted:
                    type: integer
                    description: How many lists were actually deleted.
                    examples: [2]
        '400':
          description: listIds missing, empty, not strings, or longer than 1000 entries
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["listIds must be a non-empty array of list ID strings"]

  /lists/{listId}:
    put:
      summary: Update List
      security:
        - ApiKeyAuth: []
      description: |
        Change a list. Only the fields you send are changed.

        You can also convert between kinds here: send type "smart" with smart_rules to turn a static
        list into a smart one, or type "static" to freeze it (its rules are cleared and its current
        members stay). Whenever the rules surface is touched the list is re-evaluated immediately and
        the result comes back in evaluation.
      tags:
        - Lists
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
        - name: listId
          in: path
          required: true
          description: ID of the list to update.
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ListUpdateRequest'
            examples:
              rename:
                summary: Rename a list
                value:
                  name: "VIP Customers 2026"
              make_smart:
                summary: Turn a static list into a smart one
                value:
                  type: "smart"
                  smart_rules:
                    match: "any"
                    conditions:
                      - field: "channel"
                        op: "is_any"
                        value: ["whatsapp"]
      responses:
        '200':
          description: List updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  list_id:
                    type: string
                    examples: ["list_abc123"]
                  evaluation:
                    oneOf:
                      - $ref: '#/components/schemas/SmartListEvaluation'
                      - type: 'null'
                    description: Result of the re-evaluation. null when the change did not touch the rules, or the list is static.
        '400':
          description: Empty body, an invalid status, or invalid smart-list rules
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["No fields to update"]
        '404':
          description: No list with this ID exists in your account
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["List not found"]
        '409':
          description: Hand-picked members were supplied for a smart list
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["This is a smart list - its members are computed from its rules."]

  /lists/preview:
    post:
      summary: Preview Smart List Rules
      security:
        - ApiKeyAuth: []
      description: |
        Count and sample the contacts a set of smart-list rules would match, without creating or
        changing anything. Use it to check a rule set before you save it.
      tags:
        - Lists
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - smart_rules
              properties:
                smart_rules:
                  $ref: '#/components/schemas/SmartListRules'
            examples:
              preview_vips:
                summary: How many contacts are tagged vip
                value:
                  smart_rules:
                    match: "all"
                    conditions:
                      - field: "tags"
                        op: "has_any"
                        value: ["tag_vip"]
      responses:
        '200':
          description: Preview computed
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  count:
                    type: integer
                    description: How many contacts match the rules right now.
                    examples: [143]
                  sample:
                    type: array
                    description: A small sample of the matching contacts, for showing who would be in the list.
                    items:
                      type: object
                      properties:
                        id:
                          type: string
                          examples: ["contact_abc123"]
                        first_name:
                          type: ['string', 'null']
                          examples: ["Ada"]
                        last_name:
                          type: ['string', 'null']
                          examples: ["Lovelace"]
                        phone_number:
                          type: ['string', 'null']
                          examples: ["+31612345678"]
                        email:
                          type: ['string', 'null']
                          examples: ["ada@example.com"]
                        channel:
                          type: ['string', 'null']
                          examples: ["whatsapp"]
        '400':
          description: The rules are missing or invalid
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["A smart list needs at least one rule. Add a condition and try again."]

  /lists/{listId}/evaluate:
    post:
      summary: Evaluate Smart List
      security:
        - ApiKeyAuth: []
      description: |
        Re-run a smart list's rules now and update its membership - the "refresh now" action. Smart
        lists are also refreshed automatically in the background, so this is only needed when you
        want the change immediately.
      tags:
        - Lists
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
        - name: listId
          in: path
          required: true
          description: ID of the smart list to re-evaluate.
          schema:
            type: string
      responses:
        '200':
          description: Evaluation finished
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  list_id:
                    type: string
                    examples: ["list_abc123"]
                  evaluation:
                    $ref: '#/components/schemas/SmartListEvaluation'
        '404':
          description: No list with this ID exists in your account
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["List not found"]
        '409':
          description: The list is static, so it has no rules to evaluate
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["This list is not a smart list - only smart lists are evaluated from rules."]

  /lists/{listId}/campaigns:
    post:
      summary: Link Campaign to List
      security:
        - ApiKeyAuth: []
      description: |
        Legacy campaign linkage. Records a campaign on the list's campaigns field. Both the list and
        the campaign must belong to your account. Linking a campaign that is already linked does
        nothing and still succeeds.
      tags:
        - Lists
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
        - name: listId
          in: path
          required: true
          description: ID of the list.
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - campaign_id
              properties:
                campaign_id:
                  type: string
                  description: ID of the campaign to link.
                  examples: ["campaign123"]
            examples:
              link:
                summary: Link a campaign
                value:
                  campaign_id: "campaign123"
      responses:
        '200':
          description: Campaign linked
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  list_id:
                    type: string
                    examples: ["list_abc123"]
                  campaign_id:
                    type: string
                    examples: ["campaign123"]
        '400':
          description: campaign_id is missing or empty
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["Missing required field: campaign_id"]
        '404':
          description: The list or the campaign does not exist in your account
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["List not found"]

  /lists/{listId}/campaigns/{campaignId}:
    delete:
      summary: Unlink Campaign from List
      security:
        - ApiKeyAuth: []
      description: |
        Legacy campaign linkage. Removes a campaign from the list's campaigns field. Unlinking a
        campaign that is not linked does nothing and still succeeds.
      tags:
        - Lists
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
        - name: listId
          in: path
          required: true
          description: ID of the list.
          schema:
            type: string
        - name: campaignId
          in: path
          required: true
          description: ID of the campaign to unlink.
          schema:
            type: string
      responses:
        '200':
          description: Campaign unlinked
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  list_id:
                    type: string
                    examples: ["list_abc123"]
                  campaign_id:
                    type: string
                    examples: ["campaign123"]
        '404':
          description: No list with this ID exists in your account
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["List not found"]

  /chat-sessions/recent:
    get:
      summary: Get Recent Chat Sessions
      description: |
        Retrieve chat sessions from the past X hours across all contacts belonging to the authenticated user.
        Supports optional filtering by session status and message inclusion.
      tags:
        - Chat Sessions
      security:
        - ApiKeyAuth: []
        - UserIdAuth: []
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
        - $ref: '#/components/parameters/UserIdParam'
        - name: hours
          in: query
          required: true
          schema:
            type: integer
            minimum: 1
          description: Number of hours to look back for recent sessions
          example: 24
        - name: status
          in: query
          required: false
          schema:
            type: string
            enum: [ChatSessionOpened, ChatSessionClosed]
          description: Filter sessions by status
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            default: 100
            minimum: 1
          description: Maximum number of sessions to return
        - name: includeMessages
          in: query
          required: false
          schema:
            type: boolean
            default: false
          description: Whether to include messages in each session
      responses:
        '200':
          description: Recent chat sessions retrieved successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  data:
                    type: object
                    properties:
                      hours_ago:
                        type: integer
                        description: Number of hours looked back
                        examples: [24]
                      total_sessions:
                        type: integer
                        description: Total number of sessions found
                        examples: [15]
                      sessions:
                        type: array
                        items:
                          $ref: '#/components/schemas/RecentChatSession'
              example:
                success: true
                data:
                  hours_ago: 24
                  total_sessions: 2
                  sessions:
                    - session_id: session_01H8X9Y2Z3
                      contact_id: contact_abc123
                      contact_name: Jane Doe
                      contact_phone: '+15551234567'
                      contact_email: jane@example.com
                      start_date_time: '2026-04-29T08:15:00Z'
                      end_date_time: '2026-04-29T08:42:30Z'
                      status: ChatSessionClosed
                      tag: support
                    - session_id: session_01H8X9Y2Z4
                      contact_id: contact_def456
                      contact_name: John Smith
                      contact_phone: '+15557654321'
                      contact_email: john@example.com
                      start_date_time: '2026-04-29T09:05:11Z'
                      end_date_time: '2026-04-29T09:12:48Z'
                      status: ChatSessionOpened
                      tag: null
        '400':
          description: Missing or invalid hours parameter
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error_code:
                    type: integer
                    examples: [400]
                  error:
                    type: string
                    examples: ["Missing required parameter: hours"]
              example:
                success: false
                error_code: 400
                error: 'Missing required parameter: hours'
        '401':
          description: Authentication required or invalid credentials
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error_code:
                    type: integer
                    examples: [401]
                  error:
                    type: string
                    examples: ["Invalid API key"]
        '500':
          description: Internal server error
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error_code:
                    type: integer
                    examples: [500]
                  error:
                    type: string

  /chat-sessions/{contactId}:
    get:
      summary: Get Chat Sessions for Contact
      description: |
        Retrieve all chat sessions for a specific contact. The contact must belong to the authenticated user.
      tags:
        - Chat Sessions
      security:
        - ApiKeyAuth: []
        - UserIdAuth: []
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
        - $ref: '#/components/parameters/UserIdParam'
        - name: contactId
          in: path
          required: true
          schema:
            type: string
          description: Firestore document ID of the contact
          example: "abc123xyz"
        - name: status
          in: query
          required: false
          schema:
            type: string
            enum: [ChatSessionOpened, ChatSessionClosed]
          description: Filter sessions by status
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            default: 100
            minimum: 1
          description: Maximum number of sessions to return
        - name: includeMessages
          in: query
          required: false
          schema:
            type: boolean
            default: false
          description: Whether to include messages in each session
      responses:
        '200':
          description: Chat sessions retrieved successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  data:
                    type: object
                    properties:
                      contact_id:
                        type: string
                        description: Contact identifier
                        examples: ["abc123xyz"]
                      contact_name:
                        type: string
                        description: Contact full name
                        examples: ["John Doe"]
                      total_sessions:
                        type: integer
                        description: Total number of sessions found
                        examples: [5]
                      sessions:
                        type: array
                        items:
                          $ref: '#/components/schemas/ChatSession'
              example:
                success: true
                data:
                  contact_id: contact_abc123
                  contact_name: Jane Doe
                  total_sessions: 2
                  sessions:
                    - session_id: session_01H8X9Y2Z3
                      start_date_time: '2026-04-29T08:15:00Z'
                      end_date_time: '2026-04-29T08:42:30Z'
                      status: ChatSessionClosed
                      tag: support
                    - session_id: session_01H8X9Y2Z4
                      start_date_time: '2026-04-29T09:05:11Z'
                      end_date_time: null
                      status: ChatSessionOpened
                      tag: null
        '400':
          description: Missing required parameters
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error_code:
                    type: integer
                    examples: [400]
                  error:
                    type: string
                    examples: ["Missing required parameter: contactId"]
              example:
                success: false
                error_code: 400
                error: 'Missing required parameter: contactId'
        '401':
          description: Authentication required or invalid credentials
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error_code:
                    type: integer
                    examples: [401]
                  error:
                    type: string
                    examples: ["Invalid API key"]
        '403':
          description: Contact does not belong to authenticated user
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error_code:
                    type: integer
                    examples: [403]
                  error:
                    type: string
                    examples: ["You don't have permission to access this contact's chat sessions"]
              example:
                success: false
                error_code: 403
                error: You don't have permission to access this contact's chat sessions
        '404':
          description: Contact not found
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error_code:
                    type: integer
                    examples: [404]
                  error:
                    type: string
                    examples: ["Contact not found"]
              example:
                success: false
                error_code: 404
                error: Contact not found

  /chat-exports/recent:
    get:
      summary: Get Recent Chat Exports
      description: |
        Export chat history for all contacts with recent activity within the specified number of hours.
        Supports JSON (default) and plain text file formats.

        Text format returns a downloadable .txt file attachment with all conversations combined.
        JSON format returns structured data with per-contact message exports.
      tags:
        - Chat Exports
      security:
        - ApiKeyAuth: []
        - UserIdAuth: []
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
        - $ref: '#/components/parameters/UserIdParam'
        - name: hours
          in: query
          required: true
          schema:
            type: integer
            minimum: 1
          description: Number of hours to look back for recent contact activity
          example: 24
        - name: format
          in: query
          required: false
          schema:
            type: string
            enum: [json, txt]
            default: json
          description: Output format. Use 'txt' to download a plain text file attachment.
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            default: 50
            minimum: 1
          description: Maximum number of contacts to export
        - name: filter
          in: query
          required: false
          schema:
            type: string
            enum: [all, media, tool_use, text]
            default: all
          description: |
            Filter messages by type:
            - all: Include all messages
            - media: Only messages with media attachments
            - tool_use: Only tool use / AI action messages
            - text: Only plain text messages (no media, no tool use)
      responses:
        '200':
          description: |
            Chat exports retrieved successfully.
            When format=txt, returns a downloadable text file.
            When format=json (default), returns structured JSON.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  data:
                    type: object
                    properties:
                      hours_ago:
                        type: integer
                        description: Number of hours looked back
                        examples: [24]
                      total_contacts:
                        type: integer
                        description: Number of contacts in the export
                        examples: [10]
                      exports:
                        type: array
                        items:
                          $ref: '#/components/schemas/ChatExport'
              example:
                success: true
                data:
                  hours_ago: 24
                  total_contacts: 2
                  exports:
                    - contactId: contact_abc123
                      contactName: Jane Doe
                      phoneNumber: '+15551234567'
                      email: jane@example.com
                      messageCount: 12
                      chatExport: "[2026-04-29 08:15] Jane Doe: Hi, I have a question about my order\n[2026-04-29 08:16] Agent: Sure, what's your order number?\n..."
                    - contactId: contact_def456
                      contactName: John Smith
                      phoneNumber: '+15557654321'
                      email: john@example.com
                      messageCount: 4
                      chatExport: "[2026-04-29 09:05] John Smith: Hello\n[2026-04-29 09:05] Agent: Hi John, how can I help?\n..."
            text/plain:
              schema:
                type: string
                description: Combined chat export as plain text file (when format=txt)
              example: |
                === Jane Doe (+15551234567) ===
                [2026-04-29 08:15] Jane Doe: Hi, I have a question about my order
                [2026-04-29 08:16] Agent: Sure, what's your order number?

                === John Smith (+15557654321) ===
                [2026-04-29 09:05] John Smith: Hello
                [2026-04-29 09:05] Agent: Hi John, how can I help?
        '400':
          description: Missing or invalid hours parameter
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error_code:
                    type: integer
                    examples: [400]
                  error:
                    type: string
                    examples: ["Missing required parameter: hours"]
              example:
                success: false
                error_code: 400
                error: 'Missing required parameter: hours'
        '401':
          description: Authentication required or invalid credentials
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error_code:
                    type: integer
                    examples: [401]
                  error:
                    type: string
                    examples: ["Invalid API key"]

  /agencies/usage/export:
    get:
      summary: Export Agency Usage Report
      description: |
        Export credit usage data for all sub-accounts as a CSV file.

        **Requires Agency or Dev role.** This endpoint only accepts apiKey authentication (userId is not supported).

        Returns a downloadable CSV file with columns: Sub Account ID, Sub Account Email, Sub Account Name,
        Date, Amount, Reason, Campaign Name, Contact Name, Credit Type, Custom Keys Used, Description, AI Model.
      tags:
        - Agency
      security:
        - ApiKeyAuth: []
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: from
          in: query
          required: false
          schema:
            type: string
            format: date
          description: Start date for the report range (YYYY-MM-DD). Defaults to 30 days ago.
          example: "2024-01-01"
        - name: to
          in: query
          required: false
          schema:
            type: string
            format: date
          description: End date for the report range (YYYY-MM-DD). Defaults to today.
          example: "2024-01-31"
        - name: subAccountId
          in: query
          required: false
          schema:
            type: string
          description: Filter report to a specific sub-account ID. If omitted, all sub-accounts are included.
          example: "uid123abc"
      responses:
        '200':
          description: CSV usage report downloaded successfully
          content:
            text/csv:
              schema:
                type: string
                description: |
                  CSV file with agency usage data. Headers: Sub Account ID, Sub Account Email,
                  Sub Account Name, Date, Amount, Reason, Campaign Name, Contact Name,
                  Credit Type, Custom Keys Used, Description, AI Model
              example: |
                Sub Account ID,Sub Account Email,Sub Account Name,Date,Amount,Reason,Campaign Name,Contact Name,Credit Type,Custom Keys Used,Description,AI Model
                subacct_abc123,sub@example.com,John Doe,2026-04-29T10:15:00Z,-1,ai_message,Welcome Flow,Jane Doe,ai,false,Outbound AI reply,claude-sonnet-4
                subacct_abc123,sub@example.com,John Doe,2026-04-29T11:02:14Z,-1,whatsapp_message,Welcome Flow,Jane Doe,messaging,false,WhatsApp outbound,
                subacct_def456,sub2@example.com,Acme Co,2026-04-29T12:30:00Z,500,manual_topup,,,credit,false,Auto-recharge callback,
        '400':
          description: Invalid date format or date range
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error_code:
                    type: integer
                    examples: [400]
                  error:
                    type: string
                    examples: ["Invalid date format. Use YYYY-MM-DD"]
              example:
                success: false
                error_code: 400
                error: Invalid date format. Use YYYY-MM-DD
        '401':
          description: Invalid or missing API key
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["Missing required parameters. Please provide either an API key or user ID."]
        '403':
          description: Authenticated user is not an Agency or Dev account
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error_code:
                    type: integer
                    examples: [403]
                  error:
                    type: string
                    examples: ["Access denied: Agency account required"]
        '500':
          description: Internal server error
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error_code:
                    type: integer
                    examples: [500]
                  error:
                    type: string
                    examples: ["Internal server error while generating usage report"]

  /zenchef-widget:
    get:
      summary: Zenchef Booking Widget (query param)
      description: |
        Returns an embeddable JavaScript widget that adds a Zenchef restaurant booking button to a webpage.

        This endpoint is **public** — no authentication required.

        The widget displays a floating booking button that opens a Zenchef booking iframe modal,
        along with a WhatsApp contact button. Embed using a `<script src="...">` tag.

        Phone number can be provided via the phone query parameter.
      tags:
        - Widgets
      parameters:
        - name: phone
          in: query
          required: true
          schema:
            type: string
          description: WhatsApp phone number (digits only, no + or spaces)
          example: "33612345678"
        - name: restaurantId
          in: query
          required: true
          schema:
            type: string
          description: Zenchef restaurant ID for booking widget
          example: "123456"
        - name: position
          in: query
          required: false
          schema:
            type: string
            enum: [header, footer]
            default: footer
          description: Widget button position on the page
        - name: buttonText
          in: query
          required: false
          schema:
            type: string
            default: "Book a Table"
          description: Text to display on the booking button
        - name: primaryColor
          in: query
          required: false
          schema:
            type: string
            default: "#a47e1f"
          description: Primary color for the widget (hex code)
          example: "#a47e1f"
      responses:
        '200':
          description: JavaScript widget code
          content:
            application/javascript:
              schema:
                type: string
                description: Self-executing JavaScript function that injects the booking widget
              example: |
                (function () {
                  var btn = document.createElement('button');
                  btn.textContent = 'Book a Table';
                  btn.style.cssText = 'background:#a47e1f;color:#fff;border:0;padding:12px 18px;border-radius:24px;position:fixed;bottom:20px;right:20px;';
                  btn.onclick = function () {
                    window.open('https://bookings.zenchef.com/results?rid=123456', '_blank');
                  };
                  document.body.appendChild(btn);
                })();
        '400':
          description: Missing required parameters
          content:
            text/plain:
              schema:
                type: string
                examples: ["Restaurant ID is required (add ?restaurantId=YOUR_RESTAURANT_ID to the script URL)"]
              example: 'Restaurant ID is required (add ?restaurantId=YOUR_RESTAURANT_ID to the script URL)'

  /zenchef-widget/{phone}:
    get:
      summary: Zenchef Booking Widget (phone in path)
      description: |
        Returns an embeddable JavaScript widget with phone number provided via the URL path.

        This endpoint is **public** — no authentication required.
        Embed using a `<script src="/v1/zenchef-widget/33612345678?restaurantId=123456">` tag.
      tags:
        - Widgets
      parameters:
        - name: phone
          in: path
          required: true
          schema:
            type: string
          description: WhatsApp phone number (digits only, no + or spaces)
          example: "33612345678"
        - name: restaurantId
          in: query
          required: true
          schema:
            type: string
          description: Zenchef restaurant ID for booking widget
          example: "123456"
        - name: position
          in: query
          required: false
          schema:
            type: string
            enum: [header, footer]
            default: footer
          description: Widget button position on the page
        - name: buttonText
          in: query
          required: false
          schema:
            type: string
            default: "Book a Table"
          description: Text to display on the booking button
        - name: primaryColor
          in: query
          required: false
          schema:
            type: string
            default: "#a47e1f"
          description: Primary color for the widget (hex code)
      responses:
        '200':
          description: JavaScript widget code
          content:
            application/javascript:
              schema:
                type: string
              example: |
                (function () {
                  var btn = document.createElement('button');
                  btn.textContent = 'Book a Table';
                  btn.style.cssText = 'background:#a47e1f;color:#fff;border:0;padding:12px 18px;border-radius:24px;position:fixed;bottom:20px;right:20px;';
                  btn.onclick = function () {
                    window.open('https://bookings.zenchef.com/results?rid=123456', '_blank');
                  };
                  document.body.appendChild(btn);
                })();
        '400':
          description: Missing required parameters
          content:
            text/plain:
              schema:
                type: string
              example: 'Restaurant ID is required'

  /zenchef-widget/{phone}/{message}:
    get:
      summary: Zenchef Booking Widget (phone + pre-filled message)
      description: |
        Returns an embeddable JavaScript widget with phone number and pre-filled WhatsApp message in the URL path.

        This endpoint is **public** — no authentication required.
        The message path segment is passed as a pre-filled WhatsApp message when the user clicks the WhatsApp button.
      tags:
        - Widgets
      parameters:
        - name: phone
          in: path
          required: true
          schema:
            type: string
          description: WhatsApp phone number (digits only, no + or spaces)
          example: "33612345678"
        - name: message
          in: path
          required: true
          schema:
            type: string
          description: Pre-filled WhatsApp message text
          example: "Hello, I'd like to make a reservation"
        - name: restaurantId
          in: query
          required: true
          schema:
            type: string
          description: Zenchef restaurant ID for booking widget
          example: "123456"
        - name: position
          in: query
          required: false
          schema:
            type: string
            enum: [header, footer]
            default: footer
        - name: buttonText
          in: query
          required: false
          schema:
            type: string
            default: "Book a Table"
        - name: primaryColor
          in: query
          required: false
          schema:
            type: string
            default: "#a47e1f"
      responses:
        '200':
          description: JavaScript widget code
          content:
            application/javascript:
              schema:
                type: string
              example: |
                (function () {
                  var waBtn = document.createElement('a');
                  waBtn.href = 'https://wa.me/33612345678?text=' + encodeURIComponent("Hello, I'd like to make a reservation");
                  waBtn.textContent = 'WhatsApp';
                  waBtn.style.cssText = 'background:#25D366;color:#fff;padding:12px 18px;border-radius:24px;position:fixed;bottom:20px;right:20px;';
                  document.body.appendChild(waBtn);
                })();
        '400':
          description: Missing required parameters
          content:
            text/plain:
              schema:
                type: string
              example: 'Restaurant ID is required'

  /formitable-widget:
    get:
      summary: Formitable Booking Widget (query param)
      description: |
        Returns an embeddable JavaScript widget that adds a Formitable restaurant booking button to a webpage.

        This endpoint is **public** — no authentication required.

        The widget displays a floating booking button that opens a Formitable booking iframe modal,
        along with a WhatsApp contact button. Embed using a `<script src="...">` tag.

        Phone number can be provided via the phone query parameter.
      tags:
        - Widgets
      parameters:
        - name: phone
          in: query
          required: true
          schema:
            type: string
          description: WhatsApp phone number (digits only, no + or spaces)
          example: "33612345678"
        - name: restaurantId
          in: query
          required: true
          schema:
            type: string
          description: Formitable restaurant ID for booking widget
          example: "abc123"
        - name: position
          in: query
          required: false
          schema:
            type: string
            enum: [header, footer]
            default: footer
          description: Widget button position on the page
        - name: buttonText
          in: query
          required: false
          schema:
            type: string
            default: "Book a Table"
          description: Text to display on the booking button
        - name: primaryColor
          in: query
          required: false
          schema:
            type: string
            default: "#FF3800"
          description: Primary color for the widget (hex code). Default is Formitable orange.
          example: "#FF3800"
      responses:
        '200':
          description: JavaScript widget code
          content:
            application/javascript:
              schema:
                type: string
                description: Self-executing JavaScript function that injects the booking widget
              example: |
                (function () {
                  var btn = document.createElement('button');
                  btn.textContent = 'Book a Table';
                  btn.style.cssText = 'background:#FF3800;color:#fff;border:0;padding:12px 18px;border-radius:24px;position:fixed;bottom:20px;right:20px;';
                  btn.onclick = function () {
                    window.open('https://formitable.com/r/abc123', '_blank');
                  };
                  document.body.appendChild(btn);
                })();
        '400':
          description: Missing required parameters
          content:
            text/plain:
              schema:
                type: string
                examples: ["Restaurant ID is required (add ?restaurantId=YOUR_RESTAURANT_ID to the script URL)"]
              example: 'Restaurant ID is required (add ?restaurantId=YOUR_RESTAURANT_ID to the script URL)'

  /formitable-widget/{phone}:
    get:
      summary: Formitable Booking Widget (phone in path)
      description: |
        Returns an embeddable JavaScript widget with phone number provided via the URL path.

        This endpoint is **public** — no authentication required.
        Embed using a `<script src="/v1/formitable-widget/33612345678?restaurantId=abc123">` tag.
      tags:
        - Widgets
      parameters:
        - name: phone
          in: path
          required: true
          schema:
            type: string
          description: WhatsApp phone number (digits only, no + or spaces)
          example: "33612345678"
        - name: restaurantId
          in: query
          required: true
          schema:
            type: string
          description: Formitable restaurant ID for booking widget
          example: "abc123"
        - name: position
          in: query
          required: false
          schema:
            type: string
            enum: [header, footer]
            default: footer
          description: Widget button position on the page
        - name: buttonText
          in: query
          required: false
          schema:
            type: string
            default: "Book a Table"
          description: Text to display on the booking button
        - name: primaryColor
          in: query
          required: false
          schema:
            type: string
            default: "#FF3800"
          description: Primary color for the widget (hex code)
      responses:
        '200':
          description: JavaScript widget code
          content:
            application/javascript:
              schema:
                type: string
              example: |
                (function () {
                  var btn = document.createElement('button');
                  btn.textContent = 'Book a Table';
                  btn.style.cssText = 'background:#FF3800;color:#fff;border:0;padding:12px 18px;border-radius:24px;position:fixed;bottom:20px;right:20px;';
                  btn.onclick = function () {
                    window.open('https://formitable.com/r/abc123', '_blank');
                  };
                  document.body.appendChild(btn);
                })();
        '400':
          description: Missing required parameters
          content:
            text/plain:
              schema:
                type: string
              example: 'Restaurant ID is required'

  /formitable-widget/{phone}/{message}:
    get:
      summary: Formitable Booking Widget (phone + pre-filled message)
      description: |
        Returns an embeddable JavaScript widget with phone number and pre-filled WhatsApp message in the URL path.

        This endpoint is **public** — no authentication required.
        The message path segment is passed as a pre-filled WhatsApp message when the user clicks the WhatsApp button.
      tags:
        - Widgets
      parameters:
        - name: phone
          in: path
          required: true
          schema:
            type: string
          description: WhatsApp phone number (digits only, no + or spaces)
          example: "33612345678"
        - name: message
          in: path
          required: true
          schema:
            type: string
          description: Pre-filled WhatsApp message text
          example: "Hello, I'd like to make a reservation"
        - name: restaurantId
          in: query
          required: true
          schema:
            type: string
          description: Formitable restaurant ID for booking widget
          example: "abc123"
        - name: position
          in: query
          required: false
          schema:
            type: string
            enum: [header, footer]
            default: footer
        - name: buttonText
          in: query
          required: false
          schema:
            type: string
            default: "Book a Table"
        - name: primaryColor
          in: query
          required: false
          schema:
            type: string
            default: "#FF3800"
      responses:
        '200':
          description: JavaScript widget code
          content:
            application/javascript:
              schema:
                type: string
              example: |
                (function () {
                  var waBtn = document.createElement('a');
                  waBtn.href = 'https://wa.me/33612345678?text=' + encodeURIComponent("Hello, I'd like to make a reservation");
                  waBtn.textContent = 'WhatsApp';
                  waBtn.style.cssText = 'background:#25D366;color:#fff;padding:12px 18px;border-radius:24px;position:fixed;bottom:20px;right:20px;';
                  document.body.appendChild(waBtn);
                })();
        '400':
          description: Missing required parameters
          content:
            text/plain:
              schema:
                type: string
              example: 'Restaurant ID is required'

  /checkout:
    get:
      summary: Agency Public Checkout
      description: |
        Creates a Stripe Checkout session for unauthenticated users (prospects) to purchase
        credits and become sub-accounts of an agency.

        This endpoint is **public** — no authentication required. It is designed for shareable
        payment links that agencies can distribute to potential sub-accounts. The ready-made link
        for each of your plans is returned as `checkout_url` by
        `GET /agency/pricing-tiers`.

        **Flow**:
        1. Agency shares the checkout URL with query params `id` (the agency uid) and `tierIndex`
        2. Endpoint validates params, fetches agency pricing config
        3. Creates a Stripe Checkout session on the agency's own Stripe account (BYOK)
        4. Redirects (303) to the Stripe hosted checkout page
        5. Stripe collects the buyer's email and name
        6. On payment success, a Firebase Auth user and Firestore doc are created automatically
        7. A welcome email with a temporary password is sent to the buyer

        **Paid plans vs trials** — what the buyer sees depends on the plan:

        - A plan with no `trial_days` sends the buyer straight to Stripe Checkout and charges
          immediately; the subscription starts on payment.
        - A plan with `trial_days` starts a FREE TRIAL instead: no charge until the trial ends,
          and the trial's credits (`trial_credits`, defaulting to the plan's `credits`) are
          granted at trial start out of your credit pool.
        - `trial_card_required: false` runs the trial WITHOUT collecting a card. Nothing to charge
          means the plan cancels itself at trial end unless the client adds a payment method.
        - `trial_hard_expiry: true` goes one step further at trial end: the client's UNUSED trial
          credits are revoked back into your pool and the account is blocked until they buy a paid
          plan.

        A public checkout link cannot know who is buying until Stripe collects the email, so a
        configured trial always applies here (the one-trial-per-account rule only governs checkouts
        started from inside an existing client account).
      tags:
        - Agency
      parameters:
        - name: id
          in: query
          required: true
          schema:
            type: string
          description: |
            Firebase UID of the agency. This is the parameter every current link uses.
          example: "fHjQASIIzfUcrvuAvaYWdDd65Bu2"
        - name: agencyId
          in: query
          required: false
          schema:
            type: string
          description: |
            LEGACY alias for `id`, still accepted so payment links shared before the rename keep
            working. Send `id` for new links; if both are present, `id` wins.
          example: "fHjQASIIzfUcrvuAvaYWdDd65Bu2"
        - name: tierIndex
          in: query
          required: true
          schema:
            type: integer
            minimum: 0
          description: Zero-based index into the agency's credit pricing tiers
          example: 0
      responses:
        '303':
          description: Redirect to Stripe Checkout hosted page
        '400':
          description: Missing or invalid parameters
          content:
            text/plain:
              schema:
                type: string
              example: 'Missing id parameter'
        '404':
          description: Agency not found
          content:
            text/plain:
              schema:
                type: string
              example: Agency not found
        '500':
          description: Internal server error
          content:
            text/plain:
              schema:
                type: string
              example: Internal server error while creating Stripe Checkout session

  /agency/pricing-tiers:
    get:
      summary: List Pricing Tiers
      security:
        - ApiKeyAuth: []
      description: |
        Lists the plans you sell to your clients in SaaS mode — the same price list the SaaS-mode
        page edits — each with its zero-based `tierIndex` and its ready-to-share
        `checkout_url`.

        Agency accounts with the white-labeling feature only, and only once a Stripe key is
        connected: every plan is a Product + Price on YOUR own Stripe account, so there is nothing
        to list before that.
      tags:
        - Agency
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
      responses:
        '200':
          description: The agency's pricing tiers
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  data:
                    type: object
                    properties:
                      tiers:
                        type: array
                        items:
                          $ref: '#/components/schemas/AgencyPricingTier'
                      count:
                        type: integer
                        examples: [3]
                      max_tiers:
                        type: integer
                        description: Maximum number of plans one account may offer.
                        examples: [10]
        '400':
          description: No Stripe key connected yet
        '401':
          description: Missing or invalid API key
        '403':
          description: Not an agency account, or the white-labeling feature is missing
    post:
      summary: Create Pricing Tier
      security:
        - ApiKeyAuth: []
      description: |
        Adds one plan to the end of your price list and creates its Stripe Product + Price on your
        own Stripe account. The new plan's index is the position it was appended at.

        The whole resulting list is validated, so the 10-plan maximum and every per-plan rule
        (minimum price per credit, valid currency, valid features, trial limits) apply exactly as
        they do when saving from the dashboard. Unknown fields are rejected with a 400 that names
        them.
      tags:
        - Agency
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AgencyPricingTierInput'
            examples:
              paid:
                summary: A straightforward paid monthly plan
                value:
                  credits: 1000
                  price_cents: 4900
                  currency: usd
                  label: Growth
                  features: [whatsapp, instagram]
              trial:
                summary: A 14-day no-card trial that hard-expires
                value:
                  credits: 1000
                  price_cents: 4900
                  currency: usd
                  label: Growth
                  trial_days: 14
                  trial_credits: 200
                  trial_card_required: false
                  trial_hard_expiry: true
      responses:
        '201':
          description: Plan created
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  data:
                    type: object
                    properties:
                      tier:
                        $ref: '#/components/schemas/AgencyPricingTier'
        '400':
          description: |
            Invalid tier (the message names the field and the reason), an unknown field, the 10-plan
            maximum, or no Stripe key connected yet
        '401':
          description: Missing or invalid API key
        '403':
          description: |
            Not an agency account, the white-labeling feature is missing, or the plan includes a
            product you do not hold yourself (Find Leads, Social Scheduler, SEO Agent)

  /agency/pricing-tiers/{tierIndex}:
    patch:
      summary: Update Pricing Tier
      security:
        - ApiKeyAuth: []
      description: |
        Updates one plan. Only the fields you send are changed — everything else stays as it is —
        and the resulting plan is then validated in full, exactly like a dashboard save.

        What that means for Stripe: changing `credits`, `price_cents`, `currency`,
        `billing_interval` or `billing_interval_count` RECREATES the plan's Stripe Price (those
        are immutable on a Stripe price) and archives the old one — clients already subscribed stay
        on the price they signed up on. Changing only a trial setting, the label or the description
        does not touch the price; the Stripe product name/description is re-synced instead.

        Unknown fields are rejected with a 400 that names them, and `stripe_price_id` /
        `stripe_product_id` cannot be set.
      tags:
        - Agency
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: tierIndex
          in: path
          required: true
          schema:
            type: integer
            minimum: 0
          description: Zero-based index of the plan in your price list
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AgencyPricingTierFields'
            examples:
              price_change:
                summary: Raise the price (recreates the Stripe price)
                value:
                  price_cents: 5900
              add_trial:
                summary: Add a 7-day trial with hard expiry (price untouched)
                value:
                  trial_days: 7
                  trial_hard_expiry: true
              remove_trial:
                summary: Remove the trial (also clears the other trial settings)
                value:
                  trial_days: 0
      responses:
        '200':
          description: Plan updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  data:
                    type: object
                    properties:
                      tier:
                        $ref: '#/components/schemas/AgencyPricingTier'
        '400':
          description: |
            Invalid tier (the message names the field and the reason), an unknown field, an empty
            body, or no Stripe key connected yet
        '401':
          description: Missing or invalid API key
        '403':
          description: |
            Not an agency account, the white-labeling feature is missing, or the plan includes a
            product you do not hold yourself (Find Leads, Social Scheduler, SEO Agent)
        '404':
          description: No plan at that index
    delete:
      summary: Delete Pricing Tier
      security:
        - ApiKeyAuth: []
      description: |
        Removes one plan and archives its Stripe Price.

        Refused with `409` while the plan still has ACTIVE subscribers — the same safety check the
        dashboard runs before letting you delete a plan (see
        `GET /subaccounts/{tierIndex}/can-delete` under SubAccounts). A plan that was never
        published to Stripe, or whose price no longer exists there, is always deletable.

        **Indexes shift.** The list is compacted, so every plan after the deleted one moves down by
        one position — the same thing that happens when the dashboard deletes a row and saves.
        Checkout links carry the index, so re-read `GET /agency/pricing-tiers` afterwards and
        re-share any link whose index moved.
      tags:
        - Agency
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: tierIndex
          in: path
          required: true
          schema:
            type: integer
            minimum: 0
          description: Zero-based index of the plan in your price list
      responses:
        '200':
          description: Plan deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  data:
                    type: object
                    properties:
                      deleted_tier_index:
                        type: integer
                        examples: [1]
                      remaining_tiers:
                        type: array
                        description: The price list after the delete, with the new indexes.
                        items:
                          $ref: '#/components/schemas/AgencyPricingTier'
        '400':
          description: Invalid tierIndex, or no Stripe key connected yet
        '401':
          description: Missing or invalid API key
        '403':
          description: Not an agency account, or the white-labeling feature is missing
        '404':
          description: No plan at that index
        '409':
          description: The plan has active subscribers and cannot be deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["This tier has 3 active subscriber(s) and cannot be deleted."]
                  data:
                    type: object
                    properties:
                      canDelete:
                        type: boolean
                        examples: [false]
                      activeSubscribers:
                        type: integer
                        examples: [3]

  /agency/credit-price:
    get:
      summary: Get Per-Credit Price
      security:
        - ApiKeyAuth: []
      description: |
        Returns the price you charge your clients for ONE credit when they top up a custom amount,
        the currency it is charged in, the short note shown under that price on their top-up page,
        and the lowest price you are allowed to charge in that currency.

        Everything is `null` while you have not set a price yet.

        Agency accounts with the white-labeling feature only. No Stripe connection is needed to read
        or set this price — it is only used to work out the total when a client buys credits.

        A key limited to the `Agency Credit Price` tag can reach these two operations and nothing
        else in the API, and a `read_only` key can do this GET.
      tags:
        - Agency
        - Agency Credit Price
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
      responses:
        '200':
          description: The per-credit price
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AgencyCreditPriceResponse'
        '401':
          description: Missing or invalid API key
        '403':
          description: Not an agency account, or the white-labeling feature is missing
    patch:
      summary: Update Per-Credit Price
      security:
        - ApiKeyAuth: []
      description: |
        Changes the price of one credit, the currency, and/or the note shown under the price. Send
        only the fields you want to change; anything you leave out stays as it is. Unknown fields are
        rejected with a message naming them.

        The price and the currency are always checked together, so changing only the currency
        re-checks your existing price against that currency's minimum — every currency has its own
        lowest allowed price, returned as `minimum_cents`. If you have never set a price, you must
        send `price_per_credit_cents` (a currency on its own has nothing to price).

        The note is trimmed and limited to 200 characters — it is a one-line clarification under the
        price (for example "R$1.25 = USD 0.25 at our reference rate"), not a terms page. Send an
        empty string (or `null`) to remove it.

        A key limited to the `Agency Credit Price` tag can reach these two operations and nothing
        else in the API.
      tags:
        - Agency
        - Agency Credit Price
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              minProperties: 1
              properties:
                price_per_credit_cents:
                  type: integer
                  minimum: 1
                  description: |
                    Price of ONE credit in the smallest unit of the currency (for example 25 = $0.25
                    when the currency is usd). Must be at least `minimum_cents` for that currency.
                  examples: [25]
                currency:
                  type: string
                  description: Currency the price is charged in (ISO 4217, lowercase).
                  examples: [usd]
                note:
                  type: [string, "null"]
                  maxLength: 200
                  description: |
                    Short line shown under the price on your client's top-up page. Empty string or
                    null removes it.
                  examples: ["R$1.25 = USD 0.25 at our reference rate"]
            examples:
              price:
                summary: Set the price and currency
                value:
                  price_per_credit_cents: 25
                  currency: usd
              note:
                summary: Add the note only
                value:
                  note: R$1.25 = USD 0.25 at our reference rate
              clearNote:
                summary: Remove the note
                value:
                  note: ''
      responses:
        '200':
          description: The per-credit price after the change
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AgencyCreditPriceResponse'
        '400':
          description: |
            An empty body, an unknown field, a price below the minimum for the currency, an
            unsupported currency, a note longer than 200 characters, or only a currency when no price
            has ever been set
        '401':
          description: Missing or invalid API key
        '403':
          description: Not an agency account, or the white-labeling feature is missing

  /analytics/summary:
    get:
      summary: Get Analytics Summary
      security:
        - ApiKeyAuth: []
      description: |
        Returns aggregated message-event counts for your account over a date
        range: sent, delivered, read, replied, booked, contacts created, and
        credits spent and recharged. Counts are returned both as range totals
        and as a per-day series with every day in the range present (days with
        no activity are zero-filled). Optionally filter to a single campaign.
        The range defaults to the last 30 days and is capped at 366 days.
      tags:
        - Analytics
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
        - name: from
          in: query
          required: false
          schema:
            type: string
            format: date
          description: Start of the range (YYYY-MM-DD, inclusive). Defaults to 30 days ago.
          example: "2026-05-01"
        - name: to
          in: query
          required: false
          schema:
            type: string
            format: date
          description: End of the range (YYYY-MM-DD, inclusive). Defaults to today.
          example: "2026-05-31"
        - name: campaign_id
          in: query
          required: false
          schema:
            type: string
          description: >-
            Only count events belonging to this campaign. Legacy; prefer agent_id
            (nothing has minted a campaign behind a new agent for some time, so a
            migrated account's events carry no campaign at all).
        - name: agent_id
          in: query
          required: false
          schema:
            type: string
          description: |
            Only count activity belonging to this AI Agent. This is the scope to
            use: the Campaigns surface is hidden on a migrated account, so an
            agent id is normally the only scope you have. Send agent_id OR
            campaign_id, never both (both is a 400). An agent id that is not on
            your account returns 404.
          example: "agent_abc123"
      responses:
        '200':
          description: Aggregated event counts
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  from:
                    type: string
                    format: date
                    description: Effective range start.
                  to:
                    type: string
                    format: date
                    description: Effective range end.
                  totals:
                    $ref: '#/components/schemas/AnalyticsEventCounts'
                  by_date:
                    type: array
                    description: Per-day counters, sorted by date ascending, with quiet days zero-filled.
                    items:
                      allOf:
                        - type: object
                          properties:
                            date:
                              type: string
                              format: date
                        - $ref: '#/components/schemas/AnalyticsEventCounts'
        '400':
          description: Both agent_id and campaign_id were supplied (send exactly one)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AnalyticsApiError'
        '404':
          description: Campaign not found, or agent not found on this account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AnalyticsApiError'
  /analytics/credits:
    get:
      summary: Get Credit Usage
      security:
        - ApiKeyAuth: []
      description: |
        Returns credit usage for your account over a date range: a paginated
        list of individual usage records plus range totals and breakdowns by
        reason and by campaign. Balance adjustments such as bonuses, plan
        renewals, and corrections are excluded from the totals and breakdowns
        (they are balance changes, not consumption) but still appear in the
        record list flagged with is_adjustment. Cost and token details are
        included only for records billed to your own provider API keys.
        The range defaults to the last 30 days and is capped at 366 days.
      tags:
        - Analytics
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
        - name: from
          in: query
          required: false
          schema:
            type: string
            format: date
          description: Start of the range (YYYY-MM-DD, inclusive). Defaults to 30 days ago.
          example: "2026-05-01"
        - name: to
          in: query
          required: false
          schema:
            type: string
            format: date
          description: End of the range (YYYY-MM-DD, inclusive). Defaults to today.
          example: "2026-05-31"
        - name: campaign_id
          in: query
          required: false
          schema:
            type: string
          description: Only include usage attributed to this campaign. Legacy; prefer agent_id.
        - name: agent_id
          in: query
          required: false
          schema:
            type: string
          description: |
            Only include usage attributed to this AI Agent. This is the scope to use
            on an agent-native account, where spend carries an agent rather than a
            campaign. Unlike the other analytics reads, this endpoint narrows by BOTH
            filters when you send agent_id and campaign_id together rather than
            rejecting the pair.
          example: "agent_abc123"
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 50
          description: Page size for the records list, between 1 and 100.
        - name: cursor
          in: query
          required: false
          schema:
            type: string
          description: Opaque pagination cursor. Pass the next_cursor value from the previous page to fetch the next one.
      responses:
        '200':
          description: Credit usage with totals and breakdowns
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  from:
                    type: string
                    format: date
                  to:
                    type: string
                    format: date
                  totals:
                    type: object
                    properties:
                      credits_used:
                        type: number
                        description: Credits consumed in the range, excluding balance adjustments.
                      cost_usd:
                        type: number
                        description: USD cost of calls billed to your own provider API keys.
                      records:
                        type: integer
                        description: Total number of records in the range, including adjustments.
                  by_reason:
                    type: object
                    additionalProperties:
                      type: number
                    description: Credits consumed keyed by usage reason.
                  by_reason_cost:
                    type: object
                    additionalProperties:
                      type: number
                    description: USD cost keyed by usage reason.
                  by_campaign:
                    type: object
                    additionalProperties:
                      type: number
                    description: Credits consumed keyed by campaign name.
                  by_agent:
                    type: object
                    additionalProperties:
                      type: number
                    description: >-
                      Credits consumed keyed by AI Agent ID. Sits alongside
                      by_campaign rather than replacing it - they are two independent
                      ways of grouping the same records. Usage with no agent (an
                      account-level top-up, say) is left out rather than piled into an
                      "unknown" bucket.
                  records:
                    type: array
                    items:
                      $ref: '#/components/schemas/AnalyticsCreditRecord'
                  next_cursor:
                    type: ['string', 'null']
                    description: Cursor for the next page, or null when there are no more records.
                  costs_redacted:
                    type: boolean
                    description: True when USD cost and token figures are hidden for this account and returned as zero.
        '400':
          description: Invalid date range, limit, or cursor
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AnalyticsApiError'
  /analytics/ai-cost:
    get:
      summary: Get AI Cost Rollup
      security:
        - ApiKeyAuth: []
      description: |
        Returns the per-day AI spend rollup for your account over a date range:
        total USD per day, the split between spend billed to your own provider
        API keys and spend covered by credits, per-provider totals, cost
        components (input, output, prompt-cache reads and writes), and call
        counts. Every day in the range is present; days with no AI activity are
        zero-filled. The range defaults to the last 30 days and is capped at
        366 days.
      tags:
        - Analytics
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
        - name: from
          in: query
          required: false
          schema:
            type: string
            format: date
          description: Start of the range (YYYY-MM-DD, inclusive). Defaults to 30 days ago.
          example: "2026-05-01"
        - name: to
          in: query
          required: false
          schema:
            type: string
            format: date
          description: End of the range (YYYY-MM-DD, inclusive). Defaults to today.
          example: "2026-05-31"
      responses:
        '200':
          description: Per-day AI cost rollup
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  from:
                    type: string
                    format: date
                  to:
                    type: string
                    format: date
                  totals:
                    type: object
                    properties:
                      total_usd:
                        type: number
                        description: Total AI spend in the range.
                      byok_usd:
                        type: number
                        description: Spend billed to your own provider API keys.
                      platform_usd:
                        type: number
                        description: Spend that ran on the platform rather than your own key. Only returned to accounts using their own provider key; zero when costs are redacted.
                      calls:
                        type: integer
                        description: Total AI calls in the range.
                  days:
                    type: array
                    description: Per-day rollups, sorted by date ascending, with quiet days zero-filled.
                    items:
                      $ref: '#/components/schemas/AnalyticsAiCostDay'
                  costs_redacted:
                    type: boolean
                    description: True when cost figures are hidden for this account and returned as zero.
        '400':
          description: Invalid date range
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AnalyticsApiError'
  /agencies/usage/summary:
    get:
      summary: Get Agency Usage Summary
      description: |
        Returns a JSON summary of credit usage per sub-account over a date
        range: totals, a breakdown by usage reason, and the top campaigns by
        consumption and by cost. Balance adjustments such as bonuses, renewals,
        and corrections are excluded from all sums. USD cost and token figures
        are only summed from records billed against a provider API key.

        **Requires Agency or Dev role.** This endpoint only accepts apiKey
        authentication (userId is not supported). The range defaults to the
        last 30 days and is capped at 366 days.
      tags:
        - Agency
      security:
        - ApiKeyAuth: []
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: from
          in: query
          required: false
          schema:
            type: string
            format: date
          description: Start of the range (YYYY-MM-DD, inclusive). Defaults to 30 days ago.
          example: "2026-05-01"
        - name: to
          in: query
          required: false
          schema:
            type: string
            format: date
          description: End of the range (YYYY-MM-DD, inclusive). Defaults to today.
          example: "2026-05-31"
        - name: sub_account_id
          in: query
          required: false
          schema:
            type: string
          description: Only include usage for this sub-account.
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 50
          description: Maximum number of sub-accounts to return, between 1 and 100.
      responses:
        '200':
          description: Per-sub-account usage summary
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  from:
                    type: string
                    format: date
                  to:
                    type: string
                    format: date
                  totals:
                    type: object
                    properties:
                      total_credits_used:
                        type: number
                        description: Credits consumed across all sub-accounts, excluding adjustments.
                      total_cost_usd:
                        type: number
                        description: USD cost across all sub-accounts.
                      total_records:
                        type: integer
                        description: Number of sub-account usage records considered.
                      sub_account_count:
                        type: integer
                        description: Number of sub-accounts with usage in the range.
                  sub_accounts:
                    type: array
                    description: Per-sub-account summaries, sorted by USD cost then credits used, descending.
                    items:
                      $ref: '#/components/schemas/AgencySubAccountUsageSummary'
        '403':
          description: Account does not have the Agency or Dev role
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AnalyticsApiError'
  /api-keys/current:
    get:
      summary: Get API Key Metadata
      security:
        - ApiKeyAuth: []
      description: |
        Returns your account's active API key: the full key in api_key when a
        retrievable copy exists, a masked preview (first 4 and last 4
        characters), and, when available, the date the key was created.
        api_key is null for keys created before retrievable copies were kept -
        rotate once to receive a key that can be shown again later.
      tags:
        - API Keys
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
      responses:
        '200':
          description: API key metadata
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiKeyMetadata'
              example:
                success: true
                api_key: "abcdEFGH1234ijkl5678MNOP9012qrst"
                api_key_masked: "abcd...qrst"
                created_at: "2026-06-01T10:00:00.000Z"
        '404':
          description: The account has no API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiKeyErrorResponse'
              example:
                success: false
                error: "No API key found for this account"
    delete:
      summary: Revoke API Key
      security:
        - ApiKeyAuth: []
      description: |
        Permanently revokes your account's API key. Revocation takes effect
        immediately: every request that uses the revoked key - including
        integrations such as Make, Zapier, or custom scripts - will be rejected
        with a 401 error from this moment on. To restore API access, generate a
        new key from your account settings while signed in to the app.
      tags:
        - API Keys
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
      responses:
        '200':
          description: API key revoked
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiKeyRevokeResponse'
              example:
                success: true
                revoked: true
                message: "API key revoked. All requests using it will be rejected immediately."
        '404':
          description: The account has no API key to revoke
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiKeyErrorResponse'
              example:
                success: false
                error: "No API key found for this account"
  /api-keys/rotate:
    post:
      summary: Rotate API Key
      security:
        - ApiKeyAuth: []
      description: |
        Generates a new API key for your account and invalidates the previous
        one in the same step. Use this if you suspect your key has leaked or as
        part of a regular credential-rotation policy.

        The new key is returned once in this response and cannot be retrieved
        in full afterwards - store it securely immediately and update every
        integration that uses the old key, since the old key stops working the
        moment this call succeeds.
      tags:
        - API Keys
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
      responses:
        '200':
          description: New API key generated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiKeyRotateResponse'
              example:
                success: true
                api_key: "abcdEFGH1234ijkl5678MNOP9012qrst"
                message: "API key rotated. The previous key is no longer valid. Store this key now - it will not be shown again."
        '400':
          description: The account is missing an email address needed to generate a key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiKeyErrorResponse'
              example:
                success: false
                error: "An account email is required to generate an API key"
  /api-keys/usage:
    get:
      summary: Get Rate-Limit Usage
      security:
        - ApiKeyAuth: []
      description: |
        Returns your account's rate-limit usage for the current window: the
        request limit per window, how many requests have been counted so far,
        how many remain, and when the window resets. Useful for building
        client-side throttling so your integration backs off before hitting
        429 responses. If no requests have been recorded in the current window
        yet, usage is reported as zero alongside an explanatory note.
      tags:
        - API Keys
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
      responses:
        '200':
          description: Current rate-limit usage
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiKeyUsage'
              example:
                success: true
                usage:
                  limit: 120
                  window_seconds: 60
                  used: 37
                  remaining: 83
                  window_resets_at: "2026-06-09T12:01:00.000Z"
        '401':
          description: Missing or invalid authentication
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiKeyErrorResponse'
              example:
                success: false
                error: "Invalid API key"
  /appointments:
    post:
      summary: Book Appointment
      security:
        - ApiKeyAuth: []
      description: |
        Book a new appointment for a contact on one of your event types.

        The booking is conflict-checked: if the requested slot overlaps an existing confirmed
        appointment on the same event type, the request fails with a 409 and nothing is created.
        The appointment end time is derived from the event type's slot duration. When a Google
        Calendar connection is active, the matching calendar event is created automatically in
        the background shortly after booking.
      tags:
        - Appointments
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AppointmentCreateRequest'
            examples:
              book_appointment:
                summary: Book a 30 minute slot
                value:
                  contact_id: "contact_abc123"
                  event_id: "event_xyz789"
                  start_time: "2026-06-15T10:00:00.000Z"
                  room_name: "Room A"
      responses:
        '201':
          description: Appointment booked successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  appointment_id:
                    type: string
                    examples: ["aBcD1234eFgH5678"]
                  appointment:
                    $ref: '#/components/schemas/Appointment'
        '400':
          description: Missing or invalid fields
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["Missing required fields: contact_id, event_id and start_time are required"]
        '404':
          description: Contact or event not found
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["Contact not found"]
        '409':
          description: The requested time slot is already taken
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["Time slot is already taken."]
    get:
      summary: List Appointments
      security:
        - ApiKeyAuth: []
      description: |
        List appointments for your account, newest first.

        Without filters, all appointments on the account are returned page by page. Filter by
        contact_id to see one contact's confirmed appointments, optionally narrowed to a single
        day with the date filter (YYYY-MM-DD). Without a contact filter you can also filter by
        status (Confirmed or Canceled). Use limit and cursor to page through results: pass the
        next_cursor value from one response as the cursor of the next request.
      tags:
        - Appointments
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
        - name: contact_id
          in: query
          required: false
          description: Only return appointments for this contact. Contact-filtered listings include confirmed appointments only.
          schema:
            type: string
        - name: date
          in: query
          required: false
          description: Only return appointments on this calendar day (YYYY-MM-DD). Requires contact_id.
          schema:
            type: string
            pattern: '^[0-9]{4}-[0-9]{2}-[0-9]{2}$'
          example: "2026-06-15"
        - name: status
          in: query
          required: false
          description: Filter by appointment status. Only available without contact_id.
          schema:
            type: string
            enum: ["Confirmed", "Canceled"]
        - name: limit
          in: query
          required: false
          description: Maximum number of appointments to return per page (1-100, default 50).
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 50
        - name: cursor
          in: query
          required: false
          description: Pagination cursor from a previous response's next_cursor field.
          schema:
            type: string
      responses:
        '200':
          description: Appointments retrieved successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  appointments:
                    type: array
                    items:
                      $ref: '#/components/schemas/Appointment'
                  next_cursor:
                    type: ['string', 'null']
                    description: Cursor for the next page, or null when there are no more results.
        '400':
          description: Invalid filter, limit or cursor
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["limit must be an integer between 1 and 100"]
        '404':
          description: Contact not found
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["Contact not found"]

  /appointments/{appointmentId}:
    get:
      summary: Get Appointment
      security:
        - ApiKeyAuth: []
      description: |
        Retrieve a single appointment by its ID, including its calendar sync state
        (google_calendar_event_id and calendar_synced).
      tags:
        - Appointments
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
        - name: appointmentId
          in: path
          required: true
          description: ID of the appointment to retrieve.
          schema:
            type: string
      responses:
        '200':
          description: Appointment retrieved successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  appointment:
                    $ref: '#/components/schemas/Appointment'
        '404':
          description: Appointment not found
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["Appointment not found"]
    put:
      summary: Update Appointment
      security:
        - ApiKeyAuth: []
      description: |
        Reschedule an appointment or change its details. You can move the start and end time,
        change the room, or edit the description and summary. The combined start and end must
        stay in chronological order. Changes are synced to the linked calendar event
        automatically in the background.
      tags:
        - Appointments
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
        - name: appointmentId
          in: path
          required: true
          description: ID of the appointment to update.
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AppointmentUpdateRequest'
            examples:
              reschedule:
                summary: Move the appointment to the next day
                value:
                  start_time: "2026-06-16T10:00:00.000Z"
                  end_time: "2026-06-16T10:30:00.000Z"
      responses:
        '200':
          description: Appointment updated successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  appointment_id:
                    type: string
                  appointment:
                    $ref: '#/components/schemas/Appointment'
        '400':
          description: Invalid or missing update fields
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["No fields to update"]
        '404':
          description: Appointment not found
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["Appointment not found"]
    delete:
      summary: Delete Appointment
      security:
        - ApiKeyAuth: []
      description: |
        Permanently delete an appointment. This removes the appointment and its references;
        if you only want to call the booking off while keeping the record, use the cancel
        endpoint instead.
      tags:
        - Appointments
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
        - name: appointmentId
          in: path
          required: true
          description: ID of the appointment to delete.
          schema:
            type: string
      responses:
        '200':
          description: Appointment deleted successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
        '404':
          description: Appointment not found
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["Appointment not found"]

  /appointments/{appointmentId}/cancel:
    post:
      summary: Cancel Appointment
      security:
        - ApiKeyAuth: []
      description: |
        Cancel a confirmed appointment, optionally recording a cancellation reason. The
        appointment stays in your account with status Canceled, and the linked calendar
        event is removed automatically in the background.
      tags:
        - Appointments
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
        - name: appointmentId
          in: path
          required: true
          description: ID of the appointment to cancel.
          schema:
            type: string
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AppointmentCancelRequest'
            examples:
              cancel_with_reason:
                summary: Cancel with a reason
                value:
                  cancellation_reason: "Client asked to reschedule next month"
      responses:
        '200':
          description: Appointment canceled successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  appointment_id:
                    type: string
                    examples: ["aBcD1234eFgH5678"]
        '400':
          description: The appointment is already canceled or the reason is invalid
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["Appointment is already canceled"]
        '404':
          description: Appointment not found
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["Appointment not found"]
  /events:
    get:
      summary: List Event Types
      security:
        - ApiKeyAuth: []
      description: |
        List the bookable event types on your account, newest first.

        Start here before booking: POST /appointments needs an event_id, and this is the only place
        the API hands one out. Each row is a summary - name, meeting modality, appointment length,
        notice period, booking window and whether any availability is set - rather than the whole
        configuration, so a long list stays readable. All of your event types are returned in one
        response; there is no paging.
      tags:
        - Events
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
      responses:
        '200':
          description: Event types retrieved successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  events:
                    type: array
                    description: Your event types, newest first. Empty when you have not created any.
                    items:
                      $ref: '#/components/schemas/EventSummary'
              example:
                success: true
                events:
                  - id: event_xyz789
                    event_name: 30 minute intro call
                    event_type: google_meet
                    location: null
                    slot_duration: 30
                    notice: 4
                    booking_until_x_days: 28
                    has_availability: true
                    room_count: 0
                    start_date: null
                    end_date: null
                    created_at: '2026-01-01T12:00:00.000Z'
    post:
      summary: Create Event Type
      security:
        - ApiKeyAuth: []
      description: |
        Create a bookable event type.

        Everything is optional, so you can create a blank draft and configure it afterwards. A draft
        is born with the same defaults the dashboard uses: 30 minute appointments offered every 15
        minutes, 4 hours of notice required, and bookings accepted up to 28 days ahead. Any value you
        send yourself is kept as-is.
      tags:
        - Events
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/EventWriteRequest'
            examples:
              intro_call:
                summary: A 30 minute Google Meet call, weekday mornings
                value:
                  event_name: "30 minute intro call"
                  event_type: "google_meet"
                  slot_duration: 30
                  granularity: 15
                  notice: 4
                  booking_until_x_days: 28
                  daily_availability:
                    monday:
                      start_time: "09:00"
                      end_time: "12:00"
                    tuesday:
                      start_time: "09:00"
                      end_time: "12:00"
              blank_draft:
                summary: A blank draft to configure later
                value: {}
      responses:
        '201':
          description: Event type created
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  event_id:
                    type: string
                    description: Use this as the event_id when booking an appointment.
                    examples: ["event_xyz789"]
        '400':
          description: An invalid field value (event_name, event_type, a date, a numeric setting, rooms, or a capacity)
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["Invalid field: event_type. Must be one of: in_person, google_meet, custom_link, zoom"]
        '409':
          description: The requested booking-page link is already taken by another event
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["That booking link is already in use."]
                  error_reason:
                    type: string
                    examples: ["slug_taken"]

  /events/{eventId}:
    put:
      summary: Update Event Type
      security:
        - ApiKeyAuth: []
      description: |
        Change an event type. Only the fields you send are changed.

        Two things behave specially. Sending null for start_date or end_date clears that boundary
        instead of storing an empty value. And rooms are edited in one of two ways: send rooms to
        replace the whole set, or send add_room with a single room to append one without touching
        the others - never both in the same request.
      tags:
        - Events
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
        - name: eventId
          in: path
          required: true
          description: ID of the event type to update.
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/EventWriteRequest'
            examples:
              rename:
                summary: Rename the event type
                value:
                  event_name: "45 minute strategy call"
                  slot_duration: 45
              clear_end_date:
                summary: Remove the booking window end
                value:
                  end_date: null
              add_a_room:
                summary: Append one room, leaving existing rooms alone
                value:
                  add_room:
                    name: "Treatment room 2"
                    daily_availability:
                      monday:
                        start_time: "09:00"
                        end_time: "17:00"
                    slot_capacity: 1
      responses:
        '200':
          description: Event type updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  event_id:
                    type: string
                    examples: ["event_xyz789"]
        '400':
          description: Empty body, or an invalid field value
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["No fields to update"]
        '404':
          description: No event type with this ID exists in your account
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["Event not found"]
        '409':
          description: The requested booking-page link is already taken by another event
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["That booking link is already in use."]
                  error_reason:
                    type: string
                    examples: ["slug_taken"]
    delete:
      summary: Delete Event Type
      security:
        - ApiKeyAuth: []
      description: |
        Delete an event type.

        If an ACTIVE AI Agent still uses this event type for booking, the delete is refused with a
        409 that lists the agents and campaigns involved - deleting it would silently break their
        booking setup. Point them at another event type, switch them off, or repeat the call with
        force=true to delete anyway.
      tags:
        - Events
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
        - name: eventId
          in: path
          required: true
          description: ID of the event type to delete.
          schema:
            type: string
        - name: force
          in: query
          required: false
          description: Set to true to delete even while an active agent still uses this event type.
          schema:
            type: boolean
            default: false
      responses:
        '200':
          description: Event type deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
        '404':
          description: No event type with this ID exists in your account
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["Event not found"]
        '409':
          description: An active agent still uses this event type
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error_code:
                    type: string
                    examples: ["EVENT_IN_USE"]
                  error:
                    type: string
                    examples: ["This event type is still used by an active AI Agent."]
                  agents:
                    type: array
                    items:
                      $ref: '#/components/schemas/EventReferencingAgent'
                  campaigns:
                    type: array
                    items:
                      $ref: '#/components/schemas/EventReferencingCampaign'

  /events/{eventId}/references:
    get:
      summary: List Event Type References
      security:
        - ApiKeyAuth: []
      description: |
        List which AI Agents and campaigns currently use an event type - the check to run before
        deleting one.

        Both lists come back empty when nothing uses the event type AND when the lookup could not be
        performed, so an empty result means "nothing known", not a guarantee.
      tags:
        - Events
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
        - name: eventId
          in: path
          required: true
          description: ID of the event type to inspect.
          schema:
            type: string
      responses:
        '200':
          description: References retrieved successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  agents:
                    type: array
                    description: AI Agents that use this event type, whether switched on or off.
                    items:
                      $ref: '#/components/schemas/EventReferencingAgent'
                  campaigns:
                    type: array
                    description: Campaigns that use this event type.
                    items:
                      $ref: '#/components/schemas/EventReferencingCampaign'
              example:
                success: true
                agents:
                  - id: ag7HkQ2ZpLxR3mNb
                    name: Front desk assistant
                    active: true
                campaigns: []

  /campaigns:
    get:
      summary: List Campaigns
      security:
        - ApiKeyAuth: []
      description: |
        Returns the account's campaigns, newest first. Archived campaigns are
        excluded unless the archived query parameter is set to true. Use the
        limit and cursor parameters to page through large accounts; pass the
        next_cursor value from one response as the cursor of the next request.
      tags:
        - Campaigns
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
        - name: limit
          in: query
          required: false
          description: Maximum number of campaigns to return (default 50, maximum 100)
          schema:
            type: integer
            minimum: 1
            maximum: 100
        - name: cursor
          in: query
          required: false
          description: Opaque pagination cursor from a previous response's next_cursor field
          schema:
            type: string
        - name: archived
          in: query
          required: false
          description: Set to true to include archived campaigns in the results
          schema:
            type: string
            enum: ['true', 'false']
      responses:
        '200':
          description: Campaigns retrieved successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  campaigns:
                    type: array
                    items:
                      $ref: '#/components/schemas/CampaignSummary'
                  next_cursor:
                    type: ['string', 'null']
                    description: Cursor for the next page, or null when there are no more results
        '400':
          description: Invalid limit or cursor
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["Invalid cursor"]
    post:
      summary: Create Campaign
      security:
        - ApiKeyAuth: []
      description: |
        Creates a new campaign. The campaign owner and creation time are set
        automatically. Provide a name and a type; any other campaign fields
        (language, ai_mode, bot configuration, enabled channels, and so on)
        can be included and are stored with the new campaign.
      tags:
        - Campaigns
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, type]
              additionalProperties: true
              properties:
                name:
                  type: string
                  description: Campaign name
                  examples: ["Inbound WhatsApp Leads"]
                type:
                  type: string
                  enum: ['Incoming from Unknown Contacts', 'Outgoing', 'Keywords', 'Combined']
                  description: Campaign type
                status:
                  type: string
                  enum: ['Draft', 'Pending Approval', 'Scheduled', 'Live', 'Paused', 'Completed', 'Completed with errors', 'Sent', 'Failed']
                  description: Lifecycle status. Defaults to "Draft" when omitted. For an always-on incoming campaign (type "Incoming from Unknown Contacts", "Keywords", or "Combined") set this to "Live" so it immediately receives messages and replies to comments. An out-of-set value is rejected with 400.
                language:
                  type: string
                  description: Language the bot replies in
                  examples: ["en"]
                ai_mode:
                  type: boolean
                  description: Whether AI mode is on for this campaign
                booking_provider:
                  type: string
                  enum: [default, zenchef, formitable]
                  description: Which appointment-booking integration the bot uses. "default" uses the built-in booking flow; "zenchef"/"formitable" route to that reservation provider. An out-of-set value is rejected with 400.
                bot:
                  $ref: '#/components/schemas/CampaignBotConfig'
            examples:
              create_campaign:
                summary: Create an outgoing campaign with initial bot config
                value:
                  name: Spring Promo
                  type: Outgoing
                  language: en
                  ai_mode: true
                  bot:
                    instructions: Greet warmly and ask about their goals.
                    goal: Book a discovery call.
      responses:
        '201':
          description: Campaign created successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  campaign_id:
                    type: string
                    description: ID of the newly created campaign
        '400':
          description: Missing or invalid name or type
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["Missing required field: name"]
  /campaigns/{campaignId}:
    get:
      summary: Get Campaign
      security:
        - ApiKeyAuth: []
      description: |
        Returns the full campaign document, including the bot configuration
        (instructions, rules, availability, AI tier, follow-up settings).
        Document reference fields are returned as path strings and timestamps
        as epoch milliseconds.
      tags:
        - Campaigns
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
        - name: campaignId
          in: path
          required: true
          description: ID of the campaign to fetch
          schema:
            type: string
      responses:
        '200':
          description: Campaign retrieved successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  campaign:
                    $ref: '#/components/schemas/CampaignDetail'
        '404':
          description: Campaign not found
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["Campaign not found"]
    put:
      summary: Update Campaign
      security:
        - ApiKeyAuth: []
      description: |
        Partially updates a campaign. Send only the fields you want to change.
        Note that top-level object fields are replaced wholesale; to update
        individual bot fields without overwriting the rest of the bot
        configuration, use the bot-config endpoint instead. The campaign owner
        cannot be changed.
      tags:
        - Campaigns
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
        - name: campaignId
          in: path
          required: true
          description: ID of the campaign to update
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              description: Any subset of campaign fields to update
              additionalProperties: true
              properties:
                name:
                  type: string
                  description: Campaign name
                status:
                  type: string
                  enum: ['Draft', 'Pending Approval', 'Scheduled', 'Live', 'Paused', 'Completed', 'Completed with errors', 'Sent', 'Failed']
                  description: Lifecycle status. Set to "Live" to make an always-on incoming campaign active (required for it to receive messages and reply to comments). An out-of-set value is rejected with 400.
                language:
                  type: string
                  description: Language the bot replies in
                booking_provider:
                  type: string
                  enum: [default, zenchef, formitable]
                  description: Which appointment-booking integration the bot uses. An out-of-set value is rejected with 400.
                enabled_channels:
                  type: array
                  description: Channels this campaign runs on
                  items:
                    type: string
                follow_up_config:
                  type: object
                  description: Follow-up behaviour configuration, stored as provided
                  additionalProperties: true
            examples:
              rename_campaign:
                summary: Rename a campaign
                value:
                  name: Spring Promo v2
      responses:
        '200':
          description: Campaign updated successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  campaign_id:
                    type: string
        '404':
          description: Campaign not found
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["Campaign not found"]
    delete:
      summary: Delete Campaign
      security:
        - ApiKeyAuth: []
      description: Permanently deletes a campaign. This cannot be undone; consider archiving instead if you may need the campaign again.
      tags:
        - Campaigns
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
        - name: campaignId
          in: path
          required: true
          description: ID of the campaign to delete
          schema:
            type: string
      responses:
        '200':
          description: Campaign deleted successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
        '404':
          description: Campaign not found
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["Campaign not found"]
  /campaigns/{campaignId}/duplicate:
    post:
      summary: Duplicate Campaign
      security:
        - ApiKeyAuth: []
      description: |
        Creates a copy of the campaign with all settings preserved. The copy
        starts disabled and its name gets a "(copy)" suffix so it never sends
        messages until you explicitly enable it.
      tags:
        - Campaigns
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
        - name: campaignId
          in: path
          required: true
          description: ID of the campaign to duplicate
          schema:
            type: string
      responses:
        '201':
          description: Campaign duplicated successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  campaign_id:
                    type: string
                    description: ID of the newly created copy
        '404':
          description: Campaign not found
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["Campaign not found"]
  /campaigns/{campaignId}/enabled:
    patch:
      summary: Enable or Disable Campaign
      security:
        - ApiKeyAuth: []
      description: Turns a campaign on or off. A disabled campaign stops engaging contacts but keeps all of its configuration.
      tags:
        - Campaigns
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
        - name: campaignId
          in: path
          required: true
          description: ID of the campaign to toggle
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [enabled]
              properties:
                enabled:
                  type: boolean
                  description: The desired enabled state
            examples:
              enable:
                summary: Enable the campaign
                value:
                  enabled: true
      responses:
        '200':
          description: Enabled state updated successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  campaign_id:
                    type: string
                  enabled:
                    type: boolean
        '400':
          description: Missing or non-boolean enabled field
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["Missing or invalid required field: enabled (boolean)"]
        '404':
          description: Campaign not found
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["Campaign not found"]
  /campaigns/{campaignId}/archived:
    patch:
      summary: Archive or Unarchive Campaign
      security:
        - ApiKeyAuth: []
      description: Archives or restores a campaign. Archived campaigns are hidden from the default campaign list but keep all their data and can be restored at any time.
      tags:
        - Campaigns
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
        - name: campaignId
          in: path
          required: true
          description: ID of the campaign to archive or restore
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [archived]
              properties:
                archived:
                  type: boolean
                  description: true to archive, false to restore
            examples:
              archive:
                summary: Archive the campaign
                value:
                  archived: true
      responses:
        '200':
          description: Archived state updated successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  campaign_id:
                    type: string
                  archived:
                    type: boolean
        '400':
          description: Missing or non-boolean archived field
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["Missing or invalid required field: archived (boolean)"]
        '404':
          description: Campaign not found
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["Campaign not found"]
  /campaigns/{campaignId}/bot-config:
    put:
      summary: Update Bot Configuration
      security:
        - ApiKeyAuth: []
      description: |
        Updates only the bot configuration of a campaign. Each field you send
        is merged into the existing bot configuration, so fields you leave out
        are preserved. This is the safe way to change individual bot settings
        such as instructions or AI speed without affecting the rest.
      tags:
        - Campaigns
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
        - name: campaignId
          in: path
          required: true
          description: ID of the campaign whose bot configuration to update
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CampaignBotConfig'
            examples:
              update_instructions:
                summary: Change the bot instructions and AI speed
                value:
                  instructions: Always answer in a friendly, concise tone.
                  ai_speed: balanced
      responses:
        '200':
          description: Bot configuration updated successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  campaign_id:
                    type: string
        '404':
          description: Campaign not found
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["Campaign not found"]
  /campaigns/{campaignId}/active-hours:
    put:
      summary: Set Bot Active Hours
      security:
        - ApiKeyAuth: []
      description: |
        Sets the bot's availability schedule for the campaign. Outside the
        configured windows the bot does not reply automatically. Each weekday
        accepts a single time window or an array of windows in 24-hour HH:MM
        format.
      tags:
        - Campaigns
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
        - name: campaignId
          in: path
          required: true
          description: ID of the campaign whose schedule to set
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [availability]
              properties:
                availability:
                  $ref: '#/components/schemas/AvailabilitySchedule'
            examples:
              weekday_hours:
                summary: Weekday business hours
                value:
                  availability:
                    monday:
                      start_time: "09:00"
                      end_time: "17:00"
                    tuesday:
                      start_time: "09:00"
                      end_time: "17:00"
      responses:
        '200':
          description: Active hours updated successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  campaign_id:
                    type: string
        '400':
          description: Missing availability object or unknown weekday keys
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["Missing required field: availability (object with weekday keys)"]
  /campaigns/{campaignId}/custom-functions:
    get:
      summary: List Campaign Custom Functions
      security:
        - ApiKeyAuth: []
      description: |
        Returns the custom functions linked to this campaign, resolved into
        full definitions. Custom functions are external HTTP actions the bot
        can call during a conversation, for example checking stock in your
        store or creating a record in your CRM.
      tags:
        - Campaigns
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
        - name: campaignId
          in: path
          required: true
          description: ID of the campaign whose custom functions to list
          schema:
            type: string
      responses:
        '200':
          description: Custom functions retrieved successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  custom_functions:
                    type: array
                    items:
                      $ref: '#/components/schemas/CampaignCustomFunction'
        '404':
          description: Campaign not found
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["Campaign not found"]
    post:
      summary: Link Campaign Custom Function
      security:
        - ApiKeyAuth: []
      description: |
        Links a custom function (an external HTTP action the bot can call during a conversation) to
        a campaign by ID. The linked custom function must already exist in the account's own custom
        functions collection — this endpoint only creates the link, not the function itself (see
        POST /custom-functions to create one). Linking an already-linked function is a harmless
        no-op. To unlink, use DELETE /campaigns/{campaignId}/custom-functions/{customFunctionId}.
      tags:
        - Campaigns
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
        - name: campaignId
          in: path
          required: true
          description: ID of the campaign
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [custom_function_id]
              properties:
                custom_function_id:
                  type: string
                  description: ID of the custom function to link. Letters, numbers, underscores, and hyphens only.
      responses:
        '200':
          description: Custom function linked successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  campaign_id:
                    type: string
                  custom_function_id:
                    type: string
        '400':
          description: Missing campaignId or custom_function_id, or a custom_function_id containing characters other than letters, numbers, underscores, and hyphens
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
        '404':
          description: Campaign not found
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["Campaign not found"]
  /phone-numbers:
    get:
      summary: List Phone Numbers
      security:
        - ApiKeyAuth: []
      description: |
        Lists every phone number and messaging identity connected to your
        account: purchased WhatsApp numbers, WhatsApp Web numbers, Telegram
        accounts, LINE official accounts, and bring-your-own SMS numbers.

        Results are ordered by phone number and paginated with an opaque
        cursor. When a type filter is applied, a page may contain fewer than
        limit entries while more records exist - keep following next_cursor
        until it is null.

        Provider credentials, internal resource identifiers, and billing
        details are never included in the response.
      tags:
        - Phone Numbers
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
        - name: limit
          in: query
          required: false
          description: Maximum records per page (1-100). Defaults to 50.
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 50
        - name: cursor
          in: query
          required: false
          description: Opaque pagination cursor from a previous response's next_cursor field.
          schema:
            type: string
        - name: type
          in: query
          required: false
          description: Filter by connection type.
          schema:
            type: string
            enum: [twilio, whatsapp_web, telegram, line, sms]
      responses:
        '200':
          description: The account's phone numbers
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PhoneNumberListResponse'
              example:
                success: true
                phone_numbers:
                  - id: "+31612345678"
                    phone_number: "+31612345678"
                    display_name: "Support line"
                    type: "whatsapp_web"
                    channel: "whatsapp_web"
                    is_active: true
                    follow_ups_disabled: false
                    whatsapp_web:
                      status: "open"
                      phone_country: "nl"
                next_cursor: null
        '400':
          description: Invalid limit, cursor, or type filter
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChannelsErrorResponse'
              example:
                success: false
                error: "Invalid type filter. Allowed values: twilio, whatsapp_web, telegram, line, sms"
    post:
      summary: Purchase Phone Number
      security:
        - ApiKeyAuth: []
      description: |
        Purchases a WhatsApp-capable phone number for your account, using a
        number returned by the available-numbers search. Credits are deducted
        on purchase and are not refunded on release.

        The number is provisioned in the PURCHASED state. WhatsApp sender
        registration then proceeds asynchronously (PURCHASED to PENDING to
        ONLINE); poll GET /phone-numbers/{phoneNumber}/status until the status
        reaches ONLINE before sending.
      tags:
        - Phone Numbers
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PurchasePhoneNumberRequest'
            example:
              phone_number: "+14155551234"
              country_code: "US"
              display_name: "Support line"
      responses:
        '201':
          description: The number was purchased and provisioning has started.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PurchasePhoneNumberResponse'
              example:
                success: true
                phone_number: "+14155551234"
                channel: "whatsapp"
                whatsapp_status: "PURCHASED"
                outgoing_status: "PURCHASED"
                status: "PURCHASED"
                purchase_credits: 11.5
                monthly_credits: 11.5
        '400':
          description: Missing or invalid fields, or the number could not be provisioned (e.g. a required business address is missing).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PhoneNumberPurchaseErrorResponse'
              example:
                success: false
                error: "Missing required field: phone_number"
        '401':
          description: Authentication required.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PhoneNumberPurchaseErrorResponse'
        '403':
          description: The account's plan does not permit purchasing a number, or a daily purchase cap was reached.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PhoneNumberPurchaseErrorResponse'
        '404':
          description: The referenced managed account could not be resolved.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PhoneNumberPurchaseErrorResponse'
        '409':
          description: The number is no longer available, is in cooldown, or there are insufficient credits.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PhoneNumberPurchaseErrorResponse'
              example:
                success: false
                error_code: 409
                reason: number_unavailable
                error: "That number is no longer available. Please search again and pick another."
        '500':
          description: An unexpected error occurred.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PhoneNumberPurchaseErrorResponse'
  /phone-numbers/{phoneNumber}:
    get:
      summary: Get Phone Number
      security:
        - ApiKeyAuth: []
      description: |
        Returns a single phone number record by its E.164 number (for LINE
        connections, use the official account's bot user id). A number that
        does not belong to your account returns 404.

        Note that campaign-to-number bindings are not stored on the number
        record; to see which number a conversation uses, read the from and to
        fields on the conversation's messages.
      tags:
        - Phone Numbers
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
        - name: phoneNumber
          in: path
          required: true
          description: The phone number in E.164 format. A missing leading plus sign is tolerated.
          schema:
            type: string
          example: "+31612345678"
      responses:
        '200':
          description: The phone number record
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PhoneNumberDetailResponse'
              example:
                success: true
                phone_number:
                  id: "+31612345678"
                  phone_number: "+31612345678"
                  display_name: "Support line"
                  type: "whatsapp_web"
                  channel: "whatsapp_web"
                  is_active: true
        '404':
          description: The number does not exist on this account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChannelsErrorResponse'
              example:
                success: false
                error: "Phone number not found"
    patch:
      summary: Update Phone Number
      security:
        - ApiKeyAuth: []
      description: |
        Updates display-layer settings on a phone number. Writable fields are
        display_name, category, and follow_ups_disabled - connection state,
        registration status, and billing fields cannot be changed through the
        API. Returns the updated record.
      tags:
        - Phone Numbers
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
        - name: phoneNumber
          in: path
          required: true
          description: The phone number in E.164 format.
          schema:
            type: string
          example: "+31612345678"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PhoneNumberUpdateRequest'
            example:
              display_name: "Sales line"
              follow_ups_disabled: true
      responses:
        '200':
          description: The updated phone number record
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PhoneNumberDetailResponse'
              example:
                success: true
                phone_number:
                  id: "+31612345678"
                  phone_number: "+31612345678"
                  display_name: "Sales line"
                  follow_ups_disabled: true
        '400':
          description: No updatable fields provided or a field failed validation
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChannelsErrorResponse'
              example:
                success: false
                error: "No updatable fields provided. Allowed fields: display_name, category, follow_ups_disabled"
        '404':
          description: The number does not exist on this account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChannelsErrorResponse'
              example:
                success: false
                error: "Phone number not found"
    delete:
      summary: Release Phone Number
      security:
        - ApiKeyAuth: []
      description: |
        Releases a phone number from your account. Provider-side cleanup
        (de-registration, number release, billing task cancellation) is
        performed server-side. A 7-day cooldown is applied to the released
        number, during which it cannot be repurchased by any account. No
        credits are refunded.
      tags:
        - Phone Numbers
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: phoneNumber
          in: path
          required: true
          description: The phone number in E.164 format. A missing leading plus sign is tolerated.
          schema:
            type: string
          example: "+14155551234"
        - name: sub_account_id
          in: query
          required: false
          description: Optional. Release a number belonging to a managed account you administer.
          schema:
            type:
              - string
              - "null"
      responses:
        '200':
          description: The number was released.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ReleasePhoneNumberResponse'
              example:
                success: true
                phone_number: "+14155551234"
                released: true
        '400':
          description: Missing phone number parameter, or provider credentials are unavailable.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PhoneNumberPurchaseErrorResponse'
              example:
                success: false
                error: "Missing required parameter: phoneNumber"
        '403':
          description: The caller is not permitted to release this number.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PhoneNumberPurchaseErrorResponse'
        '404':
          description: The number does not exist on this account.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PhoneNumberPurchaseErrorResponse'
              example:
                success: false
                error_code: 404
                reason: phone_number_not_found
                error: "Phone number not found"
        '500':
          description: An unexpected error occurred.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PhoneNumberPurchaseErrorResponse'
  /phone-numbers/{phoneNumber}/status:
    get:
      summary: Get Phone Number Status
      security:
        - ApiKeyAuth: []
      description: |
        Returns the connection status for a phone number. Registered WhatsApp
        numbers are checked live against the messaging provider, and WhatsApp
        Web numbers are checked live against their connection session when
        reachable; Telegram, LINE, and SMS numbers report their stored state.

        The live flag tells you whether the returned status is a real-time
        result (true) or the last stored state (false). Live results are also
        persisted to the stored record, so repeated polling keeps the record
        fresh.
      tags:
        - Phone Numbers
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
        - name: phoneNumber
          in: path
          required: true
          description: The phone number in E.164 format.
          schema:
            type: string
          example: "+31612345678"
      responses:
        '200':
          description: The connection status
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PhoneNumberStatusResponse'
              example:
                success: true
                phone_number: "+31612345678"
                channel: "whatsapp_web"
                status: "open"
                status_reason: null
                live: true
        '404':
          description: The number does not exist on this account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChannelsErrorResponse'
              example:
                success: false
                error: "Phone number not found"
  /channels:
    get:
      summary: List Channel Connections
      security:
        - ApiKeyAuth: []
      description: |
        Returns a connection summary for every messaging channel on your
        account: WhatsApp, WhatsApp Web, SMS, Telegram, LINE, Instagram, and
        Messenger. Each channel reports whether it is connected plus the
        numbers, accounts, or page powering it. Ideal for a channel-health
        dashboard widget.
      tags:
        - Channels
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
      responses:
        '200':
          description: Per-channel connection summary
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChannelsSummaryResponse'
              example:
                success: true
                channels:
                  whatsapp:
                    connected: true
                    numbers:
                      - phone_number: "+15551234567"
                        display_name: "Main line"
                        status: "ONLINE"
                  whatsapp_web:
                    connected: true
                    numbers:
                      - phone_number: "+31612345678"
                        display_name: "Support line"
                        status: "open"
                  sms:
                    connected: false
                    numbers: []
                  telegram:
                    connected: false
                    accounts: []
                  line:
                    connected: false
                    accounts: []
                  instagram:
                    connected: true
                    page_id: "1234567890"
                    page_name: "My Business Page"
                    instagram_business_account_id: "17890000000000000"
                  messenger:
                    connected: true
                    page_id: "1234567890"
                    page_name: "My Business Page"
        '401':
          description: Missing or invalid authentication
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChannelsErrorResponse'
              example:
                success: false
                error: "Invalid API key"
  /channels/campaign:
    post:
      summary: Route Channels To Campaign
      security:
        - ApiKeyAuth: []
      description: |
        Routes new, unknown incoming contacts on one or more channels to a
        campaign, so the campaign's assistant automatically answers first-time
        messages on those channels. The campaign must belong to your account
        and must be of type Incoming from Unknown Contacts or Combined.

        The account's per-channel routing map and the campaign's enabled
        channel list are updated together, atomically. Channels already routed
        to a different campaign are re-pointed to this one.
      tags:
        - Channels
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ChannelCampaignAssignRequest'
            example:
              campaign_id: "abc123campaign"
              channels: ["whatsapp", "instagram"]
      responses:
        '200':
          description: Channels routed to the campaign
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChannelCampaignAssignResponse'
              example:
                success: true
                campaign_id: "abc123campaign"
                channels: ["whatsapp", "instagram"]
        '400':
          description: Missing campaign_id, invalid channels, or the campaign type cannot handle incoming routing
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChannelsErrorResponse'
              example:
                success: false
                error: "channels contains an invalid entry. Allowed: whatsapp, whatsapp_web, telegram, instagram, messenger, chat_widget, custom_channel, sms"
        '404':
          description: The campaign does not exist on this account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChannelsErrorResponse'
              example:
                success: false
                error: "Campaign not found"
  /channels/campaign/{channel}:
    delete:
      summary: Clear Channel Campaign Routing
      security:
        - ApiKeyAuth: []
      description: |
        Removes the incoming-campaign routing for a single channel: new,
        unknown contacts on that channel will no longer be answered by a
        campaign. The previously routed campaign's enabled channel list is
        kept in sync automatically.

        The call is idempotent - clearing a channel that has no campaign
        routed succeeds and reports cleared as false.
      tags:
        - Channels
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
        - name: channel
          in: path
          required: true
          description: The channel to clear.
          schema:
            type: string
            enum: [whatsapp, whatsapp_web, telegram, instagram, messenger, chat_widget, custom_channel, sms]
          example: "whatsapp"
      responses:
        '200':
          description: Routing cleared (or nothing was routed)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChannelCampaignClearResponse'
              example:
                success: true
                channel: "whatsapp"
                cleared: true
                campaign_id: "abc123campaign"
        '400':
          description: Invalid channel
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChannelsErrorResponse'
              example:
                success: false
                error: "Invalid channel. Allowed: whatsapp, whatsapp_web, telegram, instagram, messenger, chat_widget, custom_channel, sms"
  /contacts/bulk-flag:
    post:
      summary: Bulk Set Contact Flag
      security:
        - ApiKeyAuth: []
      description: |
        Set one boolean flag on many contacts at once. Supported flags are
        bot_active (AI assistant on or off), dnd (pause automated outreach),
        spam, private, important (the star marker), and mark_chat_closed
        (archive or un-archive the chat). Contacts that do not exist on your
        account are skipped and counted in the skipped total. Up to 500 IDs
        per request.
      tags:
        - Contacts
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - contactIds
                - field
                - value
              properties:
                contactIds:
                  type: array
                  items:
                    type: string
                  maxItems: 500
                  description: IDs of the contacts to update.
                field:
                  type: string
                  enum: [spam, bot_active, dnd, private, important, mark_chat_closed]
                  description: Which flag to set.
                value:
                  type: boolean
                  description: The value to set the flag to.
            examples:
              pauseBot:
                summary: Turn the AI assistant off for two contacts
                value:
                  contactIds: ["contactId1", "contactId2"]
                  field: "bot_active"
                  value: false
      responses:
        '200':
          description: Update result
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  updated:
                    type: integer
                    description: Number of contacts updated.
                  skipped:
                    type: integer
                    description: Number of IDs skipped because they were not found on this account.
        '400':
          description: Invalid contactIds, unknown field, or non-boolean value
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContactsApiError'
  /contacts/bulk-assign-agent:
    post:
      summary: Bulk Assign Contacts to AI Agent
      security:
        - ApiKeyAuth: []
      description: |
        Hands MANY conversations to a different AI Agent in one call, or clears their
        assignment with agentId null. A pure routing change: no message is sent and
        the agent does not reply to anyone - it simply answers each contact's next
        message. There is deliberately no triggerAIResponse here.

        Select the contacts ONE way: contactIds (up to 500 explicit ids), filter (the
        same keys as GET /contacts/count: agentId or none, channel, tag name, listId,
        botActive, status) or rules (the smart-list vocabulary). A filter or rules
        selection is resolved on the server, newest contact first, limit contacts at a
        time (default and maximum 500). The response says how many matched in total
        and how many still remain after this call - with a filter that stops matching
        once a contact has moved (for example filter agentId = the OLD agent), call
        again until remaining is 0 to move everyone. Always count first with
        GET /contacts/count and confirm the number before moving a whole group.
      tags:
        - Contacts
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - agentId
              properties:
                agentId:
                  type: ["string", "null"]
                  description: The AI Agent that should answer these contacts from now on, or null to clear the assignment.
                contactIds:
                  type: array
                  items:
                    type: string
                  maxItems: 500
                  description: Explicit contact ids to move (max 500). Use exactly one of contactIds, filter or rules.
                filter:
                  type: object
                  description: Select contacts by filter (server-side). At least one key is required.
                  properties:
                    agentId:
                      type: string
                      description: Contacts currently assigned to this agent, or none for contacts with no assigned agent.
                    channel:
                      type: string
                      description: Contacts on this channel.
                    tag:
                      type: string
                      description: Contacts carrying this tag NAME.
                    listId:
                      type: string
                      description: Contacts on this list.
                    botActive:
                      type: boolean
                      description: Contacts whose AI assistant is on (true) or off (false).
                    status:
                      type: string
                      description: Contacts with this status.
                rules:
                  type: object
                  description: A smart-list rules object (match + conditions), same shape as smart_rules on lists.
                limit:
                  type: integer
                  minimum: 1
                  maximum: 500
                  default: 500
                  description: How many of the matching contacts to move in this call (filter or rules selections only).
            examples:
              moveEveryoneOffOldAgent:
                summary: Move all Messenger contacts still on the old agent
                value:
                  agentId: "agent_xyz789"
                  filter:
                    agentId: "agent_abc123"
                    channel: "messenger"
              explicitIds:
                summary: Move two contacts by id
                value:
                  agentId: "agent_xyz789"
                  contactIds: ["contactId1", "contactId2"]
      responses:
        '200':
          description: What was moved
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  agentId:
                    type: ["string", "null"]
                  matched:
                    type: integer
                    description: How many contacts the selection found in total.
                  updated:
                    type: integer
                    description: How many were moved in this call.
                  skipped:
                    type: integer
                    description: Ids that were not found on this account.
                  remaining:
                    type: integer
                    description: How many contacts still match the selection after this call (always 0 for contactIds).
                  filters:
                    type: object
                    description: The filters that were applied (filter or rules selections).
              example:
                success: true
                agentId: agent_xyz789
                matched: 3415
                updated: 500
                skipped: 0
                remaining: 2915
                filters:
                  agentId: agent_abc123
                  channel: messenger
        '400':
          description: Missing or invalid agentId, not exactly one selection, an empty filter, an invalid limit, or malformed rules
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContactsApiError'
        '404':
          description: The agent, tag name or listId does not exist on this account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContactsApiError'
  /contacts/import:
    post:
      summary: Import Contacts
      security:
        - ApiKeyAuth: []
      description: |
        Create up to 500 contacts in one call from a JSON array. Each record
        needs a phone_number in international format; first_name, last_name,
        email, channel, lead_profile, custom_fields, and tags are optional.
        Records with invalid phone numbers, unsupported channels, or phone
        numbers that already exist on your account are skipped, and every
        skipped record is reported with its index and reason so you can retry
        just the failures. Set updateExisting to true to update contacts that
        already exist instead of skipping them - handy when you re-import the
        same file to refresh names, emails, or custom fields. Tags are given by
        name; any tag that does not exist yet is created and then added to the
        contact. Optionally assign every contact from this import to a list with
        listId. Imports stop early if your plan's contact limit is reached; the
        remaining records are reported as skipped.
      tags:
        - Contacts
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - contacts
              properties:
                contacts:
                  type: array
                  maxItems: 500
                  description: Contact records to create.
                  items:
                    type: object
                    required:
                      - phone_number
                    properties:
                      phone_number:
                        type: string
                        description: Phone number in international format. A leading plus sign is added when missing.
                      first_name:
                        type: string
                        description: First name.
                      last_name:
                        type: string
                        description: Last name.
                      email:
                        type: string
                        description: Email address.
                      channel:
                        type: string
                        enum: [whatsapp, sms, whatsapp_web]
                        description: Messaging channel for this contact. Defaults to the request-level defaultChannel.
                      is_bot_active:
                        type: boolean
                        description: Whether the AI assistant responds to this contact. Defaults to true.
                      is_private:
                        type: boolean
                        description: Whether the contact is private. Defaults to false.
                      lead_profile:
                        type: string
                        description: Free-text lead profile notes.
                      custom_fields:
                        type: object
                        description: Map of custom field keys to values.
                      tags:
                        oneOf:
                          - type: array
                            maxItems: 25
                            items:
                              type: string
                              maxLength: 60
                          - type: string
                        description: >-
                          Tag names to add to this contact (not tag IDs). Either an array of names
                          or one string separated by commas, semicolons, or pipes. Matching is
                          case-insensitive; a tag that does not exist yet is created. Tags are only
                          added, never removed. Up to 25 tags per record, 60 characters per name -
                          anything beyond that is ignored.
                listId:
                  type: string
                  description: Optional list to add every contact from this import to (created and updated alike). Must be a list on your account.
                defaultChannel:
                  type: string
                  enum: [whatsapp, sms, whatsapp_web]
                  description: Channel applied to records that omit channel. Defaults to whatsapp.
                updateExisting:
                  type: boolean
                  default: false
                  description: >-
                    When true, a record whose phone number already exists on your account updates
                    that contact instead of being skipped as a duplicate. Only the fields present in
                    the record are written (first_name, last_name, email, lead_profile, and
                    custom_fields merged key by key); channel, is_bot_active, is_private, and the
                    phone number are never changed. Updated contacts are reported in updated and
                    updated_contact_ids, not in skipped.
            examples:
              twoContacts:
                summary: Import two contacts into a list
                value:
                  contacts:
                    - phone_number: "+12025551234"
                      first_name: "Ann"
                      last_name: "Lee"
                      tags: ["vip", "newsletter"]
                    - phone_number: "+12025551235"
                      first_name: "Bob"
                      tags: "lead, eu"
                  listId: "list123"
                  defaultChannel: "whatsapp_web"
              refreshExisting:
                summary: Re-import a file to update contacts you already have
                value:
                  updateExisting: true
                  contacts:
                    - phone_number: "+12025551234"
                      first_name: "Ann"
                      email: "ann@example.com"
                      custom_fields:
                        plan: "gold"
                      tags: ["vip"]
      responses:
        '201':
          description: Import result with created IDs and per-record skip reasons
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  imported:
                    type: integer
                    description: Number of contacts created.
                  contact_ids:
                    type: array
                    items:
                      type: string
                    description: IDs of the newly created contacts, in input order.
                  updated:
                    type: integer
                    description: Number of existing contacts updated (always 0 unless updateExisting is true).
                  updated_contact_ids:
                    type: array
                    items:
                      type: string
                    description: IDs of the existing contacts that were updated, in input order.
                  skipped:
                    type: array
                    items:
                      $ref: '#/components/schemas/ContactImportSkippedRecord'
                    description: Records that were neither created nor updated, with the reason for each.
        '403':
          description: Your plan's contact limit does not allow this many new contacts
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContactsApiError'
  /contacts/import-csv:
    post:
      summary: Start CSV Contact Import
      security:
        - ApiKeyAuth: []
      description: |
        Imports contacts from a CSV file that has already been uploaded to your
        DM Champ file storage. You do NOT post the file here: upload the CSV first
        (the same upload the in-app importer uses), then send its storage path as
        csvStoragePath.

        This starts a background job and answers immediately with 202 and a job_id -
        it does not wait for the import. Poll the import job status endpoint with
        that job_id until status is completed or failed. Built for large files: up
        to roughly 50,000 rows per job. For a handful of contacts sent as JSON, use
        the bulk import endpoint instead.
      tags:
        - Contacts
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                csvStoragePath:
                  type: string
                  description: >-
                    Path of the already-uploaded CSV in your DM Champ file storage.
                  examples: ["users/abc123/contact_imports/leads-may.csv"]
                listName:
                  type: string
                  description: Name of a NEW list to create and put every imported contact into.
                existingListRefs:
                  type: array
                  items:
                    type: string
                  description: IDs of existing lists to add every imported contact to.
                defaultChannel:
                  type: string
                  description: >-
                    Channel to give rows whose channel is not set in the file, for
                    example whatsapp.
            examples:
              csv_import:
                summary: Import a CSV into a new list
                value:
                  csvStoragePath: "users/abc123/contact_imports/leads-may.csv"
                  listName: "May leads"
      responses:
        '202':
          description: The import job was queued
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  job_id:
                    type: string
                    description: Pass this to the import job status endpoint.
                  status:
                    type: string
                    examples: ["queued"]
        '400':
          description: Invalid or missing csvStoragePath
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContactsApiError'
  /contacts/import-csv/{jobId}:
    get:
      summary: Get Contact Import Job Status
      security:
        - ApiKeyAuth: []
      description: |
        Reports how a CSV contact import is going: whether it is still running, and
        how many contacts were created, updated and skipped. Poll this after
        starting an import until status is completed or failed.
      tags:
        - Contacts
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
        - name: jobId
          in: path
          required: true
          schema:
            type: string
          description: The job_id returned when the import was started.
      responses:
        '200':
          description: The job's current state
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  job_id:
                    type: string
                  status:
                    type: string
                    description: queued, processing, completed or failed.
                  imported:
                    type: integer
                    description: Contacts created.
                  updated:
                    type: integer
                    description: Existing contacts updated.
                  skipped:
                    type: integer
                    description: Rows skipped, for example duplicates or unusable phone numbers.
                  errors:
                    type: array
                    description: Per-row problems, if any.
                    items:
                      type: string
                  error_message:
                    type: ["string", "null"]
                    description: Why the whole job failed, when status is failed.
        '404':
          description: No such import job on this account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContactsApiError'
  /contacts/export:
    post:
      summary: Start Contacts Export
      security:
        - ApiKeyAuth: []
      description: |
        Exports contacts to a CSV file. Send nothing to export every contact, a
        listId to export one list, or contactIds to export a specific set.

        This starts a background job and answers immediately with 202 and a job_id.
        Poll the export job status endpoint until status is completed, then use the
        export_id it returns to fetch a fresh download link (links are short lived
        and minted on request, never stored in the job).
      tags:
        - Contacts
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                listId:
                  type: string
                  description: Export only the contacts in this list.
                contactIds:
                  type: array
                  items:
                    type: string
                  description: Export only these contacts.
            examples:
              whole_account:
                summary: Export every contact
                value: {}
              one_list:
                summary: Export one list
                value:
                  listId: "list123"
      responses:
        '202':
          description: The export job was queued
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  job_id:
                    type: string
                    description: Pass this to the export job status endpoint.
                  status:
                    type: string
                    examples: ["queued"]
  /contacts/export/{jobId}:
    get:
      summary: Get Contacts Export Job Status
      security:
        - ApiKeyAuth: []
      description: |
        Reports how a contacts export is going and, once it is finished, gives you
        the export_id you need to request a download link.
      tags:
        - Contacts
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
        - name: jobId
          in: path
          required: true
          schema:
            type: string
          description: The job_id returned when the export was started.
      responses:
        '200':
          description: The job's current state
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  job_id:
                    type: string
                  status:
                    type: string
                    description: queued, processing, completed or failed.
                  export_id:
                    type: ["string", "null"]
                    description: Use this to request a download link once status is completed.
                  contact_count:
                    type: integer
                    description: How many contacts were written to the file.
                  error_message:
                    type: ["string", "null"]
                    description: Why the job failed, when status is failed.
        '404':
          description: No such export job on this account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContactsApiError'
  /contacts/custom-fields/{fieldKey}:
    delete:
      summary: Delete Custom Field From All Contacts
      security:
        - ApiKeyAuth: []
      description: |
        Remove one custom field key from every contact on your account. Use this
        to clean up after renaming or retiring a custom field. Returns how many
        contacts were updated. This cannot be undone.
      tags:
        - Contacts
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
        - name: fieldKey
          in: path
          required: true
          schema:
            type: string
            pattern: '^[A-Za-z0-9_-]+$'
          description: The custom field key to remove. Letters, numbers, underscores, and hyphens only.
      responses:
        '200':
          description: Removal result
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  updated:
                    type: integer
                    description: Number of contacts the field was removed from.
        '400':
          description: Field key contains unsupported characters
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContactsApiError'
  /contacts/{contactId}/tags:
    post:
      summary: Add Or Remove Contact Tags
      security:
        - ApiKeyAuth: []
      description: |
        Add tags to and/or remove tags from a contact in one call. Pass tag IDs
        or tag NAMES in addTagIds and removeTagIds; at least one of the two is
        required. Names are matched case-insensitively against your account's
        tags. All referenced tags must already exist on your account — create
        them first through the tags endpoint; this endpoint never creates one.
        Returns 404 when the contact or any referenced tag does not exist.
      tags:
        - Contacts
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
        - name: contactId
          in: path
          required: true
          schema:
            type: string
          description: ID of the contact whose tags to change.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                addTagIds:
                  type: array
                  items:
                    type: string
                  description: IDs or names of tags to add to the contact. Names are matched case-insensitively; the tag must already exist.
                removeTagIds:
                  type: array
                  items:
                    type: string
                  description: IDs or names of tags to remove from the contact. Names are matched case-insensitively; the tag must already exist.
            examples:
              swap:
                summary: Add one tag and remove another
                value:
                  addTagIds: ["tagHotLead"]
                  removeTagIds: ["tagColdLead"]
              byName:
                summary: Add and remove by tag name
                value:
                  addTagIds: ["hot lead"]
                  removeTagIds: ["cold lead"]
      responses:
        '200':
          description: Tag update result
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  contact_id:
                    type: string
                    description: ID of the updated contact.
                  added:
                    type: integer
                    description: Number of tags added.
                  removed:
                    type: integer
                    description: Number of tags removed.
        '404':
          description: Contact or one of the referenced tags not found on this account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContactsApiError'
  /faqs:
    get:
      summary: List FAQs
      security:
        - ApiKeyAuth: []
      description: |
        Returns the FAQs in your account, newest first. Optionally filter to a single AI Agent's
        knowledge base (agent_id) or by active state. Use the limit and cursor parameters to page
        through large libraries: pass the next_cursor value from one response as the cursor parameter
        of the next request.
      tags:
        - FAQs
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
        - name: agent_id
          in: query
          required: false
          description: Only return FAQs in this AI Agent's knowledge base. Also accepted as agentId. Cannot be combined with campaign_id.
          schema:
            type: string
        - name: campaign_id
          in: query
          required: false
          description: Legacy; prefer agent_id. Only return FAQs linked to this campaign.
          schema:
            type: string
        - name: is_active
          in: query
          required: false
          description: Only return FAQs with this active state (true or false). Filtering is applied per page, so pages may contain fewer items than the limit.
          schema:
            type: boolean
        - name: limit
          in: query
          required: false
          description: Maximum number of FAQs per page (default 50, max 100)
          schema:
            type: integer
            minimum: 1
            maximum: 100
        - name: cursor
          in: query
          required: false
          description: FAQ ID to continue after, taken from the next_cursor field of the previous page
          schema:
            type: string
      responses:
        '200':
          description: FAQs retrieved successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  faqs:
                    type: array
                    items:
                      $ref: '#/components/schemas/Faq'
                  next_cursor:
                    type: ['string', 'null']
                    description: Pass this value as the cursor parameter to fetch the next page. Null when there are no more results.
                    examples: ["aBcD1234eFgH5678"]
        '400':
          description: Both agent_id and campaign_id were supplied, or a paging parameter was invalid
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["Send either campaign_id or agent_id, not both."]
        '404':
          description: The supplied agent_id or campaign_id does not match anything on your account
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["Agent not found"]
    post:
      summary: Create FAQ
      security:
        - ApiKeyAuth: []
      description: |
        Creates a new FAQ. Pass agent_id to add it straight to that AI Agent's knowledge base;
        campaign_id is the legacy equivalent and is still accepted; send neither to create a global
        FAQ that is not attached to anything yet. Sending both is rejected.

        The AI assistant starts using the new entry automatically once its search data has been
        prepared in the background (usually within seconds).
      tags:
        - FAQs
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/FaqCreateRequest'
            examples:
              create_faq_for_agent:
                summary: Create a shipping FAQ in an agent's knowledge base
                value:
                  agent_id: "ag7HkQ2ZpLxR3mNb"
                  question: "How long does shipping take?"
                  answer: "Standard shipping takes 3-5 business days."
                  category: "shipping"
                  tags: ["logistics"]
              create_faq:
                summary: Legacy - create a shipping FAQ on a campaign
                value:
                  campaign_id: "campaign123"
                  question: "How long does shipping take?"
                  answer: "Standard shipping takes 3-5 business days."
      responses:
        '201':
          description: FAQ created successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  faq_id:
                    type: string
                    description: ID of the newly created FAQ
                    examples: ["aBcD1234eFgH5678"]
                  agent_id:
                    type: string
                    description: Present when agent_id was supplied - the agent the FAQ was added to
                    examples: ["ag7HkQ2ZpLxR3mNb"]
        '400':
          description: A required field is missing or invalid, or both agent_id and campaign_id were supplied
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["Missing required field: question"]
        '404':
          description: The supplied agent_id does not match any of your agents
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["Agent not found"]

  /faqs/bulk-delete:
    post:
      summary: Bulk Delete FAQs
      security:
        - ApiKeyAuth: []
      description: |
        Deletes up to 500 FAQs in a single request. When agent_id is supplied, the deleted FAQs are
        also removed from that AI Agent's knowledge base first, so the agent is not left pointing at
        entries that no longer exist. campaign_id is the legacy equivalent. Sending both is rejected.
      tags:
        - FAQs
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - faq_ids
              properties:
                faq_ids:
                  type: array
                  description: IDs of the FAQs to delete (max 500)
                  items:
                    type: string
                  examples: [["faqId1", "faqId2"]]
                agent_id:
                  type: string
                  description: Also remove the deleted FAQs from this AI Agent's knowledge base. Also accepted as agentId.
                  examples: ["ag7HkQ2ZpLxR3mNb"]
                campaign_id:
                  type: string
                  description: Legacy; prefer agent_id. Also remove the deleted FAQs from this campaign's FAQ list.
                  examples: ["campaign123"]
      responses:
        '200':
          description: FAQs deleted successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  deleted_count:
                    type: integer
                    description: Number of FAQ IDs processed
                    examples: [2]
        '400':
          description: faq_ids is missing, empty, or contains invalid entries, or both agent_id and campaign_id were supplied
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["faq_ids must be a non-empty array of FAQ IDs"]
        '404':
          description: The supplied agent_id does not match any of your agents
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["Agent not found"]

  /faqs/import:
    post:
      summary: Import FAQs
      security:
        - ApiKeyAuth: []
      description: |
        Bulk-imports up to 500 FAQs at once. Items whose question matches an existing FAQ in your
        library (case-insensitive) update that FAQ instead of creating a duplicate. Large FAQ
        libraries make duplicate matching slower, so prefer fewer, larger imports over many small ones.

        Supply exactly one of agent_id or campaign_id. Bulk import is the one FAQ operation that
        still runs on a campaign behind the scenes, so an agent that has no campaign behind it
        answers 409 - add those FAQs one at a time with POST /faqs and agent_id instead.
      tags:
        - FAQs
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - faqs
              properties:
                agent_id:
                  type: string
                  description: AI Agent to import the FAQs into. Also accepted as agentId. Send this or campaign_id, not both.
                  examples: ["ag7HkQ2ZpLxR3mNb"]
                campaign_id:
                  type: string
                  description: Legacy; prefer agent_id. Campaign to link all imported FAQs to.
                  examples: ["campaign123"]
                faqs:
                  type: array
                  description: FAQ items to import (max 500)
                  items:
                    $ref: '#/components/schemas/FaqImportItem'
            examples:
              import_faqs:
                summary: Import two FAQs
                value:
                  campaign_id: "campaign123"
                  faqs:
                    - question: "Do you ship internationally?"
                      answer: "Yes, we ship to most countries worldwide."
                    - question: "What is your return policy?"
                      answer: "You can return any item within 30 days."
      responses:
        '200':
          description: FAQs imported successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  faq_ids:
                    type: array
                    description: IDs of the created or updated FAQs, in the order they were supplied
                    items:
                      type: string
                  imported_count:
                    type: integer
                    description: Number of FAQs created or updated
                    examples: [2]
                  agent_id:
                    type: string
                    description: Present when agent_id was supplied
                    examples: ["ag7HkQ2ZpLxR3mNb"]
                  campaign_id:
                    type: string
                    description: Present when agent_id was supplied - the campaign behind that agent that carried the import
                    examples: ["campaign123"]
        '400':
          description: The payload is missing required fields or contains an invalid item, or both agent_id and campaign_id were supplied
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["faqs[1] must include a non-empty question and answer"]
        '404':
          description: The supplied agent_id does not match any of your agents
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["Agent not found"]
        '409':
          description: This agent has no campaign behind it, so a bulk import cannot run for it
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  reason:
                    type: string
                    examples: ["no_attached_campaign"]
                  error:
                    type: string

  /faqs/reorder:
    post:
      summary: Reorder FAQs
      security:
        - ApiKeyAuth: []
      description: |
        Sets the display order of an AI Agent's FAQs. Supply the full list of FAQ IDs in the desired
        order; each FAQ's position is updated to match its place in the array.

        Supply exactly one of agent_id or campaign_id.
      tags:
        - FAQs
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - ordered_faq_ids
              properties:
                agent_id:
                  type: string
                  description: AI Agent whose FAQs are being reordered. Also accepted as agentId. Send this or campaign_id, not both.
                  examples: ["ag7HkQ2ZpLxR3mNb"]
                campaign_id:
                  type: string
                  description: Legacy; prefer agent_id. Campaign whose FAQs are being reordered.
                  examples: ["campaign123"]
                ordered_faq_ids:
                  type: array
                  description: All FAQ IDs in the desired display order (max 500)
                  items:
                    type: string
                  examples: [["faqId2", "faqId1", "faqId3"]]
      responses:
        '200':
          description: FAQs reordered successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  agent_id:
                    type: string
                    description: Present when agent_id was supplied
                    examples: ["ag7HkQ2ZpLxR3mNb"]
        '400':
          description: ordered_faq_ids is missing or invalid, neither agent_id nor campaign_id was supplied, or both were
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["Missing required field: campaign_id or agent_id"]
        '404':
          description: The agent, the campaign, or one of the FAQ IDs was not found in your account
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["One or more FAQs were not found"]

  /faqs/{faqId}:
    get:
      summary: Get FAQ
      security:
        - ApiKeyAuth: []
      description: Returns a single FAQ by its ID.
      tags:
        - FAQs
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
        - name: faqId
          in: path
          required: true
          description: ID of the FAQ to fetch
          schema:
            type: string
      responses:
        '200':
          description: FAQ retrieved successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  faq:
                    $ref: '#/components/schemas/Faq'
        '404':
          description: No FAQ with this ID exists in your account
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["FAQ not found"]
    put:
      summary: Update FAQ
      security:
        - ApiKeyAuth: []
      description: |
        Partially updates a FAQ. Only the supplied fields are changed; all other fields keep their
        current values. Changing the question or answer automatically refreshes the FAQ's search data in the background.
      tags:
        - FAQs
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
        - name: faqId
          in: path
          required: true
          description: ID of the FAQ to update
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              description: Any subset of the writable FAQ fields
              properties:
                question:
                  type: string
                  description: New question text
                answer:
                  type: string
                  description: New answer text
                is_active:
                  type: boolean
                  description: Whether the AI assistant may use this FAQ
                is_global:
                  type: boolean
                  description: Whether this FAQ applies to all campaigns
                category:
                  type: string
                  description: Free-form category label
                tags:
                  type: array
                  description: Labels for organizing FAQs
                  items:
                    type: string
                order_index:
                  type: integer
                  description: Display position within the campaign
            examples:
              deactivate_faq:
                summary: Temporarily disable a FAQ
                value:
                  is_active: false
      responses:
        '200':
          description: FAQ updated successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  faq_id:
                    type: string
                    examples: ["aBcD1234eFgH5678"]
        '404':
          description: No FAQ with this ID exists in your account
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["FAQ not found"]
    delete:
      summary: Delete FAQ
      security:
        - ApiKeyAuth: []
      description: |
        Permanently deletes a FAQ. When agent_id is supplied, the FAQ is also removed from that AI
        Agent's knowledge base first, so the agent is not left pointing at an entry that no longer
        exists. campaign_id is the legacy equivalent. Sending both is rejected.
      tags:
        - FAQs
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
        - name: faqId
          in: path
          required: true
          description: ID of the FAQ to delete
          schema:
            type: string
        - name: agent_id
          in: query
          required: false
          description: Also remove the FAQ from this AI Agent's knowledge base. Also accepted as agentId.
          schema:
            type: string
        - name: campaign_id
          in: query
          required: false
          description: Legacy; prefer agent_id. Also remove the FAQ from this campaign's FAQ list.
          schema:
            type: string
      responses:
        '200':
          description: FAQ deleted successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
        '400':
          description: Both agent_id and campaign_id were supplied
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["Send either campaign_id or agent_id, not both."]
        '404':
          description: No FAQ with this ID exists in your account, or the supplied agent_id is not one of your agents
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["FAQ not found"]

  /faqs/{faqId}/link:
    post:
      summary: Link FAQ to Campaign
      security:
        - ApiKeyAuth: []
      description: |
        Adds an existing FAQ to an AI Agent's knowledge base. A FAQ can be shared by any number of
        agents, so the same answer only needs to be maintained once.

        Supply exactly one of agent_id or campaign_id.
      tags:
        - FAQs
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
        - name: faqId
          in: path
          required: true
          description: ID of the FAQ to link
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                agent_id:
                  type: string
                  description: AI Agent to add the FAQ to. Also accepted as agentId. Send this or campaign_id, not both.
                  examples: ["ag7HkQ2ZpLxR3mNb"]
                campaign_id:
                  type: string
                  description: Legacy; prefer agent_id. Campaign to link the FAQ to.
                  examples: ["campaign123"]
            examples:
              link_to_agent:
                summary: Add the FAQ to an agent
                value:
                  agent_id: "ag7HkQ2ZpLxR3mNb"
      responses:
        '200':
          description: FAQ linked successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  faq_id:
                    type: string
                    examples: ["aBcD1234eFgH5678"]
                  agent_id:
                    type: string
                    description: Present when agent_id was supplied
                    examples: ["ag7HkQ2ZpLxR3mNb"]
                  campaign_id:
                    type: string
                    description: Present when campaign_id was supplied
                    examples: ["campaign123"]
        '400':
          description: Neither agent_id nor campaign_id was supplied, or both were
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["Missing required field: campaign_id or agent_id"]
        '404':
          description: The FAQ, agent, or campaign was not found in your account
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["Agent not found"]

  /faqs/{faqId}/unlink:
    post:
      summary: Unlink FAQ from Campaign
      security:
        - ApiKeyAuth: []
      description: |
        Removes a FAQ from an AI Agent's knowledge base without deleting the FAQ itself. The FAQ
        stays in your library and remains available to any other agent using it.

        Supply exactly one of agent_id or campaign_id.
      tags:
        - FAQs
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
        - name: faqId
          in: path
          required: true
          description: ID of the FAQ to unlink
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                agent_id:
                  type: string
                  description: AI Agent to remove the FAQ from. Also accepted as agentId. Send this or campaign_id, not both.
                  examples: ["ag7HkQ2ZpLxR3mNb"]
                campaign_id:
                  type: string
                  description: Legacy; prefer agent_id. Campaign to remove the FAQ from.
                  examples: ["campaign123"]
            examples:
              unlink_from_agent:
                summary: Remove the FAQ from an agent
                value:
                  agent_id: "ag7HkQ2ZpLxR3mNb"
      responses:
        '200':
          description: FAQ unlinked successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  faq_id:
                    type: string
                    examples: ["aBcD1234eFgH5678"]
                  agent_id:
                    type: string
                    description: Present when agent_id was supplied
                    examples: ["ag7HkQ2ZpLxR3mNb"]
                  campaign_id:
                    type: string
                    description: Present when campaign_id was supplied
                    examples: ["campaign123"]
        '400':
          description: Neither agent_id nor campaign_id was supplied, or both were
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["Missing required field: campaign_id or agent_id"]
        '404':
          description: The FAQ, agent, or campaign was not found in your account
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["FAQ not found"]

  /faqs/{faqId}/rebuild-embeddings:
    post:
      summary: Rebuild FAQ Search Data
      security:
        - ApiKeyAuth: []
      description: |
        Queues a rebuild of the data the AI assistant uses to find this FAQ (semantic and keyword
        search data). Useful if a FAQ is not being picked up in replies as expected. The rebuild runs
        in the background and usually completes within a few seconds; the FAQ may be temporarily
        excluded from AI replies while it is being rebuilt.
      tags:
        - FAQs
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
        - name: faqId
          in: path
          required: true
          description: ID of the FAQ to rebuild search data for
          schema:
            type: string
      responses:
        '202':
          description: Rebuild queued successfully; processing continues in the background
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  faq_id:
                    type: string
                    examples: ["aBcD1234eFgH5678"]
                  status:
                    type: string
                    description: Always "processing" — poll the FAQ if you need to confirm completion
                    examples: ["processing"]
        '404':
          description: No FAQ with this ID exists in your account
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["FAQ not found"]
  /kb-sources/url:
    post:
      summary: Add Website Page to Knowledge Base
      security:
        - ApiKeyAuth: []
      description: |
        Add one web page to your knowledge base. The page is read, split into sections, and turned
        into FAQs your AI assistant can use when it replies.

        Pass autoLinkToAgentId to attach the page to an AI Agent in this single call - the source,
        and the FAQs it produces, land on that agent's knowledge base with no follow-up linking
        step. This is the one-call way to point an agent at a web page.

        The import runs in the background: a 202 carries the source_id to poll with
        GET /kb-sources/{sourceId} until the status is ready or failed. If the same page is already
        in your knowledge base nothing new is queued and you get a 200 with status "exists"
        instead - and if you asked to auto-link, the existing source is linked for you anyway.
      tags:
        - Knowledge Base
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - url
              properties:
                url:
                  type: string
                  format: uri
                  description: Full http or https address of the page to add.
                  examples: ["https://example.com/pricing"]
                autoLinkToAgentId:
                  type: string
                  description: |
                    ID of an AI Agent to attach the imported source to. Recommended - this is how a
                    web page becomes part of an agent's knowledge base in one call.

                    Linking is best effort: an agent id that does not exist, or belongs to another
                    account, is skipped silently and the import still runs. Confirm the link by
                    reading the agent back.
                  examples: ["ag7HkQ2ZpLxR3mNb"]
                autoLinkToCampaignId:
                  type: string
                  description: Legacy; prefer autoLinkToAgentId. ID of a campaign to attach the imported source to.
                  examples: ["campaign123"]
            examples:
              attach_page_to_agent:
                summary: Give an agent your pricing page
                value:
                  url: "https://example.com/pricing"
                  autoLinkToAgentId: "ag7HkQ2ZpLxR3mNb"
      responses:
        '202':
          description: Import queued; poll the source id for progress
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  source_id:
                    type: string
                    description: Poll this with GET /kb-sources/{sourceId} until the status is ready or failed.
                    examples: ["kb_src_abc123"]
                  status:
                    type: string
                    examples: ["queued"]
                  batch_id:
                    type: string
                    description: ID of the import batch this page belongs to.
                    examples: ["batch_9f2a"]
        '200':
          description: The page is already in your knowledge base, so nothing was queued
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  status:
                    type: string
                    examples: ["exists"]
                  skipped_duplicate:
                    type: integer
                    description: How many submitted pages were already present.
                    examples: [1]
        '400':
          description: url is missing, is not a string, or is not a valid http/https address
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["url is not a valid URL."]

  /kb-sources/file:
    post:
      summary: Add Uploaded File to Knowledge Base
      security:
        - ApiKeyAuth: []
      description: |
        Add a document you have ALREADY uploaded to your account's file storage as a knowledge-base
        source. Supported types: PDF, DOCX, TXT, MD, CSV, XLSX.

        IMPORTANT - this endpoint does not carry the file. There is no multipart upload, no base64
        body and no download-from-URL: you send the storage location of a file that already exists.
        The file is uploaded separately (the dashboard does this when you drag a file in), and it
        must live under your own uploads folder - storage_path has to begin with
        users/{your user id}/uploads/ or the request is refused. If you have no way to place a file
        there, import a web page with POST /kb-sources/url instead.

        Pass autoLinkToAgentId to attach the document to an AI Agent in the same call. The import
        runs in the background: a 202 carries the source_id to poll with GET /kb-sources/{sourceId}.
      tags:
        - Knowledge Base
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - storage_path
                - filename
                - mime_type
              properties:
                storage_path:
                  type: string
                  description: |
                    Where the already-uploaded file lives in your account's storage. Must start with
                    users/{your user id}/uploads/.
                  examples: ["users/abc123uid/uploads/handbook.pdf"]
                filename:
                  type: string
                  description: Original file name, including its extension. Used to detect the file type.
                  examples: ["handbook.pdf"]
                mime_type:
                  type: string
                  description: MIME type of the file, for example application/pdf.
                  examples: ["application/pdf"]
                autoLinkToAgentId:
                  type: string
                  description: |
                    ID of an AI Agent to attach the imported document to. Recommended. Linking is
                    best effort: an agent id that does not exist, or belongs to another account, is
                    skipped silently and the import still runs.
                  examples: ["ag7HkQ2ZpLxR3mNb"]
                autoLinkToCampaignId:
                  type: string
                  description: Legacy; prefer autoLinkToAgentId. ID of a campaign to attach the imported document to.
                  examples: ["campaign123"]
            examples:
              attach_pdf_to_agent:
                summary: Give an agent an uploaded handbook
                value:
                  storage_path: "users/abc123uid/uploads/handbook.pdf"
                  filename: "handbook.pdf"
                  mime_type: "application/pdf"
                  autoLinkToAgentId: "ag7HkQ2ZpLxR3mNb"
      responses:
        '202':
          description: Import queued; poll the source id for progress
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  source_id:
                    type: string
                    description: Poll this with GET /kb-sources/{sourceId} until the status is ready or failed.
                    examples: ["kb_src_abc123"]
                  status:
                    type: string
                    examples: ["queued"]
        '400':
          description: A required field is missing, or the file type is not one we can read
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ['notes.key is not a file type we can read. Supported types: PDF, DOCX, TXT, MD, CSV, XLSX.']
        '403':
          description: storage_path is outside your own uploads folder
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["Invalid storage path."]

  /kb-sources/discover-pages:
    post:
      summary: Discover Website Pages
      security:
        - ApiKeyAuth: []
      description: |
        Explore a website from one starting address and list the pages found on the same domain,
        each with an opinion on whether it is worth adding to the knowledge base. Nothing is
        imported and nothing is selected for you - this is the "what is on this site" step you run
        before deciding what to import with POST /kb-sources/bulk-import.

        Exploration is best-effort: if the site cannot be read the response still comes back as 200,
        with success false, an empty page list and an error message.
      tags:
        - Knowledge Base
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - url
              properties:
                url:
                  type: string
                  format: uri
                  description: Address to start exploring from, usually the site's home page.
                  examples: ["https://example.com"]
                maxPages:
                  type: integer
                  description: Upper bound on how many pages to return.
                  examples: [100]
            examples:
              explore_site:
                summary: Explore a website
                value:
                  url: "https://example.com"
                  maxPages: 100
      responses:
        '200':
          description: |
            Exploration finished, or failed softly. Check success - when it is false, pages is empty
            and error explains why.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  pages:
                    type: array
                    items:
                      $ref: '#/components/schemas/KbDiscoveredPage'
                  source_type:
                    type: string
                    enum: ["sitemap", "link_discovery"]
                    description: How the pages were found - from the site's sitemap, or by following links.
                    examples: ["sitemap"]
                  error:
                    type: string
                    description: Only present when success is false.
        '400':
          description: url is missing or is not a string
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["url is required and must be a string."]

  /kb-sources/refresh-domain:
    post:
      summary: Find New Pages on a Website
      security:
        - ApiKeyAuth: []
      description: |
        Explore a website you have already imported from and report only the pages that are NOT yet
        in your knowledge base, each with a recommendation. Nothing is imported and nothing is
        changed - this is the "what is new on this site" step.

        The two follow-ups are separate on purpose: import the new pages you want with
        POST /kb-sources/bulk-import, and re-read the pages you already have with
        POST /kb-sources/trigger-domain-refresh. Walking away from this call costs nothing.
      tags:
        - Knowledge Base
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - baseUrl
              properties:
                baseUrl:
                  type: string
                  description: Any address on the website, or just the host.
                  examples: ["https://example.com"]
                maxPages:
                  type: integer
                  description: Upper bound on how many pages to explore.
                  examples: [200]
            examples:
              whats_new:
                summary: Look for pages added since the last import
                value:
                  baseUrl: "https://example.com"
      responses:
        '200':
          description: |
            Comparison finished, or failed softly. Check success - when it is false, new_pages is
            empty and error explains why.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  source_type:
                    type: string
                    enum: ["sitemap", "link_discovery"]
                    examples: ["sitemap"]
                  discovered:
                    type: integer
                    description: How many pages were found on the site in total.
                    examples: [249]
                  new_pages:
                    type: array
                    description: Pages that are not in your knowledge base yet. Import the ones you want; nothing is queued for you.
                    items:
                      $ref: '#/components/schemas/KbDiscoveredPage'
                  new_urls_queued:
                    type: integer
                    description: Always 0. Kept for backwards compatibility - this endpoint never queues anything.
                    examples: [0]
                  existing_refresh_queued:
                    type: integer
                    description: |
                      How many pages you have already imported from this website were found ready to
                      be re-read. Nothing is queued by this call - use Refresh All Pages on a
                      Website to actually re-read them.
                    examples: [249]
                  batch_id:
                    type: string
                    description: Present only when a batch was created.
                  error:
                    type: string
                    description: Only present when success is false.
        '400':
          description: baseUrl is missing or empty
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["baseUrl is required and must be a non-empty string."]

  /kb-sources/trigger-domain-refresh:
    post:
      summary: Refresh All Pages on a Website
      security:
        - ApiKeyAuth: []
      description: |
        Re-read every page you have already imported from a website, so the FAQs follow the site's
        current content. Pages that changed have their FAQs updated, new sections are added and
        removed sections are dropped.

        This queues work and returns immediately. Follow progress with
        GET /kb-sources/domain-refresh-status, and stop it with POST /kb-sources/refresh-domain/cancel.
      tags:
        - Knowledge Base
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - baseUrl
              properties:
                baseUrl:
                  type: string
                  description: Any address on the website, or just the host.
                  examples: ["https://example.com"]
            examples:
              refresh_site:
                summary: Re-read everything already imported from a site
                value:
                  baseUrl: "https://example.com"
      responses:
        '200':
          description: Refresh queued
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  queued:
                    type: integer
                    description: How many already-imported pages were queued for re-reading.
                    examples: [249]
                  error:
                    type: string
                    description: Only present when success is false.
        '400':
          description: baseUrl is missing or empty
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["baseUrl is required."]

  /kb-sources/refresh-domain/cancel:
    post:
      summary: Cancel Website Refresh
      security:
        - ApiKeyAuth: []
      description: |
        Stop a website refresh that is still working through its pages. Pages already finished keep
        their updated content; pages not started are dropped, and pages that were being re-read go
        back to their previous state.

        Cancelling twice is harmless - the second call reports the same final state. Once the
        refresh has moved on to its clean-up pass it can no longer be stopped, and the response
        comes back with success false and reason already_finalizing.
      tags:
        - Knowledge Base
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - jobId
              properties:
                jobId:
                  type: string
                  description: The domainBatchId returned by GET /kb-sources/domain-refresh-status.
                  examples: ["job_7c1e"]
            examples:
              stop_refresh:
                summary: Stop a running refresh
                value:
                  jobId: "job_7c1e"
      responses:
        '200':
          description: |
            The refresh was cancelled, was already finished, or is too far along to stop. Check
            success and reason.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    description: False only when the refresh was already in its clean-up pass.
                    examples: [true]
                  status:
                    type: string
                    enum: ["cancelled", "deduplicating", "completed", "failed"]
                    description: State of the refresh after this call.
                    examples: ["cancelled"]
                  reason:
                    type: string
                    enum: ["already_finalizing"]
                    description: Only present when success is false.
                  cancelled_units:
                    type: integer
                    description: How much work was still outstanding when the cancel landed. 0 on a repeat cancel.
                    examples: [28]
                  sources_reset:
                    type: integer
                    description: Existing pages taken back out of processing and returned to ready.
                    examples: [3]
                  sources_cancelled:
                    type: integer
                    description: Brand-new pages of this refresh that were still queued and are now cancelled.
                    examples: [25]
        '400':
          description: jobId is missing or empty
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["jobId is required."]
        '404':
          description: No such refresh job in your account
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["Refresh job not found."]

  /kb-sources/domain-refresh-status:
    get:
      summary: Get Website Refresh Status
      security:
        - ApiKeyAuth: []
      description: |
        How far along a website refresh is, so you can show progress like "221 of 249". Returns job
        as null when no refresh is running for that website.
      tags:
        - Knowledge Base
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
        - name: baseUrl
          in: query
          required: true
          description: Any address on the website, or just the host.
          schema:
            type: string
          example: "https://example.com"
      responses:
        '200':
          description: Status retrieved
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  job:
                    oneOf:
                      - $ref: '#/components/schemas/KbDomainRefreshJob'
                      - type: 'null'
                    description: The running refresh, or null when nothing is running for this website.
                  error:
                    type: string
                    description: Only present when success is false.
        '400':
          description: The baseUrl query parameter is missing or empty
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["baseUrl query parameter is required."]

  /kb-sources/bulk-import:
    post:
      summary: Bulk Import Website Pages
      security:
        - ApiKeyAuth: []
      description: |
        Add up to 100 web pages to your knowledge base in one call - the usual follow-up to
        Discover Website Pages or Find New Pages on a Website.

        Pass autoLinkToAgentId to attach every imported page to an AI Agent in the same call.
        Pages already in your knowledge base are skipped rather than duplicated (and are still
        linked to the agent when you asked for that). The import runs in the background: poll each
        returned source id with GET /kb-sources/{sourceId}.
      tags:
        - Knowledge Base
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - urls
              properties:
                urls:
                  type: array
                  description: Addresses to import. At least 1, at most 100 per call.
                  minItems: 1
                  maxItems: 100
                  items:
                    type: string
                    format: uri
                  examples: [["https://example.com/pricing", "https://example.com/faq"]]
                autoLinkToAgentId:
                  type: string
                  description: |
                    ID of an AI Agent to attach every imported page to. Recommended. Linking is
                    best effort: an agent id that does not exist, or belongs to another account, is
                    skipped silently and the import still runs.
                  examples: ["ag7HkQ2ZpLxR3mNb"]
                autoLinkToCampaignId:
                  type: string
                  description: Legacy; prefer autoLinkToAgentId. ID of a campaign to attach every imported page to.
                  examples: ["campaign123"]
            examples:
              import_selected_pages:
                summary: Import two pages onto an agent
                value:
                  urls: ["https://example.com/pricing", "https://example.com/faq"]
                  autoLinkToAgentId: "ag7HkQ2ZpLxR3mNb"
      responses:
        '202':
          description: Import queued
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  batch_id:
                    type: string
                    description: ID of this import batch.
                    examples: ["batch_9f2a"]
                  queued:
                    type: integer
                    description: How many pages were queued.
                    examples: [2]
                  skipped_duplicate:
                    type: integer
                    description: How many pages were already in your knowledge base.
                    examples: [0]
                  queued_source_ids:
                    type: array
                    description: One source id per queued page. Poll each with GET /kb-sources/{sourceId}.
                    items:
                      type: string
                    examples: [["kb_src_abc123", "kb_src_def456"]]
        '400':
          description: urls is empty, holds a non-string entry, or has more than 100 entries
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["Maximum 100 URLs per call (got 140)."]

  /kb-sources/bulk-delete:
    post:
      summary: Bulk Delete Knowledge Base Sources
      security:
        - ApiKeyAuth: []
      description: |
        Remove up to 2000 knowledge-base sources in one call, together with the FAQs they produced.
        The removal runs in the background; you get an email when it finishes.
      tags:
        - Knowledge Base
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - sourceIds
              properties:
                sourceIds:
                  type: array
                  description: IDs of the sources to remove. At least 1, at most 2000 per call.
                  minItems: 1
                  maxItems: 2000
                  items:
                    type: string
                  examples: [["kb_src_abc123", "kb_src_def456"]]
                domainLabel:
                  type: string
                  description: Friendly name for this clean-up, used only in the completion email.
                  examples: ["example.com"]
            examples:
              delete_two:
                summary: Remove two sources
                value:
                  sourceIds: ["kb_src_abc123", "kb_src_def456"]
      responses:
        '202':
          description: Removal queued
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  batch_id:
                    type: string
                    examples: ["del_batch_31a"]
                  queued:
                    type: integer
                    description: How many sources were queued for removal.
                    examples: [2]
        '400':
          description: sourceIds is empty, holds a non-string entry, or has more than 2000 entries
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["sourceIds must be a non-empty array."]

  /kb-sources/cancel-import:
    post:
      summary: Cancel Knowledge Base Import
      security:
        - ApiKeyAuth: []
      description: |
        Stop pages that are still waiting in an import queue - the "stop import" button for a crawl
        that is bigger than you expected. Cancelling a waiting page costs nothing, because it has
        not been read yet.

        Pages already being processed are NOT stopped: their work is already under way and is
        charged either way, so they finish. The response reports how many those were.
      tags:
        - Knowledge Base
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                host:
                  type: string
                  description: |
                    Only stop waiting pages on this website. Leave it out to stop every waiting
                    import on the account.
                  examples: ["docs.example.com"]
            examples:
              stop_one_site:
                summary: Stop the import of one website
                value:
                  host: "docs.example.com"
              stop_everything:
                summary: Stop every waiting import
                value: {}
      responses:
        '200':
          description: Cancellation applied
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  cancelled:
                    type: integer
                    description: How many waiting pages were stopped.
                    examples: [412]
                  in_flight:
                    type: integer
                    description: Pages already being processed. These are not stopped and will finish.
                    examples: [3]

  /kb-sources/resume-import:
    post:
      summary: Resume Knowledge Base Import
      security:
        - ApiKeyAuth: []
      description: |
        Restart an import that was paused because your own AI key stopped working. Calling this is
        your consent to finish the import on whichever key is live now, which may mean spending
        platform credits if your own key is still down.
      tags:
        - Knowledge Base
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                host:
                  type: string
                  description: Only resume paused pages on this website. Leave it out to resume everything paused.
                  examples: ["docs.example.com"]
            examples:
              resume_everything:
                summary: Resume every paused import
                value: {}
      responses:
        '200':
          description: Import resumed
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  resumed:
                    type: integer
                    description: How many pages went back into the queue.
                    examples: [58]

  /kb-sources/select-relevant-pages:
    post:
      summary: Select Most Relevant Pages
      security:
        - ApiKeyAuth: []
      description: |
        Ask the AI to pick the five pages, out of a list of candidates, that best describe a
        business - used when generating a campaign playbook from a website. Consumes credits.

        This is a helper, not a resource: on failure it still answers 200, with success false, an
        empty page list and an error message.
      tags:
        - Knowledge Base
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - urls
                - homeUrl
              properties:
                urls:
                  type: array
                  description: Candidate page addresses to choose from, usually from Discover Website Pages.
                  items:
                    type: string
                    format: uri
                  examples: [["https://example.com/about", "https://example.com/pricing"]]
                homeUrl:
                  type: string
                  format: uri
                  description: The site's home page, used as context for the choice.
                  examples: ["https://example.com"]
            examples:
              pick_five:
                summary: Pick the five most useful pages
                value:
                  homeUrl: "https://example.com"
                  urls: ["https://example.com/about", "https://example.com/pricing", "https://example.com/blog/2021-recap"]
      responses:
        '200':
          description: |
            Selection finished, or failed softly. Check success - when it is false, pages is empty
            and error explains why.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  pages:
                    type: array
                    description: The chosen pages, at most five.
                    items:
                      type: object
                      properties:
                        url:
                          type: string
                          format: uri
                          examples: ["https://example.com/pricing"]
                        title:
                          type: string
                          description: Title of the page.
                          examples: ["Pricing"]
                        type:
                          type: string
                          description: What kind of page it is, for example pricing, about or services.
                          examples: ["pricing"]
                  error:
                    type: string
                    description: Only present when success is false.

  /kb-sources/estimate-cost:
    post:
      summary: Estimate Knowledge Base Import Cost
      security:
        - ApiKeyAuth: []
      description: |
        Work out how many credits a proposed import would consume, before you commit to it. Pages
        are fetched and documents are read to measure their size, but nothing is imported and no
        credits are spent by the estimate itself.

        A page or file that cannot be read still gets a row, counted as one chunk, with an error on
        it.
      tags:
        - Knowledge Base
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              description: Send urls, files, or both.
              properties:
                urls:
                  type: array
                  description: Page addresses you are considering importing.
                  items:
                    type: string
                    format: uri
                  examples: [["https://example.com/pricing"]]
                files:
                  type: array
                  description: Already-uploaded files you are considering importing.
                  items:
                    type: object
                    required:
                      - storage_path
                      - filename
                      - mime_type
                    properties:
                      storage_path:
                        type: string
                        description: Where the uploaded file lives, under users/{your user id}/uploads/.
                        examples: ["users/abc123uid/uploads/handbook.pdf"]
                      filename:
                        type: string
                        examples: ["handbook.pdf"]
                      mime_type:
                        type: string
                        examples: ["application/pdf"]
                tier:
                  type: string
                  description: |
                    The AI quality tier the import will run on, so the estimate matches what you
                    will actually be charged. Leave it out for the standard rate.
                  examples: ["max"]
            examples:
              estimate_a_page:
                summary: Estimate one page
                value:
                  urls: ["https://example.com/pricing"]
      responses:
        '200':
          description: Estimate computed
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  estimates:
                    type: array
                    description: One row per submitted page or file.
                    items:
                      $ref: '#/components/schemas/KbCostEstimateRow'
                  total_chunks:
                    type: integer
                    examples: [7]
                  total_credits:
                    type: number
                    description: Total estimated credits for the whole batch.
                    examples: [7]

  /kb-sources/{sourceId}/refresh:
    post:
      summary: Refresh Knowledge Base Source
      security:
        - ApiKeyAuth: []
      description: |
        Re-read one web page you have already imported and bring its FAQs back in line with the
        page's current content: changed sections are updated, new ones added, removed ones dropped.

        The refresh runs in the background - poll GET /kb-sources/{sourceId} until the status leaves
        queued and processing.
      tags:
        - Knowledge Base
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
        - name: sourceId
          in: path
          required: true
          description: ID of the knowledge-base source to re-read.
          schema:
            type: string
      responses:
        '202':
          description: Refresh queued
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  source_id:
                    type: string
                    examples: ["kb_src_abc123"]
                  status:
                    type: string
                    examples: ["queued"]
        '400':
          description: sourceId is missing
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["Missing required parameter: sourceId"]
        '404':
          description: No knowledge-base source with this ID exists in your account
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["Knowledge base source not found."]

  /kb-sources/{sourceId}:
    get:
      summary: Get Knowledge Base Source Status
      security:
        - ApiKeyAuth: []
      description: |
        Check where one knowledge-base source is in the pipeline. This is the poll that follows
        every import and refresh: repeat it until the status is ready or failed, then read the FAQs
        it produced with GET /faqs.
      tags:
        - Knowledge Base
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
        - name: sourceId
          in: path
          required: true
          description: ID of the knowledge-base source, as returned by an import.
          schema:
            type: string
      responses:
        '200':
          description: Status retrieved
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/KbSourceStatus'
              example:
                success: true
                source_id: kb_src_abc123
                status: ready
                faq_count: 24
                section_count: 31
                error_message: null
        '400':
          description: sourceId is missing
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["Missing required parameter: sourceId"]
        '404':
          description: No knowledge-base source with this ID exists in your account
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["Knowledge base source not found."]
    delete:
      summary: Delete Knowledge Base Source
      security:
        - ApiKeyAuth: []
      description: |
        Remove one knowledge-base source. By default the FAQs it produced are kept; add
        delete_faqs=true to remove those as well.
      tags:
        - Knowledge Base
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
        - name: sourceId
          in: path
          required: true
          description: ID of the knowledge-base source to remove.
          schema:
            type: string
        - name: delete_faqs
          in: query
          required: false
          description: Set to true to also delete every FAQ this source produced.
          schema:
            type: boolean
            default: false
      responses:
        '200':
          description: Source removed
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  faqs_deleted:
                    type: integer
                    description: How many FAQs were deleted along with the source. 0 unless delete_faqs was true.
                    examples: [24]
        '400':
          description: sourceId is missing
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["Missing required parameter: sourceId"]
        '404':
          description: No knowledge-base source with this ID exists in your account
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["Knowledge base source not found."]

  /kb-groups:
    post:
      summary: Create Knowledge Base Group
      security:
        - ApiKeyAuth: []
      description: |
        Create a KB group - a named bundle of FAQs you can apply to an agent in one go. The group
        starts empty; add FAQs to it with POST /kb-groups/{groupId}/faqs.
      tags:
        - Knowledge Base
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - name
              properties:
                name:
                  type: string
                  description: Name of the group.
                  examples: ["Shipping and returns"]
            examples:
              create:
                summary: Create a group
                value:
                  name: "Shipping and returns"
      responses:
        '201':
          description: Group created
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  group_id:
                    type: string
                    examples: ["kbg_abc123"]
        '400':
          description: name is missing or blank
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["Missing required field: name"]

  /kb-groups/{groupId}:
    put:
      summary: Rename Knowledge Base Group
      security:
        - ApiKeyAuth: []
      description: Change a KB group's name. Its FAQs are untouched.
      tags:
        - Knowledge Base
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
        - name: groupId
          in: path
          required: true
          description: ID of the KB group.
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - name
              properties:
                name:
                  type: string
                  description: New name for the group.
                  examples: ["Shipping, returns and refunds"]
            examples:
              rename:
                summary: Rename a group
                value:
                  name: "Shipping, returns and refunds"
      responses:
        '200':
          description: Group renamed
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  group_id:
                    type: string
                    examples: ["kbg_abc123"]
                  name:
                    type: string
                    examples: ["Shipping, returns and refunds"]
        '400':
          description: name is missing or blank
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["Missing required field: name"]
        '404':
          description: No KB group with this ID exists in your account
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["KB group not found"]
    delete:
      summary: Delete Knowledge Base Group
      security:
        - ApiKeyAuth: []
      description: |
        Delete a KB group. Only the bundle is removed - the FAQs in it stay in your library, and
        anything the group was already applied to keeps those FAQs.
      tags:
        - Knowledge Base
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
        - name: groupId
          in: path
          required: true
          description: ID of the KB group to delete.
          schema:
            type: string
      responses:
        '200':
          description: Group deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
        '404':
          description: No KB group with this ID exists in your account
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["KB group not found"]

  /kb-groups/{groupId}/faqs:
    post:
      summary: Add FAQ to Knowledge Base Group
      security:
        - ApiKeyAuth: []
      description: |
        Put an existing FAQ into a KB group. This only changes the bundle - it does not attach the
        FAQ to any agent by itself; apply the group for that.
      tags:
        - Knowledge Base
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
        - name: groupId
          in: path
          required: true
          description: ID of the KB group.
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - faq_id
              properties:
                faq_id:
                  type: string
                  description: ID of the FAQ to add.
                  examples: ["aBcD1234eFgH5678"]
            examples:
              add:
                summary: Add a FAQ to a group
                value:
                  faq_id: "aBcD1234eFgH5678"
      responses:
        '200':
          description: FAQ added to the group
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  group_id:
                    type: string
                    examples: ["kbg_abc123"]
                  faq_id:
                    type: string
                    examples: ["aBcD1234eFgH5678"]
        '400':
          description: faq_id is missing or blank
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["Missing required field: faq_id"]
        '404':
          description: The KB group or the FAQ does not exist in your account
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["KB group not found"]

  /kb-groups/{groupId}/faqs/{faqId}:
    delete:
      summary: Remove FAQ from Knowledge Base Group
      security:
        - ApiKeyAuth: []
      description: |
        Take a FAQ out of a KB group. The FAQ itself is not deleted, and agents the group was
        already applied to keep it.
      tags:
        - Knowledge Base
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
        - name: groupId
          in: path
          required: true
          description: ID of the KB group.
          schema:
            type: string
        - name: faqId
          in: path
          required: true
          description: ID of the FAQ to remove from the group.
          schema:
            type: string
      responses:
        '200':
          description: FAQ removed from the group
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  group_id:
                    type: string
                    examples: ["kbg_abc123"]
                  faq_id:
                    type: string
                    examples: ["aBcD1234eFgH5678"]
        '404':
          description: No KB group with this ID exists in your account
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["KB group not found"]

  /kb-groups/{groupId}/apply-to-agent:
    post:
      summary: Apply Knowledge Base Group to Agent
      security:
        - ApiKeyAuth: []
      description: |
        Add every FAQ in a KB group to an AI Agent's knowledge base in one call - the fast way to
        give a new agent a whole body of knowledge you have already curated.

        Only FAQs the agent does not have yet are added, so applying the same group twice is
        harmless and added_count comes back as 0 the second time.
      tags:
        - Knowledge Base
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
        - name: groupId
          in: path
          required: true
          description: ID of the KB group to apply.
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - agent_id
              properties:
                agent_id:
                  type: string
                  description: ID of the AI Agent to apply the group to.
                  examples: ["ag7HkQ2ZpLxR3mNb"]
            examples:
              apply:
                summary: Give an agent a whole KB group
                value:
                  agent_id: "ag7HkQ2ZpLxR3mNb"
      responses:
        '200':
          description: Group applied
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  group_id:
                    type: string
                    examples: ["kbg_abc123"]
                  agent_id:
                    type: string
                    examples: ["ag7HkQ2ZpLxR3mNb"]
                  added_count:
                    type: integer
                    description: How many FAQs were actually added. 0 when the group is empty or already applied.
                    examples: [12]
        '400':
          description: agent_id is missing or blank
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["Missing required field: agent_id"]
        '404':
          description: The KB group or the agent does not exist in your account
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["KB group not found"]

  /kb-groups/{groupId}/apply-to-campaign:
    post:
      summary: Apply Knowledge Base Group to Campaign
      security:
        - ApiKeyAuth: []
      description: |
        Legacy. Add every FAQ in a KB group to a campaign's knowledge base. On an agent-based
        account use Apply Knowledge Base Group to Agent instead.

        Only FAQs the campaign does not have yet are added, so re-applying a group is harmless.
      tags:
        - Knowledge Base
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
        - name: groupId
          in: path
          required: true
          description: ID of the KB group to apply.
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - campaign_id
              properties:
                campaign_id:
                  type: string
                  description: ID of the campaign to apply the group to.
                  examples: ["campaign123"]
            examples:
              apply:
                summary: Apply a KB group to a campaign
                value:
                  campaign_id: "campaign123"
      responses:
        '200':
          description: Group applied
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  group_id:
                    type: string
                    examples: ["kbg_abc123"]
                  campaign_id:
                    type: string
                    examples: ["campaign123"]
                  added_count:
                    type: integer
                    description: How many FAQs were actually added. 0 when the group is empty or already applied.
                    examples: [12]
        '400':
          description: campaign_id is missing or blank
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["Missing required field: campaign_id"]
        '404':
          description: The KB group or the campaign does not exist in your account
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["KB group not found"]

  /contacts/{contactId}/stats:
    get:
      summary: Get Contact Stats
      security:
        - ApiKeyAuth: []
      description: |
        Returns the message totals for one contact: how many messages in total, how
        many you sent and received, how many replies came from the AI versus a
        human, the credits this conversation has used, and when the first and last
        message happened.

        The message counts are computed from the reporting database. If it cannot
        answer they come back as 0 rather than a wrong number, while creditsUsed and
        botMessageCount - which are read straight off the contact - stay correct.
      tags:
        - Contacts
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
        - name: contactId
          in: path
          required: true
          schema:
            type: string
          description: ID of the contact.
      responses:
        '200':
          description: The contact's message statistics
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  totalMessages:
                    type: integer
                  sent:
                    type: integer
                    description: Messages sent to the contact.
                  received:
                    type: integer
                    description: Messages received from the contact.
                  aiReplies:
                    type: integer
                    description: Outgoing messages written by the AI.
                  humanReplies:
                    type: integer
                    description: Outgoing messages written by a person.
                  creditsUsed:
                    type: number
                    description: Credits this conversation has consumed.
                  botMessageCount:
                    type: integer
                    description: The AI-reply counter shown on the contact (reset by writing bot_message_count 0).
                  firstMessageAt:
                    type: ["string", "null"]
                    format: date-time
                  lastMessageAt:
                    type: ["string", "null"]
                    format: date-time
        '404':
          description: Contact not found on this account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContactsApiError'
  /contacts/{contactId}/department:
    post:
      summary: Assign Contact to a Department
      security:
        - ApiKeyAuth: []
      description: |
        Files a contact under one of your departments (Sales, Support, HR) and, by
        default, hands it to whoever on that department has the fewest leads right now.

        A department says which TEAM owns the lead; the assign endpoint says which
        PERSON does. They are independent - assigning a person never changes the
        department, and filing a lead under a department never takes it away from the
        person already handling it.

        Send department_id with the department, or null to remove the contact from
        whichever department it is in. Set hand_to_member to false to file the lead
        without handing it to anyone, for teams that pull from a shared queue.
      tags:
        - Contacts
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
        - name: contactId
          in: path
          required: true
          schema:
            type: string
          description: ID of the contact to file.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - department_id
              properties:
                department_id:
                  type: ["string", "null"]
                  description: >-
                    ID of the department (see GET /team/departments), or null to remove
                    the contact from its department. Required - an empty string is rejected.
                hand_to_member:
                  type: boolean
                  default: true
                  description: >-
                    Also hand the lead to the least busy member of that department.
                    Never overrides an assignment the contact already has, and does
                    nothing when the department has auto-assign switched off or has no
                    available members.
            examples:
              file_under_sales:
                summary: File the lead under Sales and hand it to someone
                value:
                  department_id: "mg_abc123"
                  hand_to_member: true
              queue_only:
                summary: File it under Support without handing it to anyone
                value:
                  department_id: "mg_support"
                  hand_to_member: false
              clear:
                summary: Remove the contact from its department
                value:
                  department_id: null
      responses:
        '200':
          description: The contact was filed
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  department_id:
                    type: ["string", "null"]
                    description: The department the contact now belongs to.
                  assigned_to:
                    type: ["string", "null"]
                    description: >-
                      Who the contact is assigned to after the call. Null when nobody
                      is - a normal outcome for a shared-queue department.
        '404':
          description: Contact or department not found
  /contacts/{contactId}/assign-agent:
    post:
      summary: Assign Contact to AI Agent
      security:
        - ApiKeyAuth: []
      description: |
        Hands a live conversation to a different AI Agent, mid-chat. This is how you
        move a contact from one agent to another - for example passing a qualified
        lead from the front-desk agent to a specialist one - without waiting for the
        contact to message again.

        Send agentId with the agent that should take over, or null to unassign the
        contact so normal routing decides who answers next.

        Set triggerAIResponse to true to have the NEW agent read the outstanding
        messages and reply right away. Leave it out and the new agent simply handles
        the next incoming message. If that immediate reply cannot be started, the
        assignment still succeeds and aiTriggerError explains why - the hand-off is
        never rolled back because of a failed reply.
      tags:
        - Contacts
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
        - name: contactId
          in: path
          required: true
          schema:
            type: string
          description: ID of the contact to hand over.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - agentId
              properties:
                agentId:
                  type: ["string", "null"]
                  description: >-
                    ID of the AI Agent that should answer this contact from now on, or
                    null to unassign. Required - an empty string is rejected.
                triggerAIResponse:
                  type: boolean
                  default: false
                  description: >-
                    Have the new agent answer the contact's outstanding messages
                    immediately. Ignored when agentId is null.
            examples:
              hand_over:
                summary: Hand the conversation to a specialist agent and reply now
                value:
                  agentId: "agent_abc123"
                  triggerAIResponse: true
              unassign:
                summary: Unassign the contact
                value:
                  agentId: null
      responses:
        '200':
          description: The contact was reassigned
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    type: object
                    properties:
                      contactId:
                        type: string
                      agentId:
                        type: ["string", "null"]
                      aiResponseTriggered:
                        type: boolean
                        description: Whether the new agent's immediate reply was started.
                      aiTriggerError:
                        type: string
                        description: Present only when the immediate reply could not be started. The assignment still succeeded.
        '400':
          description: agentId missing, or not a non-empty string or null
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContactsApiError'
        '404':
          description: Contact or agent not found on this account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContactsApiError'
  /contacts/{contactId}/link-channel:
    post:
      summary: Link Contact to Another Channel
      security:
        - ApiKeyAuth: []
      description: |
        "Continue on WhatsApp": finds or creates this same person's contact on
        another phone-based channel and links the two, so both conversations are
        recognised as one person.

        A contact cannot change channel - the channel is baked into it - so this
        creates a SIBLING contact on the target channel and returns its ID. Send
        that channel's message to the returned contact_id.

        Safe to call twice: the second call finds the existing sibling, returns the
        same IDs and created false, and leaves the original link date intact.

        Returns 422 (the request was fine, your account state was not) when the
        contact is already on that channel family, when there is no phone number to
        address, or when you have no connected sender for the target channel - that
        last check exists so you never get a linked contact whose first message
        cannot be delivered. Returns 409 when the two contacts already belong to two
        different people; unlink one of them first.
      tags:
        - Contacts
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
        - name: contactId
          in: path
          required: true
          schema:
            type: string
          description: ID of the contact to continue from.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - channel
              properties:
                channel:
                  type: string
                  enum: ['whatsapp', 'whatsapp_web', 'sms']
                  description: The channel to continue the conversation on.
                phoneNumber:
                  type: string
                  description: >-
                    Phone number to use on the new channel. Defaults to the contact's
                    existing number.
            examples:
              continue_on_whatsapp:
                summary: Continue a chat widget lead on WhatsApp
                value:
                  channel: "whatsapp"
      responses:
        '200':
          description: The contacts are linked
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    type: object
                    properties:
                      contact_id:
                        type: string
                        description: The contact on the target channel - message this one.
                      person_id:
                        type: string
                        description: The shared person the two contacts now belong to.
                      created:
                        type: boolean
                        description: True when this call created the sibling contact, false when it already existed.
        '400':
          description: channel missing or not one of whatsapp, whatsapp_web, sms
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContactsApiError'
        '404':
          description: Contact not found on this account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContactsApiError'
        '409':
          description: The two contacts already belong to different people
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContactsApiError'
        '422':
          description: >-
            Already on that channel, no phone number on the contact, or no connected
            sender for the target channel
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContactsApiError'
  /contacts/{contactId}/messages:
    get:
      summary: List Messages (paginated)
      security:
        - ApiKeyAuth: []
      description: |
        Returns messages for a contact, newest first, with cursor-based
        pagination. Pass the next_cursor value from a previous response as the
        cursor query parameter to fetch the next (older) page. Optional filter
        and direction parameters narrow the results within each page; when a
        filter is active a page may contain fewer items than the limit, but
        next_cursor still advances through the full conversation.
      tags:
        - Messages
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
        - name: contactId
          in: path
          required: true
          description: ID of the contact whose messages to list.
          schema:
            type: string
        - name: limit
          in: query
          required: false
          description: Page size. Default 50, maximum 100.
          schema:
            type: integer
            default: 50
            maximum: 100
        - name: cursor
          in: query
          required: false
          description: Opaque message ID from a previous response's next_cursor. Returns messages older than the cursor.
          schema:
            type: string
        - name: filter
          in: query
          required: false
          description: Filter messages by content type.
          schema:
            type: string
            enum: [all, text, media, tool_use]
            default: all
        - name: direction
          in: query
          required: false
          description: Filter messages by direction.
          schema:
            type: string
            enum: [all, inbound, outbound]
            default: all
      responses:
        '200':
          description: A page of messages
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  contact_id:
                    type: string
                  messages:
                    type: array
                    items:
                      $ref: '#/components/schemas/MessageItem'
                  next_cursor:
                    type: ['string', 'null']
                    description: Pass this value as the cursor parameter to fetch the next page. Null when there are no more messages.
        '404':
          description: Contact not found
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  error:
                    type: string
              example:
                success: false
                error: "Contact not found"
  /contacts/{contactId}/messages/bulk-delete:
    post:
      summary: Bulk Delete Messages
      security:
        - ApiKeyAuth: []
      description: |
        Deletes several messages from a conversation in one call, on YOUR side only.
        The messages stop showing content - the body and any attachment are cleared -
        but nothing is retracted on the customer's phone. To also remove a message
        from the customer's device, delete it one at a time with the single-message
        delete, which attempts a retraction.

        Up to 500 message IDs per call.
      tags:
        - Messages
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
        - name: contactId
          in: path
          required: true
          schema:
            type: string
          description: ID of the contact whose conversation the messages belong to.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - message_ids
              properties:
                message_ids:
                  type: array
                  items:
                    type: string
                  maxItems: 500
                  description: IDs of the messages to delete. messageIds is accepted as an alias.
            examples:
              two_messages:
                value:
                  message_ids: ["msg_1", "msg_2"]
      responses:
        '200':
          description: The messages were deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  contact_id:
                    type: string
                  deleted:
                    type: integer
        '400':
          description: message_ids missing, empty, not all strings, or over 500 entries
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContactsApiError'
        '404':
          description: Contact not found on this account, or one of the message IDs does not exist
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContactsApiError'
  /contacts/{contactId}/messages/{messageId}:
    patch:
      summary: Set Message Score or Star
      security:
        - ApiKeyAuth: []
      description: |
        Rates a message thumbs up or thumbs down, and/or stars it as important. Send
        at least one of the two; only what you send is written, so starring a message
        never clears its rating and vice versa.
      tags:
        - Messages
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
        - name: contactId
          in: path
          required: true
          schema:
            type: string
          description: ID of the contact whose conversation the message belongs to.
        - name: messageId
          in: path
          required: true
          schema:
            type: string
          description: ID of the message.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                score:
                  type: integer
                  enum: [-1, 0, 1]
                  description: 1 for thumbs up, -1 for thumbs down, 0 to clear the rating.
                is_important:
                  type: boolean
                  description: Star or unstar the message. Must be a real boolean, not "true".
            examples:
              thumbs_up:
                value:
                  score: 1
              star:
                value:
                  is_important: true
      responses:
        '200':
          description: The message was updated. Only the fields you sent are echoed back.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  contact_id:
                    type: string
                  message_id:
                    type: string
                  score:
                    type: integer
                  is_important:
                    type: boolean
        '400':
          description: Neither score nor is_important sent, or an invalid value
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContactsApiError'
        '404':
          description: Message not found on this account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContactsApiError'
    delete:
      summary: Delete Message
      security:
        - ApiKeyAuth: []
      description: |
        Deletes one message from the conversation and, where the channel allows it,
        also retracts the copy on the customer's device.

        Always answers 200 when the message existed, even if the retraction failed -
        our copy IS gone, so an error would be a lie. Read revoke_supported (can this
        channel retract at all), revoked (did it this time) and revoke_reason (why
        not) to tell the user what actually happened.
      tags:
        - Messages
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
        - name: contactId
          in: path
          required: true
          schema:
            type: string
          description: ID of the contact whose conversation the message belongs to.
        - name: messageId
          in: path
          required: true
          schema:
            type: string
          description: ID of the message to delete.
      responses:
        '200':
          description: The message was deleted on our side
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  contact_id:
                    type: string
                  message_id:
                    type: string
                  revoke_supported:
                    type: boolean
                    description: Whether this channel can retract messages at all.
                  revoked:
                    type: boolean
                    description: Whether the copy on the customer's device was removed.
                  revoke_reason:
                    type: string
                    description: Why the retraction did not happen, when revoked is false.
        '404':
          description: Message not found on this account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContactsApiError'
  /contacts/{contactId}/messages/{messageId}/edit:
    post:
      summary: Edit Message
      security:
        - ApiKeyAuth: []
      description: |
        Rewrites a message you already sent, on the customer's device and in your
        copy.

        Unlike deleting, this FAILS LOUDLY when the channel refuses: you get a 409
        and your copy is left untouched, because showing an edit the customer never
        received would put the two sides out of step. edit_reason says whether it was
        the channel's edit time window, a disconnected channel, or something else.
      tags:
        - Messages
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
        - name: contactId
          in: path
          required: true
          schema:
            type: string
          description: ID of the contact whose conversation the message belongs to.
        - name: messageId
          in: path
          required: true
          schema:
            type: string
          description: ID of the message to edit.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - body
              properties:
                body:
                  type: string
                  description: The new message text.
            examples:
              fix_typo:
                value:
                  body: "Sorry - I meant Thursday at 3pm."
      responses:
        '200':
          description: The message was edited
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  contact_id:
                    type: string
                  message_id:
                    type: string
                  edited:
                    type: boolean
                  edit_reason:
                    type: string
        '400':
          description: body missing or not a string
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContactsApiError'
        '404':
          description: Message not found on this account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContactsApiError'
        '409':
          description: >-
            The channel would not take the edit (for example the edit window has
            closed). Nothing was changed; edit_reason says why.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContactsApiError'
  /contacts/{contactId}/messages/{messageId}/react:
    post:
      summary: React to Message
      security:
        - ApiKeyAuth: []
      description: |
        Puts your own emoji reaction on a message, or takes it back by sending an
        empty string. The contact's own reactions are never touched.

        Like editing, this fails loudly rather than showing a reaction the customer
        never got. The failure is split by whether retrying could help: 422 means it
        can never be delivered on this conversation (the channel does not support
        reactions, the message has no provider ID, or the emoji is outside the set
        that channel allows), 409 means the channel is momentarily unreachable and a
        retry may work.

        The emoji must be a single whitespace-free string of at most 16 characters.
      tags:
        - Messages
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
        - name: contactId
          in: path
          required: true
          schema:
            type: string
          description: ID of the contact whose conversation the message belongs to.
        - name: messageId
          in: path
          required: true
          schema:
            type: string
          description: ID of the message to react to.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - emoji
              properties:
                emoji:
                  type: string
                  maxLength: 16
                  description: The emoji to react with, or "" to remove your reaction.
            examples:
              thumbs_up:
                value:
                  emoji: "👍"
              remove:
                summary: Remove your reaction
                value:
                  emoji: ""
      responses:
        '200':
          description: The reaction was delivered
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  contact_id:
                    type: string
                  message_id:
                    type: string
                  reaction_supported:
                    type: boolean
                  reaction_reason:
                    type: string
                  reactions:
                    type: array
                    description: Every reaction now on the message, yours and the contact's.
                    items:
                      type: object
                      additionalProperties: true
        '400':
          description: emoji missing, not a string, or not a whitespace-free string of at most 16 characters
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContactsApiError'
        '404':
          description: Message not found on this account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContactsApiError'
        '409':
          description: The channel was momentarily unreachable. Nothing was changed; a retry may work.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContactsApiError'
        '422':
          description: >-
            The reaction can never be delivered on this conversation - the channel
            does not support reactions, the message has no provider ID, or the emoji
            is not in that channel's allowed set.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContactsApiError'
  /contacts/{contactId}/messages/mark-read:
    post:
      summary: Mark Messages as Read
      security:
        - ApiKeyAuth: []
      description: |
        Marks specific messages as read for a contact. Provide the IDs of the
        messages to update; each one gets its status set to read.
      tags:
        - Messages
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
        - name: contactId
          in: path
          required: true
          description: ID of the contact the messages belong to.
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - message_ids
              properties:
                message_ids:
                  type: array
                  description: IDs of the messages to mark as read.
                  items:
                    type: string
            example:
              message_ids: ["aB3dE5fG7hI9jK1lM2nO", "cD4eF6gH8iJ0kL2mN3oP"]
      responses:
        '200':
          description: Messages marked as read
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  contact_id:
                    type: string
                  marked_read:
                    type: integer
                    description: Number of messages updated.
              example:
                success: true
                contact_id: "contact123"
                marked_read: 2
        '404':
          description: Contact not found
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  error:
                    type: string
              example:
                success: false
                error: "Contact not found"
  /contacts/{contactId}/mark-read:
    post:
      summary: Mark Chat as Read
      security:
        - ApiKeyAuth: []
      description: |
        Marks a contact's entire conversation as read. This clears the unread
        badge for the conversation in the inbox. No request body is required.
      tags:
        - Messages
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
        - name: contactId
          in: path
          required: true
          description: ID of the contact whose chat to mark as read.
          schema:
            type: string
      responses:
        '200':
          description: Chat marked as read
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  contact_id:
                    type: string
              example:
                success: true
                contact_id: "contact123"
        '404':
          description: Contact not found
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  error:
                    type: string
              example:
                success: false
                error: "Contact not found"
  /contacts/{contactId}/mark-unread:
    post:
      summary: Mark Chat as Unread
      security:
        - ApiKeyAuth: []
      description: |
        Marks a contact's entire conversation as unread, re-raising the unread
        badge for the conversation in the inbox. No request body is required.

        This is an inbox-only flag: it does NOT change when the conversation was
        last read, so no read receipt is sent to the contact on channels that
        support them. Use it to hand a conversation back to your team after
        someone has already opened it.
      tags:
        - Messages
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
        - name: contactId
          in: path
          required: true
          description: ID of the contact whose chat to mark as unread.
          schema:
            type: string
      responses:
        '200':
          description: Chat marked as unread
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  contact_id:
                    type: string
              example:
                success: true
                contact_id: "contact123"
        '404':
          description: Contact not found
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  error:
                    type: string
              example:
                success: false
                error: "Contact not found"
  /contacts/{contactId}/chat-sessions/{sessionId}/messages:
    get:
      summary: Get Chat Session Messages
      security:
        - ApiKeyAuth: []
      description: |
        Returns the full message thread of one chat session, oldest first,
        together with the session's metadata. Use the chat sessions list
        endpoint to discover session IDs for a contact.
      tags:
        - Chat Sessions
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
        - name: contactId
          in: path
          required: true
          description: ID of the contact the session belongs to.
          schema:
            type: string
        - name: sessionId
          in: path
          required: true
          description: ID of the chat session to fetch.
          schema:
            type: string
      responses:
        '200':
          description: The session thread
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  contact_id:
                    type: string
                  session:
                    $ref: '#/components/schemas/ChatSessionThread'
                  messages:
                    type: array
                    items:
                      $ref: '#/components/schemas/MessageItem'
        '404':
          description: Contact or chat session not found
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  error:
                    type: string
              example:
                success: false
                error: "Chat session not found"
  /contacts/send:
    post:
      summary: Send Message (by contact identity)
      security:
        - ApiKeyAuth: []
      description: |
        Channel-agnostic send. Resolves the target contact either by
        contact_id, or by channel plus the matching identity field
        (phone_number for whatsapp, whatsapp_web, and sms; instagram_id for
        instagram; messenger_id for messenger; telegram_user_id for telegram),
        then queues the message for delivery on the contact's channel.
        Delivery happens asynchronously; the response confirms the message was
        accepted. Contacts with do-not-disturb or private mode enabled are
        rejected with a 422 response. Sending to chat-widget or custom-channel
        contacts requires contact_id.
      tags:
        - Messages
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SendMessageRequest'
            examples:
              by_contact_id:
                summary: Send to an existing contact by ID
                value:
                  contact_id: "contact123"
                  body: "Hi! Your appointment is confirmed."
              by_phone_whatsapp:
                summary: Send by phone number on WhatsApp
                value:
                  channel: "whatsapp"
                  phone_number: "+31612345678"
                  body: "Hi! Your appointment is confirmed."
              by_instagram_id:
                summary: Send by Instagram user ID
                value:
                  channel: "instagram"
                  instagram_id: "17841400000000000"
                  body: "Thanks for your message!"
      responses:
        '201':
          description: Message accepted for delivery
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SendMessageResponse'
              example:
                success: true
                message_id: "aB3dE5fG7hI9jK1lM2nO"
                contact_id: "contact123"
                channel: "whatsapp"
        '404':
          description: No contact matches the given ID or identity
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  error:
                    type: string
              example:
                success: false
                error: "Contact not found"
        '422':
          description: The contact cannot receive outbound messages (do-not-disturb, private, or unsupported channel)
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  error:
                    type: string
              example:
                success: false
                error: "Contact has do_not_disturb enabled"
  /contacts/{contactId}/send-message:
    post:
      summary: Send Message to Contact
      security:
        - ApiKeyAuth: []
      description: |
        Sends a message to an existing contact on whichever channel the
        contact is on. The message is queued and delivered asynchronously;
        the response confirms it was accepted. Contacts with do-not-disturb
        or private mode enabled are rejected with a 422 response.
      tags:
        - Messages
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
        - name: contactId
          in: path
          required: true
          description: ID of the contact to send the message to.
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - body
              properties:
                body:
                  type: string
                  description: Text content of the message to send.
                mediaUrl:
                  type: string
                  description: Optional URL of a media file to attach.
                mediaContentType:
                  type: string
                  description: MIME type of the attached media file.
            example:
              body: "Hi! Your appointment is confirmed."
      responses:
        '200':
          description: Message accepted for delivery
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  messageId:
                    type: string
                  contactId:
                    type: string
                  channel:
                    type: string
                  message:
                    type: string
              example:
                success: true
                messageId: "aB3dE5fG7hI9jK1lM2nO"
                contactId: "contact123"
                channel: "whatsapp"
                message: "Message created successfully. Delivery is being processed."
        '422':
          description: The contact cannot receive outbound messages (do-not-disturb, private, or unsupported channel)
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  error:
                    type: string
              example:
                success: false
                error: "Contact has do_not_disturb enabled"
  /tasks:
    post:
      summary: Create Task
      security:
        - ApiKeyAuth: []
      description: |
        Create a new task. Only the title is required; everything else has sensible defaults.
        When no stage is given, the task is placed in the first stage of your board.
        New tasks are inserted at the top of their stage.
      tags:
        - Tasks
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - title
              properties:
                title:
                  type: string
                  description: Short title of the task.
                description:
                  type: string
                  description: Longer free-text description.
                type:
                  type: string
                  description: Task type identifier. Defaults to to_do.
                priority:
                  type: string
                  enum: [none, low, medium, high, urgent]
                  description: Task priority. Defaults to none.
                stage:
                  type: string
                  description: Stage (column) ID for the task. Defaults to the first stage on your board.
                due_date:
                  type: string
                  description: Due date as an ISO 8601 string, for example 2026-07-01T09:00:00Z.
                contact_id:
                  type: string
                  description: ID of a contact to link the task to.
                deal_id:
                  type: string
                  description: ID of a deal to link the task to.
                campaign_id:
                  type: string
                  description: ID of a campaign to link the task to.
                assigned_to:
                  type: string
                  description: ID of the team member to assign the task to.
                linked_messages:
                  type: array
                  items:
                    type: string
                  description: IDs of chat messages to link to the task.
                tags:
                  type: array
                  items:
                    type: string
                  description: Free-form labels for the task.
                notes:
                  type: string
                  description: Internal notes.
                insert_at_top:
                  type: boolean
                  description: >-
                    Put the task at the very top of its stage and push every other
                    task in that stage down one. Without it the task is still added at
                    the top, but the other tasks keep the position numbers they had -
                    which is fine unless you are mirroring the board's own ordering.
                    Sending position 0 does the same thing.
                position:
                  type: integer
                  description: >-
                    Only 0 is meaningful here, and it means the same as
                    insert_at_top true. Any other value is ignored.
                source:
                  type: string
                  enum: ['api', 'chat', 'chat_composer']
                  description: >-
                    Where the task came from, shown in the app. Defaults to api, and
                    anything outside this list is quietly recorded as api rather than
                    rejected - the internal provenances (for example ai_tool or
                    automation) are stamped by the platform and cannot be claimed here.
            examples:
              minimal:
                summary: Create a task with only a title
                value:
                  title: "Call the new lead back"
              full:
                summary: Create a fully specified task
                value:
                  title: "Send pricing proposal"
                  description: "Prepare and send the updated pricing PDF"
                  type: "email"
                  priority: "high"
                  stage: "in_progress"
                  due_date: "2026-07-01T09:00:00Z"
                  contact_id: "contact123"
                  tags: ["sales", "proposal"]
      responses:
        '201':
          description: Task created
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  task_id:
                    type: string
                    description: ID of the newly created task.
                  task:
                    $ref: '#/components/schemas/Task'
        '400':
          description: Missing required field title
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TasksApiError'
    get:
      summary: List Tasks
      security:
        - ApiKeyAuth: []
      description: |
        List all tasks for your account, ordered by their position on the board.
        Optional query filters narrow the result by stage, priority, linked contact or deal,
        assignee, and due-date range.

        Pass updated_after instead to get only what CHANGED since your last sync.
        That is a different listing: it ignores the board filters, orders by when
        each task was last changed (oldest first) and pages with limit and cursor.
      tags:
        - Tasks
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
        - name: stage
          in: query
          required: false
          schema:
            type: string
          description: Only return tasks in this stage (column).
        - name: priority
          in: query
          required: false
          schema:
            type: string
            enum: [none, low, medium, high, urgent]
          description: Only return tasks with this priority.
        - name: contact_id
          in: query
          required: false
          schema:
            type: string
          description: Only return tasks linked to this contact.
        - name: deal_id
          in: query
          required: false
          schema:
            type: string
          description: Only return tasks linked to this deal.
        - name: assigned_to
          in: query
          required: false
          schema:
            type: string
          description: Only return tasks assigned to this team member.
        - name: due_before
          in: query
          required: false
          schema:
            type: string
          description: Only return tasks due before this ISO 8601 date-time.
        - name: due_after
          in: query
          required: false
          schema:
            type: string
          description: Only return tasks due after this ISO 8601 date-time.
        - name: updated_after
          in: query
          required: false
          schema:
            type: string
          description: >-
            Incremental sync. Returns only tasks changed at or after this moment,
            oldest change first, so you can pull just the delta instead of the whole
            board. Paginate it with limit and cursor. Accepts an ISO 8601 timestamp or
            epoch milliseconds; an unparseable value is a 400. updatedAfter is
            accepted as an alias. In this mode the board filters above (stage,
            priority, contact_id, deal_id, assigned_to, due_before, due_after) are
            ignored - a sync consumer wants the raw delta, not a filtered view.
          example: "2026-05-01T00:00:00Z"
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 50
          description: Page size. Only used with updated_after; the board listing is not paginated.
        - name: cursor
          in: query
          required: false
          schema:
            type: string
          description: >-
            Pagination cursor. Only used with updated_after - pass the next_cursor
            from the previous page, and stop when it comes back null.
      responses:
        '200':
          description: Tasks for the account
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  tasks:
                    type: array
                    items:
                      $ref: '#/components/schemas/Task'
                  next_cursor:
                    type: ['string', 'null']
                    description: >-
                      Cursor for the next page in updated_after mode, or null when
                      there are no more. Always null for the board listing.
        '400':
          description: updated_after is not a valid ISO 8601 timestamp
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TasksApiError'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TasksApiError'
  /tasks/search:
    post:
      summary: Search Tasks
      security:
        - ApiKeyAuth: []
      description: |
        Search your tasks by text. The search matches the query against task titles and
        descriptions (case-insensitive) and supports the same stage, priority, and contact
        filters as the list endpoint.
      tags:
        - Tasks
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - query
              properties:
                query:
                  type: string
                  description: Text to search for in task titles and descriptions.
                stage:
                  type: string
                  description: Only search tasks in this stage.
                priority:
                  type: string
                  enum: [none, low, medium, high, urgent]
                  description: Only search tasks with this priority.
                contact_id:
                  type: string
                  description: Only search tasks linked to this contact.
            examples:
              search:
                summary: Search for follow-up tasks
                value:
                  query: "follow up"
                  stage: "todo"
      responses:
        '200':
          description: Matching tasks
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  tasks:
                    type: array
                    items:
                      $ref: '#/components/schemas/Task'
        '400':
          description: Missing required field query
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TasksApiError'
  /tasks/reorder:
    post:
      summary: Reorder Tasks In Stage
      security:
        - ApiKeyAuth: []
      description: |
        Set the exact order of all tasks in one stage (column) by sending the full list of
        task IDs in the desired order. The first ID gets position 0, the second position 1,
        and so on. IDs that no longer exist in the stage are skipped, so a slightly stale
        list does not block the reorder. Up to 1000 IDs per request.
      tags:
        - Tasks
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - stage_id
                - ordered_task_ids
              properties:
                stage_id:
                  type: string
                  description: ID of the stage (column) whose tasks are being reordered.
                ordered_task_ids:
                  type: array
                  items:
                    type: string
                  description: All task IDs in the stage, in the desired top-to-bottom order.
            examples:
              reorder:
                summary: Reorder three tasks in a column
                value:
                  stage_id: "todo"
                  ordered_task_ids: ["task2", "task1", "task3"]
      responses:
        '200':
          description: Tasks reordered
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  stage_id:
                    type: string
                    description: ID of the reordered stage.
        '400':
          description: Missing stage_id or invalid ordered_task_ids
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TasksApiError'
  /tasks/stages:
    get:
      summary: Get Task Board Stages
      security:
        - ApiKeyAuth: []
      description: |
        Return the stage (column) configuration of your task board, including which stage
        is marked as the done column.
      tags:
        - Tasks
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
      responses:
        '200':
          description: The current stage configuration
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  task_stages:
                    type: array
                    items:
                      $ref: '#/components/schemas/TaskStage'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TasksApiError'
    put:
      summary: Replace Task Board Stages
      security:
        - ApiKeyAuth: []
      description: |
        Replace the entire stage (column) configuration of your task board in one call.
        Send the full list of stages; the previous configuration is overwritten. Each stage
        needs an id, name, hex color, and position. Mark exactly one stage with
        is_completed_stage set to true so completed tasks have a column to land in.
        A board can have up to 20 stages.
      tags:
        - Tasks
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - task_stages
              properties:
                task_stages:
                  type: array
                  items:
                    $ref: '#/components/schemas/TaskStage'
                  description: The full new stage configuration, replacing the previous one.
            examples:
              board:
                summary: A simple three-column board
                value:
                  task_stages:
                    - id: "todo"
                      name: "To Do"
                      color: "#3B82F6"
                      position: 0
                    - id: "in_progress"
                      name: "In Progress"
                      color: "#F59E0B"
                      position: 1
                    - id: "done"
                      name: "Done"
                      color: "#10B981"
                      position: 2
                      is_completed_stage: true
      responses:
        '200':
          description: The saved stage configuration
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  task_stages:
                    type: array
                    items:
                      $ref: '#/components/schemas/TaskStage'
        '400':
          description: Invalid stage configuration (missing fields, bad color, duplicate IDs, or too many stages)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TasksApiError'
  /tasks/types:
    get:
      summary: Get Task Types
      security:
        - ApiKeyAuth: []
      description: |
        Return the task type definitions configured for your task board, such as call,
        email, or any custom types you have added.
      tags:
        - Tasks
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
      responses:
        '200':
          description: The current task type configuration
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  task_types:
                    type: array
                    items:
                      $ref: '#/components/schemas/TaskTypeConfig'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TasksApiError'
  /tasks/contact/{contactId}:
    get:
      summary: List Tasks For Contact
      security:
        - ApiKeyAuth: []
      description: |
        List all tasks linked to a specific contact, ordered by their position on the board.
        Returns 404 when the contact does not exist on your account.
      tags:
        - Tasks
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
        - name: contactId
          in: path
          required: true
          schema:
            type: string
          description: ID of the contact whose tasks to list.
      responses:
        '200':
          description: Tasks linked to the contact
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  tasks:
                    type: array
                    items:
                      $ref: '#/components/schemas/Task'
        '404':
          description: Contact not found on this account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TasksApiError'
  /tasks/{taskId}:
    get:
      summary: Get Task
      security:
        - ApiKeyAuth: []
      description: Return a single task by its ID.
      tags:
        - Tasks
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
        - name: taskId
          in: path
          required: true
          schema:
            type: string
          description: ID of the task to fetch.
      responses:
        '200':
          description: The requested task
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  task:
                    $ref: '#/components/schemas/Task'
        '404':
          description: Task not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TasksApiError'
    put:
      summary: Update Task
      security:
        - ApiKeyAuth: []
      description: |
        Update one or more fields on a task. Only the fields you send are changed.
        Send null for due_date, contact_id, deal_id, or assigned_to to clear them.
      tags:
        - Tasks
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
        - name: taskId
          in: path
          required: true
          schema:
            type: string
          description: ID of the task to update.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                title:
                  type: string
                  description: New title.
                description:
                  type: string
                  description: New description.
                type:
                  type: string
                  description: New task type identifier.
                priority:
                  type: string
                  enum: [none, low, medium, high, urgent]
                  description: New priority.
                stage:
                  type: string
                  description: New stage (column) ID.
                due_date:
                  type: ['string', 'null']
                  description: New due date as an ISO 8601 string, or null to clear it.
                contact_id:
                  type: ['string', 'null']
                  description: ID of the contact to link, or null to unlink.
                deal_id:
                  type: ['string', 'null']
                  description: ID of the deal to link, or null to unlink.
                assigned_to:
                  type: ['string', 'null']
                  description: ID of the team member to assign, or null to unassign.
                linked_messages:
                  type: array
                  items:
                    type: string
                  description: New list of linked chat message IDs.
                tags:
                  type: array
                  items:
                    type: string
                  description: New list of labels.
                notes:
                  type: string
                  description: New internal notes.
            examples:
              update:
                summary: Change priority and due date
                value:
                  priority: "urgent"
                  due_date: "2026-07-15T10:00:00Z"
      responses:
        '200':
          description: Task updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  task_id:
                    type: string
                    description: ID of the updated task.
        '404':
          description: Task not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TasksApiError'
    delete:
      summary: Delete Task
      security:
        - ApiKeyAuth: []
      description: Permanently delete a task. This cannot be undone.
      tags:
        - Tasks
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
        - name: taskId
          in: path
          required: true
          schema:
            type: string
          description: ID of the task to delete.
      responses:
        '200':
          description: Task deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
        '404':
          description: Task not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TasksApiError'
  /tasks/{taskId}/complete:
    post:
      summary: Complete Task
      security:
        - ApiKeyAuth: []
      description: |
        Mark a task as completed. The task is moved to the stage marked as the done column
        on your board and its completion time is recorded. Optionally add closing notes.
        Requires a stage with is_completed_stage set to true in your board configuration.
      tags:
        - Tasks
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
        - name: taskId
          in: path
          required: true
          schema:
            type: string
          description: ID of the task to complete.
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                notes:
                  type: string
                  description: Optional closing notes saved on the task.
      responses:
        '200':
          description: Task completed
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  task_id:
                    type: string
                    description: ID of the completed task.
        '400':
          description: No done column configured on the board
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TasksApiError'
  /tasks/{taskId}/move:
    post:
      summary: Move Task
      security:
        - ApiKeyAuth: []
      description: |
        Move a task to another stage (column) and/or to a specific position within that stage.
        Other tasks in the source and destination columns are automatically renumbered so
        positions stay dense - exactly like dragging a card on the board.
      tags:
        - Tasks
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
        - name: taskId
          in: path
          required: true
          schema:
            type: string
          description: ID of the task to move.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - new_stage_id
                - new_position
              properties:
                new_stage_id:
                  type: string
                  description: ID of the destination stage (column). Use the current stage ID to reposition within the same column.
                new_position:
                  type: integer
                  minimum: 0
                  description: Zero-based target position within the destination stage. 0 places the task at the top.
            examples:
              move:
                summary: Move a task to the top of In Progress
                value:
                  new_stage_id: "in_progress"
                  new_position: 0
      responses:
        '200':
          description: Task moved
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  task_id:
                    type: string
                    description: ID of the moved task.
                  stage:
                    type: string
                    description: The stage the task is now in.
                  position:
                    type: integer
                    description: The position the task now occupies.
        '404':
          description: Task not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TasksApiError'
  /tasks/{taskId}/approve-faq:
    post:
      summary: Approve FAQ Suggestion
      security:
        - ApiKeyAuth: []
      description: |
        Approve an FAQ suggestion task. This creates a real FAQ entry from the suggested
        question and answer, links it to the originating campaign, and marks the task as
        completed. You can override the suggested question or answer in the request body.
        Only works on tasks of type faq_update.
      tags:
        - Tasks
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
        - name: taskId
          in: path
          required: true
          schema:
            type: string
          description: ID of the FAQ suggestion task to approve.
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                question:
                  type: string
                  description: Override for the FAQ question. Defaults to the question captured on the task.
                answer:
                  type: string
                  description: Override for the FAQ answer. Defaults to the answer captured on the task.
                send_follow_up:
                  type: boolean
                  default: false
                  description: |
                    When true, the AI sends the answer to the task's linked contact right away in that
                    contact's chat (as a natural follow-up message, not the raw FAQ text). The reply runs
                    under the agent or campaign that handles the contact; if none can be resolved the FAQ
                    is still created and follow_up_status explains why nothing was sent.
      responses:
        '200':
          description: FAQ created and task completed
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  faq_id:
                    type: string
                    description: ID of the newly created FAQ entry.
                  task_id:
                    type: string
                    description: ID of the approved task.
                  follow_up_status:
                    type: string
                    enum: [not_requested, published, queued, skipped_no_contact, skipped_no_campaign, skipped_error]
                    description: |
                      What happened to the follow-up. not_requested when send_follow_up was not true;
                      published when the AI reply was dispatched; queued when the bot was mid-reply on that
                      contact and the answer goes out as soon as it finishes; skipped_* when the task has no
                      linked contact, no agent/campaign could answer for it, or dispatch failed (the FAQ is
                      created in every case).
        '400':
          description: Task is not an FAQ suggestion, or no question/answer available
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TasksApiError'
  /webhooks:
    get:
      summary: List Webhook Subscriptions
      security:
        - ApiKeyAuth: []
      description: |
        Returns every webhook subscription configured on the account, including the events each one
        listens to and any tag filters. The returned id (or the exact name) can be used as the
        webhookId path parameter in the other webhook endpoints.
      tags:
        - Webhooks
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
      responses:
        '200':
          description: The account's webhook subscriptions
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  webhooks:
                    type: array
                    items:
                      $ref: '#/components/schemas/WebhookSubscription'
              example:
                success: true
                webhooks:
                  - id: "0"
                    name: "Order updates hook"
                    url: "https://hooks.example.com/incoming"
                    subscribed_to: ["Contact Created", "Replies"]
                    subscribed_to_tags: []
                    created_at: "2026-06-09T12:00:00.000Z"
        '401':
          description: Missing or invalid credentials
          content:
            application/json:
              example:
                success: false
                error_code: 401
                error: "Invalid API key"
        '403':
          description: Webhooks are not enabled for this account
          content:
            application/json:
              example:
                success: false
                error: "Webhooks are not enabled for this account"
    post:
      summary: Create Webhook Subscription
      security:
        - ApiKeyAuth: []
      description: |
        Creates a new webhook subscription. The URL must use HTTPS and be publicly reachable.
        The subscribed_to array selects which event types are delivered to the URL — use
        GET /webhooks/events to discover the valid values. Optionally pass subscribed_to_tags
        (tag IDs) to scope tag-based notifications to specific tags, and a name so the
        subscription can be addressed by a stable identifier later.
      tags:
        - Webhooks
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url, subscribed_to]
              properties:
                url:
                  type: string
                  description: HTTPS URL that will receive event payloads via POST.
                subscribed_to:
                  type: array
                  description: Event types to subscribe to. Must contain at least one valid event name.
                  items:
                    type: string
                name:
                  type: string
                  description: Optional display name. Also usable as the webhookId path parameter. Defaults to a timestamped name.
                subscribed_to_tags:
                  type: array
                  description: Optional tag IDs to scope tag-based notifications.
                  items:
                    type: string
                retries_enabled:
                  type: boolean
                  description: Retry failed deliveries (1m, 5m, 30m, 2h). Off by default. See the Retries section for the duplicate-delivery caveat.
                enabled:
                  type: boolean
                  description: Account on/off switch for the subscription. Defaults to true (on). Pass false to create it switched off.
                apply_to_sub_accounts:
                  type: boolean
                  description: Agency inheritance. Set true on an agency master account to also receive events from all of its sub-accounts on this one endpoint. Off by default; inert on accounts with no sub-accounts.
                generate_signing_secret:
                  type: boolean
                  description: |
                    When true, an HMAC signing secret is minted for the new subscription (turning signing on)
                    and returned once as a top-level signing_secret field in the response. Store it then — it
                    is never returned in full again by create/list/update; only GET /webhooks/{webhookId}/signing-secret
                    exposes it afterwards. You can also turn signing on later via POST /webhooks/{webhookId}/signing-secret.
            example:
              url: "https://hooks.example.com/incoming"
              subscribed_to: ["Contact Created", "Replies"]
              name: "Order updates hook"
      responses:
        '201':
          description: Subscription created
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  webhook_id:
                    type: string
                  signing_secret:
                    type: string
                    description: The newly minted signing secret, returned only when generate_signing_secret was true. Shown once — store it now.
                    examples: ["whsec_1a2b3c..."]
                  webhook:
                    $ref: '#/components/schemas/WebhookSubscription'
              example:
                success: true
                webhook_id: "1"
                webhook:
                  id: "1"
                  name: "Order updates hook"
                  url: "https://hooks.example.com/incoming"
                  subscribed_to: ["Contact Created", "Replies"]
                  subscribed_to_tags: []
                  created_at: "2026-06-09T12:00:00.000Z"
                  enabled: true
                  retries_enabled: false
                  apply_to_sub_accounts: false
                  signing_enabled: false
                  signing_secret_created_at: null
        '400':
          description: Validation error (missing url, empty or invalid subscribed_to, or a URL that is not allowed)
          content:
            application/json:
              example:
                success: false
                error: "Webhook URL must use https"
        '403':
          description: Webhooks are not enabled for this account
          content:
            application/json:
              example:
                success: false
                error: "Webhooks are not enabled for this account"
  /webhooks/events:
    get:
      summary: List Subscribable Event Types
      security:
        - ApiKeyAuth: []
      description: |
        Returns the full list of event types that webhook subscriptions can listen to. Use these
        exact strings in the subscribed_to field when creating or updating a subscription.
      tags:
        - Webhooks
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
      responses:
        '200':
          description: The valid event types
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  events:
                    type: array
                    items:
                      type: string
              example:
                success: true
                events:
                  - "Contact Created"
                  - "Human Alerted"
                  - "Appointment Booked"
                  - "Replies"
                  - "Reads"
                  - "Deliveries"
                  - "Credits Spent"
                  - "Credits Recharged"
                  - "Contact Paused"
                  - "Contact Do Not Disturb"
                  - "Contact Unarchived"
                  - "New Message"
                  - "Contact Resumed"
                  - "Chat Concluded"
                  - "Task Created"
                  - "Task Updated"
                  - "Task Completed"
                  - "Daily Summary Created"
        '401':
          description: Missing or invalid credentials
          content:
            application/json:
              example:
                success: false
                error_code: 401
                error: "Invalid API key"
        '403':
          description: Webhooks are not enabled for this account
          content:
            application/json:
              example:
                success: false
                error: "Webhooks are not enabled for this account"
  /webhooks/{webhookId}:
    put:
      summary: Update Webhook Subscription
      security:
        - ApiKeyAuth: []
      description: |
        Updates an existing webhook subscription. Provide at least one of url, subscribed_to, name
        or subscribed_to_tags; omitted fields keep their current values. When the URL changes,
        delivery for the new URL is automatically re-enabled so a previously failing endpoint gets
        a fresh start.
      tags:
        - Webhooks
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
        - name: webhookId
          in: path
          required: true
          description: The subscription id (from the id field) or its exact name.
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                url:
                  type: string
                  description: New HTTPS delivery URL.
                subscribed_to:
                  type: array
                  description: Replacement event list. Must contain at least one valid event name.
                  items:
                    type: string
                name:
                  type: string
                  description: New display name.
                subscribed_to_tags:
                  type: array
                  description: Replacement list of tag IDs for tag-based notifications.
                  items:
                    type: string
                retries_enabled:
                  type: boolean
                  description: Turn delivery retries on or off for this subscription.
                enabled:
                  type: boolean
                  description: Account on/off switch. Set false to switch the subscription off (suppress delivery), true to switch it back on. Signing is managed separately via the signing-secret routes, not here.
                apply_to_sub_accounts:
                  type: boolean
                  description: Turn agency inheritance on or off for this subscription. Omit to leave it unchanged.
            example:
              url: "https://hooks.example.com/v2/incoming"
              subscribed_to: ["Replies", "Chat Concluded"]
      responses:
        '200':
          description: Subscription updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  webhook_id:
                    type: string
                  webhook:
                    $ref: '#/components/schemas/WebhookSubscription'
              example:
                success: true
                webhook_id: "0"
                webhook:
                  id: "0"
                  name: "Order updates hook"
                  url: "https://hooks.example.com/v2/incoming"
                  subscribed_to: ["Replies", "Chat Concluded"]
                  subscribed_to_tags: []
                  created_at: "2026-06-09T12:00:00.000Z"
        '404':
          description: No subscription with that id or name exists on this account
          content:
            application/json:
              example:
                success: false
                error: "Webhook not found"
        '403':
          description: Webhooks are not enabled for this account
          content:
            application/json:
              example:
                success: false
                error: "Webhooks are not enabled for this account"
    delete:
      summary: Delete Webhook Subscription
      security:
        - ApiKeyAuth: []
      description: |
        Removes a webhook subscription so its URL stops receiving event payloads. Delivery-health
        counters for the URL are reset, so re-adding the same URL later starts with a clean record.
      tags:
        - Webhooks
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
        - name: webhookId
          in: path
          required: true
          description: The subscription id (from the id field) or its exact name.
          schema:
            type: string
      responses:
        '200':
          description: Subscription deleted
          content:
            application/json:
              example:
                success: true
        '404':
          description: No subscription with that id or name exists on this account
          content:
            application/json:
              example:
                success: false
                error: "Webhook not found"
        '403':
          description: Webhooks are not enabled for this account
          content:
            application/json:
              example:
                success: false
                error: "Webhooks are not enabled for this account"
  /webhooks/{webhookId}/test:
    post:
      summary: Send Test Payload
      security:
        - ApiKeyAuth: []
      description: |
        Sends a sample payload to the subscription's URL so you can verify your receiver end-to-end.
        Optionally pass an event to control which event type the sample payload simulates. Test
        deliveries never affect the subscription's health counters. The response reports whether the
        delivery succeeded; a failed test returns delivered:false together with the failure details
        instead of an error status.
      tags:
        - Webhooks
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
        - name: webhookId
          in: path
          required: true
          description: The subscription id (from the id field) or its exact name.
          schema:
            type: string
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                event:
                  type: string
                  description: Event type to simulate. Must be one of the values from GET /webhooks/events. Defaults to a delivery event.
            example:
              event: "Contact Created"
      responses:
        '200':
          description: Test attempted — check the delivered flag for the outcome
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  webhook_id:
                    type: string
                  delivered:
                    type: boolean
                    description: True when the receiver accepted the test payload.
                  failure_type:
                    type: string
                    description: Present when delivered is false. One of permanent, temporary, timeout, network, unknown.
                  status_code:
                    type: ['integer', 'null']
                    description: HTTP status code returned by the receiver when delivered is false, if available.
                  error_message:
                    type: string
                    description: Error message from the failed delivery attempt, when delivered is false.
              examples:
                delivered:
                  summary: Receiver accepted the payload
                  value:
                    success: true
                    webhook_id: "0"
                    delivered: true
                failed:
                  summary: Receiver rejected the payload
                  value:
                    success: true
                    webhook_id: "0"
                    delivered: false
                    failure_type: "permanent"
                    status_code: 404
                    error_message: "Request failed with status code 404"
        '404':
          description: No subscription with that id or name exists on this account
          content:
            application/json:
              example:
                success: false
                error: "Webhook not found"
        '403':
          description: Webhooks are not enabled for this account
          content:
            application/json:
              example:
                success: false
                error: "Webhooks are not enabled for this account"
  /webhooks/{webhookId}/health:
    get:
      summary: Get Webhook Delivery Health
      security:
        - ApiKeyAuth: []
      description: |
        Returns the delivery-health record for the subscription's URL: how many deliveries have
        succeeded and failed, whether delivery is currently paused after repeated failures, and the
        details of the most recent failure. Returns health:null when no deliveries have been
        attempted yet.
      tags:
        - Webhooks
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
        - name: webhookId
          in: path
          required: true
          description: The subscription id (from the id field) or its exact name.
          schema:
            type: string
      responses:
        '200':
          description: The delivery-health record for the subscription's URL
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  webhook_id:
                    type: string
                  url:
                    type: string
                  health:
                    oneOf:
                      - $ref: '#/components/schemas/WebhookHealthStatus'
                      - type: 'null'
              example:
                success: true
                webhook_id: "0"
                url: "https://hooks.example.com/incoming"
                health:
                  consecutive_failures: 0
                  total_failures: 2
                  total_successes: 120
                  is_disabled: false
                  disabled_at: null
                  disabled_reason: null
                  last_failure: null
                  last_success_at: "2026-06-09T12:00:00.000Z"
                  created_at: "2026-05-01T08:00:00.000Z"
                  updated_at: "2026-06-09T12:00:00.000Z"
        '404':
          description: No subscription with that id or name exists on this account
          content:
            application/json:
              example:
                success: false
                error: "Webhook not found"
        '403':
          description: Webhooks are not enabled for this account
          content:
            application/json:
              example:
                success: false
                error: "Webhooks are not enabled for this account"
  /webhooks/{webhookId}/reenable:
    post:
      summary: Re-enable Webhook Delivery
      security:
        - ApiKeyAuth: []
      description: |
        Resumes delivery for a webhook whose URL was paused automatically after repeated failures.
        Resets the paused flag and failure counters. No delivery attempt is made — use the test
        endpoint afterwards to confirm the receiver is healthy again.
      tags:
        - Webhooks
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
        - name: webhookId
          in: path
          required: true
          description: The subscription id (from the id field) or its exact name.
          schema:
            type: string
      responses:
        '200':
          description: Delivery re-enabled
          content:
            application/json:
              example:
                success: true
                webhook_id: "0"
        '404':
          description: No subscription with that id or name exists on this account
          content:
            application/json:
              example:
                success: false
                error: "Webhook not found"
        '403':
          description: Webhooks are not enabled for this account
          content:
            application/json:
              example:
                success: false
                error: "Webhooks are not enabled for this account"
  /webhooks/{webhookId}/signing-secret:
    get:
      summary: Read Webhook Signing Secret
      security:
        - ApiKeyAuth: []
      description: |
        Returns the subscription's HMAC signing secret so you can configure signature verification on
        your receiver. The secret is symmetric — we hold the same value your endpoint needs — so it is
        readable rather than write-once. When signing is off, signing_enabled is false and signing_secret
        is null.

        Signed deliveries carry an `X-Webhook-Signature` header (`v1=<hex>`), an HMAC-SHA256 of
        `<X-Webhook-Timestamp>.<raw request body>` keyed with this secret. Verify against the raw
        body, reject stale timestamps, and compare in constant time.

        Requires the Integrations edit permission — including this GET, since the secret can forge
        deliveries and is not exposed to read-only roles.
      tags:
        - Webhooks
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
        - name: webhookId
          in: path
          required: true
          description: The subscription id (from the id field) or its exact name.
          schema:
            type: string
      responses:
        '200':
          description: The subscription's signing state
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  webhook_id:
                    type: string
                  signing_enabled:
                    type: boolean
                  signing_secret:
                    type: ['string', 'null']
                    description: The HMAC signing secret, or null when signing is off.
                  signing_secret_created_at:
                    type: ['string', 'null']
                    format: date-time
              examples:
                on:
                  summary: Signing is on
                  value:
                    success: true
                    webhook_id: "0"
                    signing_enabled: true
                    signing_secret: "whsec_1a2b3c..."
                    signing_secret_created_at: "2026-07-15T09:30:00.000Z"
                off:
                  summary: Signing is off
                  value:
                    success: true
                    webhook_id: "0"
                    signing_enabled: false
                    signing_secret: null
                    signing_secret_created_at: null
        '404':
          description: No subscription with that id or name exists on this account
          content:
            application/json:
              example:
                success: false
                error: "Webhook not found"
        '403':
          description: Webhooks are not enabled for this account, or the API key lacks the Integrations edit permission
          content:
            application/json:
              example:
                success: false
                error: "Webhooks are not enabled for this account"
    post:
      summary: Generate or Rotate Webhook Signing Secret
      security:
        - ApiKeyAuth: []
      description: |
        Mints a new signing secret for the subscription, or replaces the existing one. Creating a secret
        is how signing is turned on. The new secret is returned in the response.

        Rotation is a hard cutover, not a grace period: the very next delivery is signed with the new
        secret only. When rotating a live endpoint, accept both the old and new secret briefly so
        deliveries in flight during the change still verify.

        Requires the Integrations edit permission.
      tags:
        - Webhooks
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
        - name: webhookId
          in: path
          required: true
          description: The subscription id (from the id field) or its exact name.
          schema:
            type: string
      responses:
        '200':
          description: Signing secret generated or rotated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  webhook_id:
                    type: string
                  signing_enabled:
                    type: boolean
                  signing_secret:
                    type: string
                    description: The newly generated signing secret.
                  signing_secret_created_at:
                    type: ['string', 'null']
                    format: date-time
              example:
                success: true
                webhook_id: "0"
                signing_enabled: true
                signing_secret: "whsec_9f8e7d..."
                signing_secret_created_at: "2026-07-15T10:00:00.000Z"
        '404':
          description: No subscription with that id or name exists on this account
          content:
            application/json:
              example:
                success: false
                error: "Webhook not found"
        '403':
          description: Webhooks are not enabled for this account, or the API key lacks the Integrations edit permission
          content:
            application/json:
              example:
                success: false
                error: "Webhooks are not enabled for this account"
    delete:
      summary: Turn Off Webhook Signing
      security:
        - ApiKeyAuth: []
      description: |
        Removes the subscription's signing secret, turning signing off. Subsequent deliveries carry no
        signature headers, exactly as the webhook behaved before signing was enabled.

        Requires the Integrations edit permission.
      tags:
        - Webhooks
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
        - name: webhookId
          in: path
          required: true
          description: The subscription id (from the id field) or its exact name.
          schema:
            type: string
      responses:
        '200':
          description: Signing turned off
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  webhook_id:
                    type: string
                  signing_enabled:
                    type: boolean
              example:
                success: true
                webhook_id: "0"
                signing_enabled: false
        '404':
          description: No subscription with that id or name exists on this account
          content:
            application/json:
              example:
                success: false
                error: "Webhook not found"
        '403':
          description: Webhooks are not enabled for this account, or the API key lacks the Integrations edit permission
          content:
            application/json:
              example:
                success: false
                error: "Webhooks are not enabled for this account"
  /whatsapp-templates/{templateId}:
    get:
      summary: Get WhatsApp Template
      security:
        - ApiKeyAuth: []
      description: Retrieve a single WhatsApp template by its ID, including body, variables, approval status and timestamps.
      tags:
        - WhatsApp Templates
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: templateId
          in: path
          required: true
          schema:
            type: string
          description: ID of the template
        - name: sub_account_id
          in: query
          required: false
          description: Optional. Read a template on a managed account you administer.
          schema:
            type:
              - string
              - "null"
      responses:
        '200':
          description: Template found
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  template:
                    $ref: '#/components/schemas/WhatsAppTemplateDetail'
              example:
                success: true
                template:
                  id: template123
                  name: welcome_message
                  body: Hi {{first_name}}, thanks for reaching out!
                  language: en
                  variables:
                    - first_name
                  status: approved
                  sid: HXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
                  type: general
                  campaign_id: campaign123
                  date_created: '2026-06-01T10:00:00.000Z'
                  date_updated: '2026-06-02T08:30:00.000Z'
        '404':
          description: Template not found
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  error:
                    type: string
              example:
                success: false
                error: Template not found
        '403':
          description: WhatsApp templates are not enabled for this account
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["WhatsApp templates are not enabled for this account"]
    put:
      summary: Update WhatsApp Template
      security:
        - ApiKeyAuth: []
      description: |
        Edit a template that has not been approved yet. Only templates with status draft or
        rejected can be edited. Provide any combination of name, body, language and variables -
        only the supplied fields are changed. Editing does not submit the template for review;
        use the submit endpoint afterwards.
      tags:
        - WhatsApp Templates
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: templateId
          in: path
          required: true
          schema:
            type: string
          description: ID of the template
        - name: sub_account_id
          in: query
          required: false
          description: Optional. Update a template on a managed account you administer. May also be sent as a field in the JSON body.
          schema:
            type:
              - string
              - "null"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                  description: New template name
                body:
                  type: string
                  maxLength: 1024
                  description: New template body text, up to 1024 characters
                language:
                  type: string
                  description: New template language code
                variables:
                  type: array
                  description: New ordered list of variable names
                  items:
                    type: string
            example:
              body: Hi {{first_name}}, here is an update for you.
              variables:
                - first_name
      responses:
        '200':
          description: Template updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  template_id:
                    type: string
              example:
                success: true
                template_id: template123
        '400':
          description: Validation error (template not editable in its current status, no fields supplied, or invalid field values)
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  error:
                    type: string
              example:
                success: false
                error: No fields to update
        '403':
          description: WhatsApp templates are not enabled for this account
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["WhatsApp templates are not enabled for this account"]
    delete:
      summary: Delete WhatsApp Template
      security:
        - ApiKeyAuth: []
      description: |
        Delete a template from your account. Only the stored record is removed - content that
        was already approved by WhatsApp may remain registered with the messaging provider.
        If a campaign still references this template, re-point the campaign to another template
        before deleting, otherwise sends that rely on it will fail.
      tags:
        - WhatsApp Templates
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: templateId
          in: path
          required: true
          schema:
            type: string
          description: ID of the template
        - name: sub_account_id
          in: query
          required: false
          description: Optional. Delete a template on a managed account you administer.
          schema:
            type:
              - string
              - "null"
      responses:
        '200':
          description: Template deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  template_id:
                    type: string
                  note:
                    type: string
              example:
                success: true
                template_id: template123
                note: The template record was removed from your account. Content already approved by WhatsApp may remain registered with the messaging provider.
        '404':
          description: Template not found
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  error:
                    type: string
              example:
                success: false
                error: Template not found

        '403':
          description: WhatsApp templates are not enabled for this account
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["WhatsApp templates are not enabled for this account"]
  /whatsapp-templates/{templateId}/status:
    get:
      summary: Get WhatsApp Template Approval Status
      security:
        - ApiKeyAuth: []
      description: |
        Lightweight polling endpoint that returns the current approval status of a template.
        The status is read from the stored record, which is refreshed periodically in the
        background - a very recent approval or rejection can take a short while to appear.
      tags:
        - WhatsApp Templates
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: templateId
          in: path
          required: true
          schema:
            type: string
          description: ID of the template
        - name: sub_account_id
          in: query
          required: false
          description: Optional. Check the status of a template on a managed account you administer.
          schema:
            type:
              - string
              - "null"
      responses:
        '200':
          description: Current approval status
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  template_id:
                    type: string
                  name:
                    type: ['string', 'null']
                  status:
                    type: ['string', 'null']
                    description: 'Approval status: draft, received, pending, approved, or rejected'
                  sid:
                    type: ['string', 'null']
                  rejection_reason:
                    type: ['string', 'null']
                  date_updated:
                    type: ['string', 'null']
                    format: date-time
              example:
                success: true
                template_id: template123
                name: welcome_message
                status: approved
                sid: HXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
                rejection_reason: null
                date_updated: '2026-06-02T08:30:00.000Z'
        '404':
          description: Template not found
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  error:
                    type: string
              example:
                success: false
                error: Template not found

        '403':
          description: WhatsApp templates are not enabled for this account
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["WhatsApp templates are not enabled for this account"]
  /whatsapp-templates/{templateId}/submit:
    post:
      summary: Submit WhatsApp Template for Approval
      security:
        - ApiKeyAuth: []
      description: |
        Submit a draft or rejected template for approval. Templates linked to campaigns whose
        channels do not require an external review step are approved immediately; all other
        templates are sent for WhatsApp review and the returned status (usually received or
        pending) is stored on the template. Poll the status endpoint to follow the review
        outcome. Follow-up templates must declare and use their required variables (a first
        name placeholder, plus a personal context placeholder for smart follow-ups) before
        they can be submitted.
      tags:
        - WhatsApp Templates
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: templateId
          in: path
          required: true
          schema:
            type: string
          description: ID of the template
        - name: sub_account_id
          in: query
          required: false
          description: Optional. Submit a template on a managed account you administer. May also be sent as a field in the JSON body.
          schema:
            type:
              - string
              - "null"
      responses:
        '200':
          description: Template submitted (or auto-approved)
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  template_id:
                    type: string
                  status:
                    type: string
                    description: 'Resulting status: received, pending, approved, or rejected'
                  sid:
                    type: ['string', 'null']
                    description: Content ID when the template was submitted for review
                  message:
                    type: string
              example:
                success: true
                template_id: template123
                status: pending
                sid: HXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
        '400':
          description: Validation error (template not in a submittable status, missing required fields or variables, or the content was rejected)
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  error:
                    type: string
              example:
                success: false
                error: 'Template cannot be submitted - current status: approved. Only draft or rejected templates can be submitted.'
        '403':
          description: WhatsApp templates are not enabled for this account
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["WhatsApp templates are not enabled for this account"]
  /channels/line:
    post:
      security:
        - ApiKeyAuth: []
      summary: Connect a LINE Official Account
      description: Validates the submitted channel secret and channel access token against the provider, then stores the connection and returns the resolved bot user id and the webhook URL to configure. The channel secret and access token are never returned in any response. Re-submitting the same Official Account's credentials updates the stored connection.
      tags:
        - LINE Channel
      parameters:
        - $ref: "#/components/parameters/ApiKeyParam"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/LineConnectRequest"
      responses:
        "200":
          description: The Official Account was connected.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LineConnectResponse"
        "400":
          description: Missing or invalid fields, or the credentials could not be validated.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LineErrorResponse"
        "401":
          description: Authentication required or invalid.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LineErrorResponse"
        "403":
          description: The target account's plan does not include a channel tier.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LineErrorResponse"
        "409":
          description: This Official Account is already connected to another account.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LineErrorResponse"
  /channels/line/{botUserId}/status:
    get:
      security:
        - ApiKeyAuth: []
      summary: Get LINE connection status
      description: Returns the stored connection state for a connected Official Account. The status reflects the state captured at connect time; this channel has no live status endpoint. Credentials are never included.
      tags:
        - LINE Channel
      parameters:
        - $ref: "#/components/parameters/ApiKeyParam"
        - name: botUserId
          in: path
          required: true
          schema:
            type: string
          description: The connection's bot user id, as returned when connecting.
        - name: sub_account_id
          in: query
          required: false
          schema:
            type:
              - string
              - "null"
          description: Optional. Read the connection on a managed account owned by the caller.
      responses:
        "200":
          description: The stored connection state.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LineStatusResponse"
        "401":
          description: Authentication required or invalid.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LineErrorResponse"
        "404":
          description: No connection with that id exists on the account.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LineErrorResponse"
  /channels/line/{botUserId}/verify-webhook:
    post:
      security:
        - ApiKeyAuth: []
      summary: Verify a LINE connection
      description: Re-validates the stored access token against the provider and refreshes the cached chat-mode flag. Useful after the customer finishes configuring the webhook URL in the provider console. Does not return credentials.
      tags:
        - LINE Channel
      parameters:
        - $ref: "#/components/parameters/ApiKeyParam"
        - name: botUserId
          in: path
          required: true
          schema:
            type: string
          description: The connection's bot user id.
        - name: sub_account_id
          in: query
          required: false
          schema:
            type:
              - string
              - "null"
          description: Optional. Verify a connection on a managed account owned by the caller.
      responses:
        "200":
          description: The verification result.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LineVerifyWebhookResponse"
        "401":
          description: Authentication required or invalid.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LineErrorResponse"
        "404":
          description: No connection with that id exists on the account.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LineErrorResponse"
        "502":
          description: The messaging provider could not be reached to verify the connection.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LineErrorResponse"
  /channels/line/{botUserId}:
    delete:
      security:
        - ApiKeyAuth: []
      summary: Disconnect a LINE Official Account
      description: Removes the connection and its inbound routing entry. After disconnecting, inbound events for this Official Account will no longer be processed.
      tags:
        - LINE Channel
      parameters:
        - $ref: "#/components/parameters/ApiKeyParam"
        - name: botUserId
          in: path
          required: true
          schema:
            type: string
          description: The connection's bot user id.
        - name: sub_account_id
          in: query
          required: false
          schema:
            type:
              - string
              - "null"
          description: Optional. Disconnect a connection on a managed account owned by the caller.
      responses:
        "200":
          description: The Official Account was disconnected.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LineDisconnectResponse"
        "401":
          description: Authentication required or invalid.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LineErrorResponse"
        "404":
          description: No connection with that id exists on the account.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LineErrorResponse"
  /channels/meta/connect:
    post:
      security:
        - ApiKeyAuth: []
      summary: Start the Instagram & Messenger connection
      description: Begins the connection flow and returns a consent URL the end user must open in a browser, plus a one-time token correlating the attempt. No credentials are accepted here; authorization happens entirely in the browser and is stored server-side.
      tags:
        - Instagram & Messenger Connection
      parameters:
        - $ref: "#/components/parameters/ApiKeyParam"
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/MetaConnectRequest"
      responses:
        "200":
          description: Consent URL and correlation token issued.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MetaConnectResponse"
        "401":
          description: Authentication required or invalid.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MetaConnectError"
        "404":
          description: Targeted managed account not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MetaConnectError"
  /channels/meta/status:
    get:
      security:
        - ApiKeyAuth: []
      summary: Poll the Instagram & Messenger connection status
      description: Returns the current step of the connection flow, the connectable pages once they load, and the selected page. Credentials are never included.
      tags:
        - Instagram & Messenger Connection
      parameters:
        - $ref: "#/components/parameters/ApiKeyParam"
        - name: sub_account_id
          in: query
          required: false
          description: Optionally target a managed account instead of the calling account.
          schema:
            type: string
      responses:
        "200":
          description: Current connection status.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MetaStatusResponse"
        "401":
          description: Authentication required or invalid.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MetaConnectError"
        "404":
          description: Targeted managed account not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MetaConnectError"
  /channels/meta/pages:
    get:
      security:
        - ApiKeyAuth: []
      summary: List connectable pages
      description: Lists the pages available to connect after authorization completes, each stripped of any credential. Returns an empty list until pages have loaded.
      tags:
        - Instagram & Messenger Connection
      parameters:
        - $ref: "#/components/parameters/ApiKeyParam"
        - name: sub_account_id
          in: query
          required: false
          description: Optionally target a managed account instead of the calling account.
          schema:
            type: string
      responses:
        "200":
          description: Connectable pages.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MetaPagesResponse"
        "401":
          description: Authentication required or invalid.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MetaConnectError"
        "404":
          description: Targeted managed account not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MetaConnectError"
  /channels/meta/posts:
    get:
      security:
        - ApiKeyAuth: []
      summary: List recent Instagram media or Facebook Page posts
      description: >-
        Lists the connected account's recent Instagram media or Facebook Page posts so a
        comment-to-DM entry point can be pointed at specific posts without pasting raw ids.
        The returned ids are the raw Graph ids and are exactly the values a "Specific posts"
        entry point expects. A missing, unlinked, or expired connection is reported as
        200 with connected=false (never an error), so the caller can prompt the account
        holder to reconnect.
      tags:
        - Instagram & Messenger Connection
      parameters:
        - $ref: "#/components/parameters/ApiKeyParam"
        - name: platform
          in: query
          required: true
          description: Which surface to list.
          schema:
            type: string
            enum:
              - instagram
              - facebook
        - name: limit
          in: query
          required: false
          description: Page size, 1-50. Defaults to 25.
          schema:
            type: integer
            minimum: 1
            maximum: 50
            default: 25
        - name: after
          in: query
          required: false
          description: Opaque pagination cursor returned as nextCursor by a previous call.
          schema:
            type: string
        - name: sub_account_id
          in: query
          required: false
          description: Optionally target a managed account instead of the calling account.
          schema:
            type: string
      responses:
        "200":
          description: The requested page of posts, or a not-connected result.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MetaPostsResponse"
        "400":
          description: Missing or invalid platform/limit.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MetaConnectError"
        "401":
          description: Authentication required or invalid.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MetaConnectError"
        "404":
          description: Targeted managed account not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MetaConnectError"
        "502":
          description: Meta rejected the request for a reason other than an expired credential.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MetaConnectError"
  /channels/meta/select-page:
    post:
      security:
        - ApiKeyAuth: []
      summary: Select the page to connect
      description: Selects which page to connect for Instagram and Messenger. Supply only the page id; the page credential is resolved server-side and is never required from, nor returned to, the caller. Selecting a page connects both Instagram and Messenger for that page.
      tags:
        - Instagram & Messenger Connection
      parameters:
        - $ref: "#/components/parameters/ApiKeyParam"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/MetaSelectPageRequest"
      responses:
        "200":
          description: Page selected and channel connected.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MetaSelectPageResponse"
        "400":
          description: Missing or invalid request fields.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MetaConnectError"
        "401":
          description: Authentication required or invalid.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MetaConnectError"
        "404":
          description: Page not found in the connectable list, or targeted managed account not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MetaConnectError"
        "409":
          description: The page is already connected to another account.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MetaConnectError"
  /channels/meta:
    delete:
      security:
        - ApiKeyAuth: []
      summary: Disconnect Instagram & Messenger
      description: "Disconnects Instagram and Messenger by clearing the selected page. Idempotent: succeeds even when no page is currently selected."
      tags:
        - Instagram & Messenger Connection
      parameters:
        - $ref: "#/components/parameters/ApiKeyParam"
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/MetaConnectRequest"
      responses:
        "200":
          description: Channel disconnected.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MetaDisconnectResponse"
        "401":
          description: Authentication required or invalid.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MetaConnectError"
        "404":
          description: Targeted managed account not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MetaConnectError"
  /phone-numbers/available:
    get:
      summary: Search Available Phone Numbers
      security:
        - ApiKeyAuth: []
      description: Searches for purchasable phone numbers in a country and returns each number's one-time purchase cost and recurring monthly cost in credits. The first search for a new account provisions the account's underlying messaging resources, so the initial call may take slightly longer.
      tags:
        - Phone Numbers
      parameters:
        - $ref: "#/components/parameters/ApiKeyParam"
        - name: country_code
          in: query
          required: true
          description: ISO 3166-1 alpha-2 country code to search in.
          schema:
            type: string
            minLength: 2
            maxLength: 2
          example: US
        - name: type
          in: query
          required: false
          description: Optional preferred number class. Both classes may be returned.
          schema:
            type: string
            enum:
              - local
              - mobile
        - name: sub_account_id
          in: query
          required: false
          description: Optional. Search on behalf of a managed account you administer.
          schema:
            type: string
      responses:
        "200":
          description: The available numbers with pricing.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AvailablePhoneNumbersResponse"
              example:
                success: true
                phone_numbers:
                  - phone_number: "+14155551234"
                    purchase_credits: 11.5
                    monthly_credits: 11.5
                    cost_usd: 1.15
        "400":
          description: Missing or invalid country_code or type filter.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PhoneNumberPurchaseErrorResponse"
              example:
                success: false
                error: "Missing required query parameter: country_code"
        "401":
          description: Authentication required.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PhoneNumberPurchaseErrorResponse"
        "404":
          description: The referenced managed account could not be resolved.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PhoneNumberPurchaseErrorResponse"
  /channels/telegram/connect:
    post:
      security:
        - ApiKeyAuth: []
      summary: Start a connection session
      description: Begins a connection session for an account phone number. In code mode the connection service sends a one-time login code to the account and reports code_required; in qr mode it returns a login token and QR URL to display. Poll the status endpoint until the channel reports connected.
      tags:
        - Telegram Connection
      parameters:
        - $ref: "#/components/parameters/ApiKeyParam"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/TelegramConnectRequest"
      responses:
        "200":
          description: Session started.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TelegramConnectResponse"
        "400":
          description: Invalid or missing request fields.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "403":
          description: This channel is not enabled for the account, or the plan lacks a channels tier.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "409":
          description: This account is already connected elsewhere.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /channels/telegram/connect/{phoneNumber}/verify-code:
    post:
      security:
        - ApiKeyAuth: []
      summary: Submit the login code
      description: Submits the one-time login code for a pending session. On success the status becomes connected; when the account has two-factor enabled the status becomes password_required and the password must be submitted next.
      tags:
        - Telegram Connection
      parameters:
        - $ref: "#/components/parameters/ApiKeyParam"
        - name: phoneNumber
          in: path
          required: true
          description: The account phone number being connected, in E.164 format.
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/TelegramVerifyCodeRequest"
      responses:
        "200":
          description: Code accepted.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TelegramVerifyResponse"
        "400":
          description: Missing or invalid login code.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "403":
          description: This channel is not enabled for the account.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /channels/telegram/connect/{phoneNumber}/verify-password:
    post:
      security:
        - ApiKeyAuth: []
      summary: Submit the two-factor password
      description: Submits the two-factor password for a session that reported password_required. On success the status becomes connected and the channel is live.
      tags:
        - Telegram Connection
      parameters:
        - $ref: "#/components/parameters/ApiKeyParam"
        - name: phoneNumber
          in: path
          required: true
          description: The account phone number being connected, in E.164 format.
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/TelegramVerifyPasswordRequest"
      responses:
        "200":
          description: Password accepted.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TelegramVerifyResponse"
        "400":
          description: Missing or invalid password.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "403":
          description: This channel is not enabled for the account.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /channels/telegram/connect/{phoneNumber}/status:
    get:
      security:
        - ApiKeyAuth: []
      summary: Get live connection status
      description: Returns the live connection status for a session, polled from the connection service. Poll this until the status is connected or an error/disconnected status. Reports not_initialized when no session exists.
      tags:
        - Telegram Connection
      parameters:
        - $ref: "#/components/parameters/ApiKeyParam"
        - name: phoneNumber
          in: path
          required: true
          description: The account phone number, in E.164 format.
          schema:
            type: string
        - name: sub_account_id
          in: query
          required: false
          description: Optional managed-account identifier.
          schema:
            type:
              - string
              - "null"
      responses:
        "200":
          description: Current connection status.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TelegramStatusResponse"
        "404":
          description: No connection record exists for this phone number.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /channels/telegram/{phoneNumber}:
    delete:
      security:
        - ApiKeyAuth: []
      summary: Disconnect the channel
      description: "Tears down the connection session and removes the account record. Idempotent: the record is removed even when the connection service is unreachable, so repeated calls succeed."
      tags:
        - Telegram Connection
      parameters:
        - $ref: "#/components/parameters/ApiKeyParam"
        - name: phoneNumber
          in: path
          required: true
          description: The account phone number to disconnect, in E.164 format.
          schema:
            type: string
        - name: sub_account_id
          in: query
          required: false
          description: Optional managed-account identifier.
          schema:
            type:
              - string
              - "null"
      responses:
        "200":
          description: Channel removed.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TelegramDisconnectResponse"
        "404":
          description: No connection record exists for this phone number.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /channels/instagram-private/connect:
    post:
      security:
        - ApiKeyAuth: []
      summary: Start an Instagram connection
      description: >-
        Begins a connection for an Instagram account using its username and
        password. The credentials are relayed directly to Instagram via the
        connection worker and are never stored. The worker reports connected,
        two_factor_required, or challenge_required. The response also returns a
        hosted connect_url you can hand to the account holder so they enter their
        own credentials instead of you handling their password.
      tags:
        - Instagram Personal Account Connection
      parameters:
        - $ref: "#/components/parameters/ApiKeyParam"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/InstagramPrivateConnectRequest"
      responses:
        "200":
          description: Connection started.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/InstagramPrivateConnectResponse"
        "400":
          description: Invalid or missing request fields.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "403":
          description: This channel is not enabled for the account, or the plan lacks a channels tier.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "409":
          description: This Instagram account is already connected elsewhere.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /channels/instagram-private/connect/{id}/verify-2fa:
    post:
      security:
        - ApiKeyAuth: []
      summary: Submit the two-factor code
      description: Submits the Instagram two-factor code for a session that reported two_factor_required. On success the status becomes connected; Instagram may instead return challenge_required, in which case submit the challenge code next.
      tags:
        - Instagram Personal Account Connection
      parameters:
        - $ref: "#/components/parameters/ApiKeyParam"
        - name: id
          in: path
          required: true
          description: The Instagram username (connection identifier).
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/InstagramPrivateVerifyRequest"
      responses:
        "200":
          description: Code accepted.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/InstagramPrivateVerifyResponse"
        "400":
          description: Missing or invalid code.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "403":
          description: This channel is not enabled for the account.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /channels/instagram-private/connect/{id}/verify-challenge:
    post:
      security:
        - ApiKeyAuth: []
      summary: Submit the checkpoint confirmation code
      description: Submits the Instagram checkpoint confirmation code for a session that reported challenge_required. On success the status becomes connected; Instagram may instead return two_factor_required, in which case submit the two-factor code next.
      tags:
        - Instagram Personal Account Connection
      parameters:
        - $ref: "#/components/parameters/ApiKeyParam"
        - name: id
          in: path
          required: true
          description: The Instagram username (connection identifier).
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/InstagramPrivateVerifyRequest"
      responses:
        "200":
          description: Code accepted.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/InstagramPrivateVerifyResponse"
        "400":
          description: Missing or invalid code.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "403":
          description: This channel is not enabled for the account.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /channels/instagram-private/connect/{id}/status:
    get:
      security:
        - ApiKeyAuth: []
      summary: Get live connection status
      description: Returns the live connection status for a session, polled from the connection worker. Poll this until the status is connected or an error/disconnected status. Reports not_initialized when no session exists.
      tags:
        - Instagram Personal Account Connection
      parameters:
        - $ref: "#/components/parameters/ApiKeyParam"
        - name: id
          in: path
          required: true
          description: The Instagram username (connection identifier).
          schema:
            type: string
        - name: sub_account_id
          in: query
          required: false
          description: Optional managed-account identifier.
          schema:
            type:
              - string
              - "null"
      responses:
        "200":
          description: Current connection status.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/InstagramPrivateStatusResponse"
        "404":
          description: No connection record exists for this account.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /channels/instagram-private/{id}:
    delete:
      security:
        - ApiKeyAuth: []
      summary: Disconnect the channel
      description: "Tears down the connection session and removes the account record. Idempotent: the record is removed even when the connection worker is unreachable, so repeated calls succeed."
      tags:
        - Instagram Personal Account Connection
      parameters:
        - $ref: "#/components/parameters/ApiKeyParam"
        - name: id
          in: path
          required: true
          description: The Instagram username to disconnect.
          schema:
            type: string
        - name: sub_account_id
          in: query
          required: false
          description: Optional managed-account identifier.
          schema:
            type:
              - string
              - "null"
      responses:
        "200":
          description: Channel removed.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/InstagramPrivateDisconnectResponse"
        "404":
          description: No connection record exists for this account.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /channels/instagram-login/token:
    put:
      security:
        - ApiKeyAuth: []
      summary: Push an Instagram access token
      description: >-
        Stores an Instagram access token your OWN Meta app already obtained, so
        the account is connected without a DM Champ consent redirect. The token
        is checked live against Instagram, which must confirm it belongs to the
        ig_user_id you sent and that the account is a Professional (Business or
        Creator) one; the account is then subscribed to your app's webhooks. The
        response returns the webhook URL to configure in your Meta app. Pushing
        the same Instagram account again is the refresh path and simply replaces
        the stored token, expiry and username. Tokens are never refreshed for
        you, so push a fresh one before the recorded expiry passes.
      tags:
        - Instagram Login Connection
      parameters:
        - $ref: "#/components/parameters/ApiKeyParam"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/InstagramLoginTokenRequest"
      responses:
        "200":
          description: Token stored; the channel is connected.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/InstagramLoginTokenResponse"
        "400":
          description: Missing or invalid fields, a token Instagram rejected, an ig_user_id that does not match the token, or a webhook subscription your app could not complete.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "403":
          description: The caller is not allowed to edit integrations on this account.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "409":
          description: This Instagram account is already connected on another account, or on this one through the Facebook Page flow. Disconnect it there first.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
    delete:
      security:
        - ApiKeyAuth: []
      summary: Disconnect the pushed Instagram token
      description: "Removes the stored Instagram credential and unsubscribes the account from your app's webhooks. Idempotent: an already dead or already removed connection still answers 200, with disconnected set to false when there was nothing to remove."
      tags:
        - Instagram Login Connection
      parameters:
        - $ref: "#/components/parameters/ApiKeyParam"
        - name: sub_account_id
          in: query
          required: false
          description: Optional managed-account identifier.
          schema:
            type:
              - string
              - "null"
      responses:
        "200":
          description: Connection removed (or there was none).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/InstagramLoginDisconnectResponse"
        "403":
          description: The caller is not allowed to edit integrations on this account.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /channels/whatsapp/connect:
    post:
      security:
        - ApiKeyAuth: []
      tags:
        - WhatsApp Business Connection
      summary: Start a WhatsApp Business connection
      description: Prepares a WhatsApp Business connection for a phone number and returns a consent URL. The account holder must open the returned oauth_url in a browser to authorize the connection; once authorized, the connection completes in the background. Poll the status endpoint until the number reports ONLINE. The phone number must already exist on the account before calling this endpoint.
      parameters:
        - $ref: "#/components/parameters/ApiKeyParam"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/WhatsAppConnectRequest"
      responses:
        "200":
          description: Connection started; open oauth_url in a browser to continue.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WhatsAppConnectResponse"
        "400":
          description: Missing or invalid request fields.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WhatsAppConnectError"
        "401":
          description: Authentication required.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WhatsAppConnectError"
        "404":
          description: The phone number or targeted account was not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WhatsAppConnectError"
        "500":
          description: The messaging account could not be set up, or an unexpected error occurred.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WhatsAppConnectError"
  /channels/whatsapp/connect/{phoneNumber}/status:
    get:
      security:
        - ApiKeyAuth: []
      tags:
        - WhatsApp Business Connection
      summary: Get WhatsApp Business connection status
      description: Returns the current connection status for a phone number. When a sender is registered the status is checked against the provider in real time; otherwise the last known status is returned. Poll this endpoint after opening the consent URL until the status becomes ONLINE.
      parameters:
        - $ref: "#/components/parameters/ApiKeyParam"
        - name: phoneNumber
          in: path
          required: true
          description: The phone number, in E.164 format.
          schema:
            type: string
          example: "+14155551234"
        - name: sub_account_id
          in: query
          required: false
          description: Identifier of a managed account whose number is being checked. Omit for the calling account.
          schema:
            type:
              - string
              - "null"
      responses:
        "200":
          description: The current connection status.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WhatsAppConnectStatusResponse"
        "400":
          description: Missing phone number parameter.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WhatsAppConnectError"
        "401":
          description: Authentication required.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WhatsAppConnectError"
        "404":
          description: The phone number or targeted account was not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WhatsAppConnectError"
        "500":
          description: An unexpected error occurred.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WhatsAppConnectError"
  /channels/whatsapp/{phoneNumber}:
    delete:
      security:
        - ApiKeyAuth: []
      tags:
        - WhatsApp Business Connection
      summary: Disconnect a WhatsApp Business number
      description: Disconnects WhatsApp Business for a phone number by clearing the stored authorization. The phone number record itself is preserved, so the number can be reconnected later.
      parameters:
        - $ref: "#/components/parameters/ApiKeyParam"
        - name: phoneNumber
          in: path
          required: true
          description: The phone number, in E.164 format.
          schema:
            type: string
          example: "+14155551234"
        - name: sub_account_id
          in: query
          required: false
          description: Identifier of a managed account whose number is being disconnected. Omit for the calling account.
          schema:
            type:
              - string
              - "null"
      responses:
        "200":
          description: The connection was cleared.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WhatsAppDisconnectResponse"
        "400":
          description: Missing phone number parameter.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WhatsAppConnectError"
        "401":
          description: Authentication required.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WhatsAppConnectError"
        "404":
          description: The phone number or targeted account was not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WhatsAppConnectError"
        "500":
          description: An unexpected error occurred.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WhatsAppConnectError"
  /channels/whatsapp-web/connections:
    post:
      security:
        - ApiKeyAuth: []
      tags:
        - WhatsApp Web Connections
      summary: Start a WhatsApp Web pairing session
      description: Begins pairing a WhatsApp number by QR code. Creates or resets the connection and returns a session whose QR code you then fetch and have the end user scan. Poll the status endpoint until the connection reports connected. Each connected number incurs a recurring monthly maintenance charge until it is disconnected.
      parameters:
        - $ref: "#/components/parameters/ApiKeyParam"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/WhatsAppWebConnectRequest"
      responses:
        "200":
          description: Pairing session started.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WhatsAppWebConnectResponse"
        "400":
          description: Missing or invalid request fields.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WhatsAppWebError"
        "401":
          description: Authentication required or invalid.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WhatsAppWebError"
        "403":
          description: The account's plan does not include channel connections.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WhatsAppWebError"
        "404":
          description: The referenced account could not be found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WhatsAppWebError"
        "409":
          description: This number is already connected.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WhatsAppWebError"
        "503":
          description: The connection service is temporarily at capacity.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WhatsAppWebError"
  /channels/whatsapp-web/connections/{phoneNumber}/qr:
    get:
      security:
        - ApiKeyAuth: []
      tags:
        - WhatsApp Web Connections
      summary: Get the pairing QR code
      description: Returns the current QR code for a pairing session so it can be rendered for the end user to scan. The code is short-lived; poll this endpoint every 10 to 15 seconds until the status reports connected, and restart the connection if the code expires.
      parameters:
        - $ref: "#/components/parameters/ApiKeyParam"
        - name: phoneNumber
          in: path
          required: true
          description: The connected number in E.164 format.
          schema:
            type: string
          example: "+15551230000"
        - name: sub_account_id
          in: query
          required: false
          description: Optional identifier of a managed account to act on behalf of.
          schema:
            type: string
      responses:
        "200":
          description: QR code returned.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WhatsAppWebQrResponse"
        "401":
          description: Authentication required or invalid.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WhatsAppWebError"
        "404":
          description: The connection was not found, or the QR code is not ready yet.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WhatsAppWebError"
        "410":
          description: The QR code expired. Restart the connection to generate a new one.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WhatsAppWebError"
        "503":
          description: The QR service is temporarily unavailable.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WhatsAppWebError"
  /channels/whatsapp-web/connections/{phoneNumber}/status:
    get:
      security:
        - ApiKeyAuth: []
      tags:
        - WhatsApp Web Connections
      summary: Get the connection status
      description: Returns the live connection status for a pairing session. The terminal success state is connected (the service may also report open). Treat disconnected and not_initialized as terminal failure states. Poll this endpoint after the end user scans the QR code to confirm the connection.
      parameters:
        - $ref: "#/components/parameters/ApiKeyParam"
        - name: phoneNumber
          in: path
          required: true
          description: The connected number in E.164 format.
          schema:
            type: string
          example: "+15551230000"
        - name: sub_account_id
          in: query
          required: false
          description: Optional identifier of a managed account to act on behalf of.
          schema:
            type: string
      responses:
        "200":
          description: Connection status returned.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WhatsAppWebStatusResponse"
        "401":
          description: Authentication required or invalid.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WhatsAppWebError"
        "404":
          description: The connection was not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WhatsAppWebError"
        "502":
          description: The connection service could not be reached.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WhatsAppWebError"
  /channels/whatsapp-web/connections/{phoneNumber}:
    delete:
      security:
        - ApiKeyAuth: []
      tags:
        - WhatsApp Web Connections
      summary: Disconnect a WhatsApp Web connection
      description: Disconnects the pairing session, stops the recurring monthly maintenance charge, and removes the connection record. The operation is idempotent and always succeeds locally even if the connection service is unreachable.
      parameters:
        - $ref: "#/components/parameters/ApiKeyParam"
        - name: phoneNumber
          in: path
          required: true
          description: The connected number in E.164 format.
          schema:
            type: string
          example: "+15551230000"
        - name: sub_account_id
          in: query
          required: false
          description: Optional identifier of a managed account to act on behalf of.
          schema:
            type: string
      responses:
        "200":
          description: Connection removed.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WhatsAppWebDisconnectResponse"
        "401":
          description: Authentication required or invalid.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WhatsAppWebError"
        "404":
          description: The connection was not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WhatsAppWebError"
  /entry-points/routing-status:
    get:
      summary: Get Agent Routing Status
      security:
        - ApiKeyAuth: []
      description: |
        Tells you whether the agent routing rules (channel defaults and entry
        points) actually decide who answers on this account yet.

        The rules can be configured on every account, but they are switched on
        account by account. While cutover_enabled is false anything you set
        through the channel-defaults and entry-point endpoints is saved and
        returned correctly, and simply is not what picks the answering agent for
        incoming messages yet. Check this before telling someone their routing
        change is live.

        Readable with view access, so a teammate sees the same answer the owner
        does.
      tags:
        - Entry Points
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: Optional. Read the status of a managed account you administer.
          schema:
            type:
              - string
              - "null"
      responses:
        '200':
          description: Status retrieved successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  cutover_enabled:
                    type: boolean
                    description: True when these routing rules decide who answers incoming messages on this account.
                    examples: [true]
  /entry-points/channel-defaults:
    get:
      summary: List Channel Default Agents
      security:
        - ApiKeyAuth: []
      description: |
        Returns every channel default on the account - the rules that say which
        agent answers a brand new contact on a given channel - newest first.

        Both current and retired rules come back. A rule with enabled true is
        the one in force for its channel; retired rules stay as enabled false so
        you can see who answered before. Filter on enabled yourself if you only
        want the current picture.

        This is the account-wide read. Listing one agent's entry points instead
        (GET /agents/{agentId}/entry-points) cannot show you a channel that is
        set to nobody, because such a rule belongs to no agent.
      tags:
        - Entry Points
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: Optional. Read the channel defaults of a managed account you administer.
          schema:
            type:
              - string
              - "null"
      responses:
        '200':
          description: Channel defaults retrieved successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  entry_points:
                    type: array
                    items:
                      $ref: '#/components/schemas/EntryPoint'
              examples:
                success:
                  value: {
                    "success": true,
                    "entry_points": [
                      {
                        "id": "ep3KmQ8vTzXr5nWd",
                        "type": "channel_default",
                        "channels": ["whatsapp"],
                        "agent_id": "ag7HkQ2ZpLxR3mNb",
                        "enabled": true,
                        "match_config": {},
                        "created_at": 1700000000000,
                        "last_modified_at": 1700000000000
                      }
                    ]
                  }
    put:
      summary: Set Channel Default Agent
      security:
        - ApiKeyAuth: []
      description: |
        Makes one agent the answerer for new, unknown contacts arriving on a
        channel. This is the call to use for "connect WhatsApp to this agent".

        Any other agent currently set as the default for that channel is retired
        in the same call, so a channel always has exactly one answerer. Setting
        the agent that is already the default changes nothing. The response
        lists the rule now in force and any rules that were retired.

        Only affects contacts the account has not spoken to before. A contact
        already in a conversation with an agent, or manually assigned to one,
        keeps that agent.

        The agent must belong to the same account, otherwise the call is
        rejected.

        With phone_number, the default is scoped to that one connected number
        (one agent per WhatsApp number): a number-scoped default outranks the
        channel-wide one for messages arriving on that number, and setting
        either kind leaves the other kind alone.
      tags:
        - Entry Points
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: Optional. Act on a managed account you administer.
          schema:
            type:
              - string
              - "null"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - channel
                - agent_id
              properties:
                channel:
                  type: string
                  description: |
                    The channel this default applies to, for example whatsapp,
                    whatsapp_web, instagram, messenger, telegram, sms, email,
                    chat_widget or custom_channel.
                  examples: ["whatsapp"]
                agent_id:
                  type: string
                  description: ID of the agent that should answer new contacts on this channel
                  examples: ["ag7HkQ2ZpLxR3mNb"]
                phone_number:
                  type: string
                  description: |
                    Optional. Scope the default to ONE of your connected numbers on
                    this channel instead of the whole channel, written exactly as it
                    shows under connected numbers (E.164 with the leading +). A
                    message that arrives on that number goes to this agent; every
                    other number keeps following the channel-wide default, which
                    this call leaves untouched. Must be a number connected on this
                    account, otherwise 400. The rule comes back with
                    match_config.phone_numbers set.
                  examples: ["+31685101091"]
            examples:
              whatsapp:
                summary: Let one agent answer new WhatsApp conversations
                value: { "channel": "whatsapp", "agent_id": "ag7HkQ2ZpLxR3mNb" }
              one_number:
                summary: Give one connected number its own agent
                value: { "channel": "whatsapp_web", "agent_id": "agRotterdamBranch", "phone_number": "+31685101091" }
      responses:
        '200':
          description: Channel default set
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  entry_point_id:
                    type: string
                    description: The rule now in force for this channel
                    examples: ["ep3KmQ8vTzXr5nWd"]
                  disabled_entry_point_ids:
                    type: array
                    description: Rules that were retired to make room for this one. Empty when there was nothing to replace.
                    items:
                      type: string
        '400':
          description: channel or agent_id missing, the agent does not belong to this account, or phone_number is not a number connected on this account
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["Missing required field: agent_id"]
    delete:
      summary: Clear Channel Default Agent
      security:
        - ApiKeyAuth: []
      description: |
        Removes the channel default for one channel, so the channel has no agent
        configured for new contacts any more.

        Clearing means unset, not silence. On an account with exactly one active
        agent, an unconfigured channel still falls back to that one agent - so
        clearing alone will not make such an account go quiet. To stop replies
        entirely, pause the agent (PATCH /agents/{agentId}/active).

        Safe to repeat: clearing a channel that has no default is a success with
        an empty list, not an error. The channel is named as a query parameter,
        not in a body. Add phone_number to clear one connected number's own
        default instead of the channel-wide one.
      tags:
        - Entry Points
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: channel
          in: query
          required: true
          description: The channel to clear, for example whatsapp or instagram
          schema:
            type: string
          examples:
            whatsapp:
              value: "whatsapp"
        - name: phone_number
          in: query
          required: false
          description: |
            Optional. Clear only this connected number's own default (E.164 with
            the leading +), so the number goes back to whoever answers the
            channel. Without it the channel-wide default is cleared and any
            per-number defaults are left standing.
          schema:
            type: string
          examples:
            one_number:
              value: "+31685101091"
        - name: sub_account_id
          in: query
          required: false
          description: Optional. Act on a managed account you administer.
          schema:
            type:
              - string
              - "null"
      responses:
        '200':
          description: Channel default cleared
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  disabled_entry_point_ids:
                    type: array
                    description: Rules that were retired by this call. Empty when the channel had no default.
                    items:
                      type: string
        '400':
          description: The channel query parameter was missing or blank
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["Missing required query param: channel"]
  /entry-points/{entryPointId}:
    put:
      summary: Update Entry Point
      security:
        - ApiKeyAuth: []
      description: |
        Changes one entry point. Only the fields you send are changed, and
        nested settings can be addressed leaf by leaf with a dotted key (for
        example "match_config.keywords").

        Whenever the change touches type, channels or match_config the whole
        rule is re-checked, so a partial edit can never leave an unusable rule
        behind - for example switching type to keyword without ever supplying
        keywords is rejected. Sending agent_id moves the rule to another of your
        agents; a blank one is rejected.

        Ownership and identity fields are ignored.
      tags:
        - Entry Points
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: entryPointId
          in: path
          required: true
          description: ID of the entry point to update
          schema:
            type: string
        - name: sub_account_id
          in: query
          required: false
          description: Optional. Act on a managed account you administer.
          schema:
            type:
              - string
              - "null"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/EntryPointWriteRequest'
            examples:
              disable:
                summary: Retire a rule without deleting it
                value: { "enabled": false }
              keywords:
                summary: Change which keywords the rule reacts to
                value: { "match_config": { "keywords": ["pricing", "quote"] } }
              reassign:
                summary: Hand the rule to a different agent
                value: { "agent_id": "ag7HkQ2ZpLxR3mNb" }
      responses:
        '200':
          description: Entry point updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  entry_point_id:
                    type: string
        '400':
          description: Nothing to update, or the resulting rule would be invalid
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples:
                      - "No fields to update"
                      - "A keyword entry point requires at least one keyword in match_config.keywords."
                      - "agent_id cannot be blank."
        '404':
          $ref: '#/components/responses/EntryPointNotFound'
    delete:
      summary: Delete Entry Point
      security:
        - ApiKeyAuth: []
      description: |
        Deletes an entry point permanently. Nothing else refers to an entry
        point, so there is nothing to detach first.

        To stop a rule from firing but keep it around, set enabled to false with
        PUT /entry-points/{entryPointId} instead. Channel defaults in particular
        are normally retired rather than deleted, which is what
        DELETE /entry-points/channel-defaults does.
      tags:
        - Entry Points
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: entryPointId
          in: path
          required: true
          description: ID of the entry point to delete
          schema:
            type: string
        - name: sub_account_id
          in: query
          required: false
          description: Optional. Act on a managed account you administer.
          schema:
            type:
              - string
              - "null"
      responses:
        '200':
          description: Entry point deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  entry_point_id:
                    type: string
        '404':
          $ref: '#/components/responses/EntryPointNotFound'
  /channel-routing/clear:
    post:
      summary: Release Channels From Their Agent
      security:
        - ApiKeyAuth: []
      description: |
        Frees one or more channels from whichever agent currently answers them,
        without pausing or ending anything. Each channel's default rule is
        retired, leaving the channel unconfigured.

        Use this to resolve a conflict before pointing the channels at something
        else - typically followed by PUT /entry-points/channel-defaults for each
        channel you just freed.

        Like clearing a single channel default, this unsets the configuration
        rather than declaring silence: on an account with exactly one active
        agent, a freed channel still falls back to that agent.
      tags:
        - Channels
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: Optional. Act on a managed account you administer.
          schema:
            type:
              - string
              - "null"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - channels
              properties:
                channels:
                  type: array
                  description: Channels to free. Duplicates are ignored.
                  minItems: 1
                  items:
                    type: string
                    enum: [whatsapp, whatsapp_web, telegram, instagram, messenger, chat_widget, custom_channel, sms, email, tiktok]
            examples:
              two:
                summary: Free WhatsApp and Instagram
                value: { "channels": ["whatsapp", "instagram"] }
      responses:
        '200':
          description: Channels released
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  channels:
                    type: array
                    description: The channels you asked to free, with duplicates removed
                    items:
                      type: string
                  released_channels:
                    type: array
                    description: The channels that actually had an answerer to release
                    items:
                      type: string
        '400':
          description: channels was missing, empty, or named a channel that cannot be routed
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["channels must be a non-empty array"]
  /channel-routing/{channel}:
    put:
      summary: Set Channel Campaign Routing
      security:
        - ApiKeyAuth: []
      description: |
        Older, campaign-shaped way to say who answers new contacts on one
        channel: name a campaign, and that campaign's agent becomes the
        channel's answerer. Send campaignId as null to clear the channel.

        Prefer PUT /entry-points/channel-defaults, which names the agent
        directly. Use this one only if you still organise the account around
        campaigns.

        The campaign must be one of yours and must be able to handle incoming
        conversations - an outgoing-only campaign is rejected, because it has no
        inbound behaviour to offer.
      tags:
        - Channels
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: channel
          in: path
          required: true
          description: The channel to route
          schema:
            type: string
            enum: [whatsapp, whatsapp_web, telegram, instagram_private, instagram, messenger, chat_widget, custom_channel, sms, email, imessage, tiktok, linkedin, skool]
        - name: sub_account_id
          in: query
          required: false
          description: Optional. Act on a managed account you administer.
          schema:
            type:
              - string
              - "null"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                campaignId:
                  type: ['string', 'null']
                  description: ID of the campaign whose agent should answer this channel, or null to clear the channel. Omitting it is treated as null.
                  examples: ["cp5NbV8xQrT2wYzA"]
            examples:
              route:
                summary: Point WhatsApp at a campaign
                value: { "campaignId": "cp5NbV8xQrT2wYzA" }
              clear:
                summary: Clear the channel
                value: { "campaignId": null }
      responses:
        '200':
          description: Routing updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  uid:
                    type: string
                    description: The account the routing was written for
                  channel:
                    type: string
                  campaignId:
                    type: ['string', 'null']
                    description: The campaign now answering this channel, or null when it was cleared
        '400':
          description: |
            The channel is not one that can be routed, campaignId was not a
            string or null, or the campaign cannot answer incoming
            conversations (only an "Incoming from Unknown Contacts" or
            "Combined" campaign can be routed to a channel).
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error_code:
                    type: integer
                    examples: [400]
                  error:
                    type: string
                    examples:
                      - "campaignId must be a string or null."
                      - 'Only "Incoming from Unknown Contacts" or "Combined" campaigns can be routed to incoming channels (got: Outgoing).'
        '403':
          description: The campaign belongs to another account
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error_code:
                    type: integer
                    examples: [403]
                  error:
                    type: string
                    examples: ["You don't have permission to use this campaign."]
        '404':
          description: No campaign with that ID
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error_code:
                    type: integer
                    examples: [404]
                  error:
                    type: string
  /agents:
    get:
      summary: List Agents
      security:
        - ApiKeyAuth: []
      description: |
        Returns every AI Agent on the account, newest first.

        By default each agent comes back with its FULL configuration, which is large: a single agent
        can be 580 KB and a 64-agent account over 3 MB. Pass view=summary to get a short row per
        agent instead (name, whether it is live, language, goal, AI tier, whether it books or follows
        up, and how many FAQs and knowledge sources it has). Use the summary to find the agent you
        want, then read it with GET /agents/{agentId}.

        This endpoint is not paged; view=summary is the way to keep the response small. On an
        account with many agents even the summary adds up (about 600 bytes per agent), so you can
        narrow it further with fields, e.g. view=summary&fields=id,name,active.
      tags:
        - Agents
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: view
          in: query
          required: false
          description: Set to "summary" for a short row per agent instead of the full configuration. Omit for the full documents.
          schema:
            type: string
            enum: [summary]
        - name: fields
          in: query
          required: false
          description: >-
            Optional, and only applies together with view=summary. Comma-separated list of the summary
            keys to keep, for example "id,name,active". The id is always included and names that are
            not summary keys are ignored. Omit to get the whole summary row.
          schema:
            type: string
        - name: sub_account_id
          in: query
          required: false
          description: Optional. List the agents of a managed account you administer. Works together with view=summary.
          schema:
            type:
              - string
              - "null"
      responses:
        '200':
          description: Agents retrieved successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  agents:
                    type: array
                    description: Full agent documents, or short summaries when view=summary was supplied
                    items:
                      oneOf:
                        - $ref: '#/components/schemas/Agent'
                        - $ref: '#/components/schemas/AgentSummary'
        '400':
          description: view was supplied with an unsupported value
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["view must be one of: summary"]
    post:
      summary: Create Agent
      security:
        - ApiKeyAuth: []
      description: |
        Creates a new AI Agent. Ownership and timestamps are set automatically.

        Send the agent's name plus any configuration fields you already know
        (instructions, rules, goal, personality, availability, ai_speed,
        anthropic_model, and so on) - they are stored on the new agent. A new
        agent is active by default.

        Website-first creation: include scrape_urls (a list of page URLs) and the
        platform reads those pages and writes the agent's instructions for you.
        The response tells you whether that generation was started, so you know
        whether to poll the agent for its progress.

        A new agent answers nobody until you point traffic at it. To put it on a
        channel - "let this agent answer WhatsApp" - call
        PUT /entry-points/channel-defaults with the channel and the new agent's
        ID. For narrower rules (keywords, comment replies, new followers) use
        POST /agents/{agentId}/entry-points.
      tags:
        - Agents
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: Optional. Create the agent on a managed account you administer.
          schema:
            type:
              - string
              - "null"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
              properties:
                name:
                  type: string
                  description: Agent name
                  examples: ["Listing assistant"]
                active:
                  type: boolean
                  description: Whether the agent may reply straight away. Defaults to true.
                language:
                  type: string
                  description: Language the agent replies in
                instructions:
                  type: string
                  description: Primary instructions that steer how the agent talks to contacts
                rules:
                  type: string
                  description: Hard rules the agent must always follow
                goal:
                  type: string
                  description: The outcome the agent should work towards
                personality:
                  type: string
                  description: Tone-of-voice and personality description
                availability:
                  $ref: '#/components/schemas/AvailabilitySchedule'
                ai_speed:
                  type: string
                  enum: [fast, fast_thinker, balanced, thorough]
                anthropic_model:
                  type: string
                  enum: [standard, economy, max, mini]
                  description: AI quality tier used for this agent's replies
                scrape_urls:
                  type: array
                  description: Pages to read and build the agent's instructions from
                  items:
                    type: string
                  examples: [["https://example.com/about"]]
            examples:
              blank:
                summary: Create an agent with initial configuration
                value:
                  name: Listing assistant
                  language: en
                  instructions: Answer questions about our listings and book viewings.
                  goal: Book a viewing
              fromWebsite:
                summary: Build the agent from a website
                value:
                  name: Listing assistant
                  scrape_urls: ["https://example.com", "https://example.com/faq"]
      responses:
        '201':
          description: Agent created
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  agent_id:
                    type: string
                    description: ID of the new agent
                    examples: ["ag7HkQ2ZpLxR3mNb"]
                  substrate_campaign_id:
                    type: ['string', 'null']
                    description: Internal record created alongside the agent on some accounts. Null when none was created; you never need to act on it.
                  agent_generation_queued:
                    type: boolean
                    description: True when the platform started building the agent's instructions from the pages you supplied.
              examples:
                success:
                  value: {
                    "success": true,
                    "agent_id": "ag7HkQ2ZpLxR3mNb",
                    "substrate_campaign_id": null,
                    "agent_generation_queued": true
                  }
        '400':
          description: The body was not a JSON object, a supplied field was rejected, or the agent exceeds the plan's configuration size limit
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["Request body must be a JSON object"]
        '403':
          description: The account you are creating on is not allowed to use one of the supplied settings, for example an AI tier its agency has not granted
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
  /agents/{agentId}:
    get:
      summary: Get Agent
      security:
        - ApiKeyAuth: []
      description: |
        Returns one agent's full configuration.

        A full agent can be several hundred kilobytes, mostly its FAQ list, its knowledge sources and
        any page content read from your website. Pass fields with a comma-separated list of the
        fields you actually need (for example fields=name,active,goal) to get just those back. The
        agent's id is always included.
      tags:
        - Agents
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: agentId
          in: path
          required: true
          description: ID of the agent
          schema:
            type: string
        - name: fields
          in: query
          required: false
          description: |
            Comma-separated list of the fields to return, for example "name,active,goal". The id is
            always included. Names that do not exist on this agent are ignored rather than rejected.
            Omit to get the whole agent.
          schema:
            type: string
          examples:
            picker:
              value: "name,active,language,goal"
        - name: sub_account_id
          in: query
          required: false
          description: Optional. Read an agent on a managed account you administer.
          schema:
            type:
              - string
              - "null"
      responses:
        '200':
          description: Agent retrieved successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  agent:
                    $ref: '#/components/schemas/Agent'
        '404':
          $ref: '#/components/responses/AgentNotFound'
    put:
      summary: Update Agent
      security:
        - ApiKeyAuth: []
      description: |
        Updates an agent. Only the fields you send are changed - everything else
        is left untouched. Nested settings can be addressed leaf by leaf with a
        dotted key (for example "availability.monday"), which changes just that
        leaf.

        Notes:
        - To change which bookable event type the agent books into, send
          event_id (the event's ID, or null to clear it). The event field itself
          cannot be written directly.
        - enable_bookings must be a real boolean, and booking_provider must be
          one of default, zenchef, formitable.
        - Ownership and identity fields are ignored, as is internal run state
          (generation and optimisation progress).
        - Which channels and conversations reach this agent are NOT set here.
          Use PUT /entry-points/channel-defaults to make it the answerer for a
          channel, POST /agents/{agentId}/entry-points for keyword and comment
          rules, and PATCH /agents/{agentId}/active to pause or resume it.
        - To change only the conversational settings, PUT
          /agents/{agentId}/bot-config is the narrower way in; active hours have
          their own endpoint, PUT /agents/{agentId}/active-hours.
      tags:
        - Agents
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: agentId
          in: path
          required: true
          description: ID of the agent to update
          schema:
            type: string
        - name: sub_account_id
          in: query
          required: false
          description: Optional. Update an agent on a managed account you administer.
          schema:
            type:
              - string
              - "null"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
              properties:
                name:
                  type: string
                active:
                  type: boolean
                language:
                  type: string
                instructions:
                  type: string
                rules:
                  type: string
                goal:
                  type: string
                personality:
                  type: string
                availability:
                  $ref: '#/components/schemas/AvailabilitySchedule'
                ai_speed:
                  type: string
                  enum: [fast, fast_thinker, balanced, thorough]
                anthropic_model:
                  type: string
                  enum: [standard, economy, max, mini]
                enable_bookings:
                  type: boolean
                  description: Whether the agent may book appointments
                booking_provider:
                  type: string
                  enum: [default, zenchef, formitable]
                event_id:
                  type: ['string', 'null']
                  description: ID of the bookable event type the agent books into. Pass null to clear it.
                enable_follow_ups:
                  type: boolean
                follow_up_config:
                  type: object
                  description: Follow-up behaviour configuration
                  additionalProperties: true
            examples:
              editInstructions:
                summary: Change the instructions and the AI tier
                value:
                  instructions: Answer questions about our listings and always offer a viewing.
                  anthropic_model: standard
              oneDayOfActiveHours:
                summary: Change only Monday's active hours
                value:
                  availability.monday: { "start_time": "09:00", "end_time": "17:00" }
      responses:
        '200':
          description: Agent updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  agent_id:
                    type: string
        '400':
          description: Empty body, a rejected field value, or a change that exceeds the plan's configuration size limit
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["No fields to update"]
        '403':
          description: A supplied setting is locked for this account by its account provider
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
        '404':
          $ref: '#/components/responses/AgentNotFound'
    delete:
      summary: Delete Agent
      security:
        - ApiKeyAuth: []
      description: |
        Deletes an agent.

        The delete is refused while the agent is still attached to something that
        would stop working without it - a broadcast, an entry point, or (on older
        accounts) a campaign. The response lists what is holding it, so you can
        detach those first and retry.
      tags:
        - Agents
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: agentId
          in: path
          required: true
          description: ID of the agent to delete
          schema:
            type: string
        - name: sub_account_id
          in: query
          required: false
          description: Optional. Delete an agent on a managed account you administer.
          schema:
            type:
              - string
              - "null"
      responses:
        '200':
          description: Agent deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  agent_id:
                    type: string
        '404':
          $ref: '#/components/responses/AgentNotFound'
        '409':
          description: The agent is still attached to something and was not deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["Agent is still attached to one or more broadcast(s). Detach it first."]
                  blocking_campaign_ids:
                    type: array
                    items:
                      type: string
                  blocking_broadcast_ids:
                    type: array
                    items:
                      type: string
                  blocking_entry_point_ids:
                    type: array
                    items:
                      type: string
  /agents/{agentId}/active:
    patch:
      summary: Pause or Resume Agent
      security:
        - ApiKeyAuth: []
      description: |
        Turns an agent on or off. A paused agent keeps all of its configuration
        but stops replying immediately; resuming it takes effect straight away.
      tags:
        - Agents
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: agentId
          in: path
          required: true
          description: ID of the agent
          schema:
            type: string
        - name: sub_account_id
          in: query
          required: false
          description: Optional. Pause or resume an agent on a managed account you administer.
          schema:
            type:
              - string
              - "null"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - active
              properties:
                active:
                  type: boolean
                  description: true to let the agent reply, false to pause it
            examples:
              pause:
                summary: Pause the agent
                value: { "active": false }
      responses:
        '200':
          description: New state applied
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  agent_id:
                    type: string
                  active:
                    type: boolean
        '400':
          description: Missing or non-boolean active field
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["active (boolean) is required"]
        '404':
          $ref: '#/components/responses/AgentNotFound'
  /agents/{agentId}/custom-functions:
    post:
      summary: Attach Custom Function to Agent
      security:
        - ApiKeyAuth: []
      description: |
        Lets this agent call one of your custom functions during conversations.
        Attaching a function that is already attached changes nothing.

        Only functions belonging to the same account can be attached - the
        function is always resolved inside that account, whatever ID is sent.
      tags:
        - Agents
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: agentId
          in: path
          required: true
          description: ID of the agent
          schema:
            type: string
        - name: sub_account_id
          in: query
          required: false
          description: Optional. Act on a managed account you administer.
          schema:
            type:
              - string
              - "null"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - custom_function_id
              properties:
                custom_function_id:
                  type: string
                  description: ID of the custom function to attach
            examples:
              attach:
                value: { "custom_function_id": "cf7Hk2ZpLxR3mNbV" }
      responses:
        '200':
          description: Custom function attached
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  agent_id:
                    type: string
                  custom_function_id:
                    type: string
        '400':
          description: Missing or malformed custom_function_id
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["Missing required field: custom_function_id"]
        '404':
          $ref: '#/components/responses/AgentNotFound'
  /agents/{agentId}/custom-functions/{customFunctionId}:
    delete:
      summary: Detach Custom Function from Agent
      security:
        - ApiKeyAuth: []
      description: |
        Stops this agent from calling the custom function. The function itself is
        not deleted and stays available to your other agents. Detaching a
        function that is not attached changes nothing.
      tags:
        - Agents
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: agentId
          in: path
          required: true
          description: ID of the agent
          schema:
            type: string
        - name: customFunctionId
          in: path
          required: true
          description: ID of the custom function to detach
          schema:
            type: string
        - name: sub_account_id
          in: query
          required: false
          description: Optional. Act on a managed account you administer.
          schema:
            type:
              - string
              - "null"
      responses:
        '200':
          description: Custom function detached
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  agent_id:
                    type: string
                  custom_function_id:
                    type: string
        '400':
          description: Malformed customFunctionId
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["Invalid customFunctionId. Use letters, numbers, underscores, and hyphens only."]
        '404':
          $ref: '#/components/responses/AgentNotFound'
  /agents/{agentId}/media-library:
    get:
      summary: List Agent Media
      security:
        - ApiKeyAuth: []
      description: |
        Returns every media item this agent can send. Items stored on the agent
        come first, then any older items still stored on the campaign the agent
        was built from; each row says which of the two it is through media_home.
        Within each group the newest item comes first.

        The media_url on each item is the download link that was created when the
        file was uploaded, and it expires after 7 days - treat an old one as
        stale rather than broken.
      tags:
        - Agents
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: agentId
          in: path
          required: true
          description: ID of the agent
          schema:
            type: string
        - name: sub_account_id
          in: query
          required: false
          description: Optional. Read the media of an agent on a managed account you administer.
          schema:
            type:
              - string
              - "null"
      responses:
        '200':
          description: Media retrieved successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  agent_id:
                    type: string
                  media_items:
                    type: array
                    items:
                      $ref: '#/components/schemas/MediaLibraryItem'
              examples:
                success:
                  value: {
                    "success": true,
                    "agent_id": "ag7HkQ2ZpLxR3mNb",
                    "media_items": [
                      {
                        "id": "mi4RtY7uIoP1aSdF",
                        "item_id": "mi4RtY7uIoP1aSdF",
                        "agent_id": "ag7HkQ2ZpLxR3mNb",
                        "campaign_id": "",
                        "media_home": "agent",
                        "title": "Spring menu",
                        "description": "Send when someone asks what is on the menu.",
                        "send_message": "",
                        "ai_description": "A one-page menu listing seasonal dishes and prices.",
                        "type": "document",
                        "media_content_type": "application/pdf",
                        "storage_path": "ai_media/agents/ag7HkQ2ZpLxR3mNb/document/spring-menu.pdf",
                        "media_url": "https://storage.googleapis.com/...",
                        "is_voice_note": false,
                        "is_animated_gif": false,
                        "max_sends_per_conversation": 1,
                        "created_at": 1700000000000
                      }
                    ]
                  }
        '404':
          $ref: '#/components/responses/AgentNotFound'
    post:
      summary: Upload Agent Media
      security:
        - ApiKeyAuth: []
      description: |
        Adds a file to the agent's media library so it can send it during a
        conversation. The file is uploaded inline as base64 (maximum 10 MB) and
        the call returns once it is stored, so allow a little longer than for a
        normal request.

        Two things happen automatically: an animated GIF is converted to video so
        it plays on every channel, and the platform writes a short summary of
        what is actually in the file so the agent knows when it fits.

        An account can hold at most 50 media items per agent.
      tags:
        - Agents
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: agentId
          in: path
          required: true
          description: ID of the agent
          schema:
            type: string
        - name: sub_account_id
          in: query
          required: false
          description: Optional. Upload onto an agent of a managed account you administer.
          schema:
            type:
              - string
              - "null"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/MediaLibraryUploadRequest'
            examples:
              upload:
                summary: Upload a PDF the agent may send
                value:
                  base64Data: "JVBERi0xLjQKJcfs..."
                  mimeType: application/pdf
                  fileName: spring-menu.pdf
                  title: Spring menu
                  description: Send when someone asks what is on the menu.
                  maxSendsPerConversation: 1
      responses:
        '200':
          description: Media item created
          content:
            application/json:
              schema:
                allOf:
                  - type: object
                    properties:
                      agent_id:
                        type: string
                  - $ref: '#/components/schemas/MediaLibraryUploadResponse'
        '400':
          description: Missing required fields, unsupported file type, empty or oversized file, or the 50-item limit has been reached
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["File size exceeds maximum allowed size of 10MB"]
        '403':
          description: The AI media library is switched off for this account, or the agent belongs to another account
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["AI Media Library is disabled for this account."]
  /agents/{agentId}/media-library/{itemId}:
    patch:
      summary: Update Agent Media Item
      security:
        - ApiKeyAuth: []
      description: |
        Edits a media item's details. The file itself cannot be replaced - upload
        a new item and delete the old one instead.

        Items stored on the agent and older items still stored on the campaign the
        agent was built from can both be edited here; the response says which one
        was changed.
      tags:
        - Agents
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: agentId
          in: path
          required: true
          description: ID of the agent
          schema:
            type: string
        - name: itemId
          in: path
          required: true
          description: ID of the media item
          schema:
            type: string
        - name: sub_account_id
          in: query
          required: false
          description: Optional. Act on a managed account you administer.
          schema:
            type:
              - string
              - "null"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/MediaLibraryItemUpdate'
            examples:
              retitle:
                value: { "title": "Summer menu", "max_sends_per_conversation": 2 }
      responses:
        '200':
          description: Media item updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  agent_id:
                    type: string
                  campaign_id:
                    type: string
                    description: The campaign holding the item, or an empty string when it lives on the agent itself
                  media_home:
                    type: string
                    enum: [agent, campaign]
                  item_id:
                    type: string
        '400':
          description: Malformed itemId, no fields to update, or a rejected field value
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["Invalid field: max_sends_per_conversation. Must be a non-negative integer or null."]
        '404':
          description: Agent or media item not found
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["Media library item not found"]
        '409':
          description: There is nowhere to hold media for this agent yet
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["Agent has no attached campaign to hold media. Attach a campaign first."]
    delete:
      summary: Delete Agent Media Item
      security:
        - ApiKeyAuth: []
      description: |
        Removes a media item and its stored file. Deleting an item that is
        already gone succeeds and reports deleted false, so the call is safe to
        retry.
      tags:
        - Agents
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: agentId
          in: path
          required: true
          description: ID of the agent
          schema:
            type: string
        - name: itemId
          in: path
          required: true
          description: ID of the media item
          schema:
            type: string
        - name: sub_account_id
          in: query
          required: false
          description: Optional. Act on a managed account you administer.
          schema:
            type:
              - string
              - "null"
      responses:
        '200':
          description: Media item removed
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  agent_id:
                    type: string
                  item_id:
                    type: string
                  deleted:
                    type: boolean
                    description: False when the item was already gone
        '400':
          description: Malformed itemId
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["Invalid itemId. Use letters, numbers, underscores, and hyphens only."]
        '403':
          description: The agent belongs to another account
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["Agent not found in your account."]
  /agents/{agentId}/duplicate:
    post:
      summary: Duplicate Agent
      security:
        - ApiKeyAuth: []
      description: |
        Creates a copy of the agent with its configuration preserved, and
        returns the new agent's ID. Nothing is sent by the copy until you point
        a channel or an entry point at it.

        A copy counts against the plan's agent allowance exactly like creating
        one from scratch, so this can be refused when the account is at its
        limit.
      tags:
        - Agents
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: agentId
          in: path
          required: true
          description: ID of the agent to copy
          schema:
            type: string
        - name: sub_account_id
          in: query
          required: false
          description: Optional. Act on a managed account you administer.
          schema:
            type:
              - string
              - "null"
      responses:
        '201':
          description: Agent duplicated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  agent_id:
                    type: string
                    description: ID of the new copy
                    examples: ["ag9WsX3cRfV6tGyH"]
                  source_agent_id:
                    type: string
                    description: The agent that was copied
                    examples: ["ag7HkQ2ZpLxR3mNb"]
        '403':
          description: The account has reached the number of AI agents its plan allows
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["AI Agent limit reached — your plan allows up to 3 AI agents."]
        '404':
          $ref: '#/components/responses/AgentNotFound'
  /agents/{agentId}/bot-config:
    put:
      summary: Update Agent Bot Settings
      security:
        - ApiKeyAuth: []
      description: |
        Changes the agent's conversational settings - instructions, rules, goal,
        personality, AI tier, speed, and the rest. Only the fields you send are
        changed; everything else is left alone.

        An agent has no separate bot section: its settings sit directly on the
        agent, so the field names here are the same ones you would send to
        PUT /agents/{agentId}. This endpoint exists as the safe, narrow way to
        change a handful of them.

        Field names must be plain names (letters, numbers, underscores and
        hyphens) - dotted paths are not accepted here, unlike on
        PUT /agents/{agentId}. Two fields are checked against a fixed list:
        anthropic_model must be standard, economy, max or mini, and ai_speed
        must be fast, fast_thinker, balanced or thorough.

        Long text fields count against the plan's configuration size limit, so a
        very large instruction set can be refused.
      tags:
        - Agents
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: agentId
          in: path
          required: true
          description: ID of the agent
          schema:
            type: string
        - name: sub_account_id
          in: query
          required: false
          description: Optional. Act on a managed account you administer.
          schema:
            type:
              - string
              - "null"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
              minProperties: 1
              description: One or more agent settings to change. At least one is required.
              properties:
                instructions:
                  type: string
                  description: Primary instructions that steer how the agent talks to contacts
                rules:
                  type: string
                  description: Hard rules the agent must always follow
                goal:
                  type: string
                  description: The outcome the agent should work towards in each conversation
                personality:
                  type: string
                  description: Tone-of-voice and personality description
                language:
                  type: string
                  description: Language the agent replies in
                ai_speed:
                  type: string
                  enum: [fast, fast_thinker, balanced, thorough]
                  description: How much reasoning the agent applies before replying
                anthropic_model:
                  type: string
                  enum: [standard, economy, max, mini]
                  description: AI quality tier used for this agent's replies
                max_messages:
                  type: integer
                  description: Maximum number of agent messages per conversation
                alert_human_when:
                  type: string
                  description: Conditions under which the agent should alert a human teammate
                ai_transparency:
                  type: boolean
                  description: Whether the agent discloses that it is an AI
            examples:
              tone:
                summary: Change the goal and slow the agent down for better answers
                value:
                  goal: Book a viewing within three messages
                  ai_speed: thorough
      responses:
        '200':
          description: Settings updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  agent_id:
                    type: string
        '400':
          description: Nothing to update, an unusable field name, or a value outside the allowed list
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples:
                      - "No fields to update"
                      - 'Invalid bot field key "bot.goal". Use letters, numbers, underscores, and hyphens only (no dots or brackets).'
        '403':
          description: This account is not allowed to set one of the supplied values, or the change exceeds the plan's configuration size limit
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
        '404':
          $ref: '#/components/responses/AgentNotFound'
  /agents/{agentId}/active-hours:
    put:
      summary: Set Agent Active Hours
      security:
        - ApiKeyAuth: []
      description: |
        Sets the hours during which the agent replies automatically. Outside the
        windows you configure it stays quiet.

        Send an availability object keyed by weekday (monday through sunday);
        each day takes a single time window or a list of windows in 24-hour
        HH:MM form. Days you leave out keep whatever they had. Any key that is
        not a weekday is rejected, so a typo cannot silently do nothing.
      tags:
        - Agents
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: agentId
          in: path
          required: true
          description: ID of the agent
          schema:
            type: string
        - name: sub_account_id
          in: query
          required: false
          description: Optional. Act on a managed account you administer.
          schema:
            type:
              - string
              - "null"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - availability
              properties:
                availability:
                  $ref: '#/components/schemas/AvailabilitySchedule'
            examples:
              weekdays:
                summary: Office hours on weekdays only
                value:
                  availability:
                    monday: { start_time: "09:00", end_time: "17:00" }
                    tuesday: { start_time: "09:00", end_time: "17:00" }
      responses:
        '200':
          description: Active hours updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  agent_id:
                    type: string
        '400':
          description: availability was missing or not an object, or it contained something that is not a weekday
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["Invalid availability keys: funday. Allowed keys: monday through sunday."]
        '404':
          $ref: '#/components/responses/AgentNotFound'
  /agents/{agentId}/publish-draft:
    post:
      summary: Publish Agent Draft
      security:
        - ApiKeyAuth: []
      description: |
        Puts the agent's unpublished draft live. Edits made in the editor, and
        the rewrite produced by POST /agents/{agentId}/optimize, are held as a
        draft until this call moves them onto the live configuration; the draft
        is cleared in the same step.

        The response lists which settings were published, so you can show what
        changed. Publishing an agent that has no draft is not a supported call.

        Discard the draft instead with
        POST /agents/{agentId}/discard-draft.
      tags:
        - Agents
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: agentId
          in: path
          required: true
          description: ID of the agent
          schema:
            type: string
        - name: sub_account_id
          in: query
          required: false
          description: Optional. Act on a managed account you administer.
          schema:
            type:
              - string
              - "null"
      responses:
        '200':
          description: Draft published
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  agent_id:
                    type: string
                  published_keys:
                    type: array
                    description: The settings that were moved from the draft onto the live agent
                    items:
                      type: string
                    examples: [["instructions", "goal"]]
        '403':
          description: This account is not allowed to publish one of the values the draft contains
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
        '404':
          $ref: '#/components/responses/AgentNotFound'
        '500':
          description: |
            The publish did not go through. This is also what an agent with no
            unpublished draft currently returns, so check that a draft exists
            before calling.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["Internal server error"]
  /agents/{agentId}/discard-draft:
    post:
      summary: Discard Agent Draft
      security:
        - ApiKeyAuth: []
      description: |
        Throws away the agent's unpublished draft and leaves the live
        configuration exactly as it is. Safe to call when there is no draft -
        nothing happens.

        The opposite of POST /agents/{agentId}/publish-draft.
      tags:
        - Agents
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: agentId
          in: path
          required: true
          description: ID of the agent
          schema:
            type: string
        - name: sub_account_id
          in: query
          required: false
          description: Optional. Act on a managed account you administer.
          schema:
            type:
              - string
              - "null"
      responses:
        '200':
          description: Draft discarded
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  agent_id:
                    type: string
        '404':
          $ref: '#/components/responses/AgentNotFound'
  /agents/{agentId}/kb-sources:
    post:
      summary: Attach Knowledge Sources to Agent
      security:
        - ApiKeyAuth: []
      description: |
        Gives the agent access to one or more knowledge sources - the pages and
        documents the platform has read for you - so it can answer from them.

        Send kb_source_ids with a list to attach a whole set in one call, which
        is what you want after crawling a site; kb_source_id attaches a single
        one. Send one or the other. Attaching something that is already attached
        changes nothing.

        The response echoes everything now attached by this call.
      tags:
        - Agents
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: agentId
          in: path
          required: true
          description: ID of the agent
          schema:
            type: string
        - name: sub_account_id
          in: query
          required: false
          description: Optional. Act on a managed account you administer.
          schema:
            type:
              - string
              - "null"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              description: Supply kb_source_ids for several sources, or kb_source_id for one. At least one of the two is required.
              properties:
                kb_source_id:
                  type: string
                  description: ID of a single knowledge source to attach
                kb_source_ids:
                  type: array
                  description: IDs of the knowledge sources to attach, attached in one step
                  items:
                    type: string
            examples:
              one:
                summary: Attach a single page
                value: { "kb_source_id": "kb2QwErTyUi9OpAs" }
              many:
                summary: Attach a whole crawled site
                value: { "kb_source_ids": ["kb2QwErTyUi9OpAs", "kb6ZxCvBnM4kLjHg"] }
      responses:
        '200':
          description: Knowledge sources attached
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  agent_id:
                    type: string
                  kb_source_id:
                    type: string
                    description: The first attached source, echoed for callers that sent a single ID
                  kb_source_ids:
                    type: array
                    description: Every source attached by this call
                    items:
                      type: string
        '400':
          description: Neither kb_source_id nor kb_source_ids was supplied
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["Missing required field: kb_source_id (or kb_source_ids)"]
        '404':
          $ref: '#/components/responses/AgentNotFound'
  /agents/{agentId}/kb-sources/bulk-remove:
    post:
      summary: Bulk Detach Knowledge Sources from Agent
      security:
        - ApiKeyAuth: []
      description: |
        Takes several knowledge sources away from the agent in one call. The
        sources themselves are not deleted and stay available to your other
        agents.

        This is a POST rather than a DELETE because the list of IDs travels in
        the body. For a single source use
        DELETE /agents/{agentId}/kb-sources/{kbSourceId}.

        Detaching something that is not attached changes nothing.
      tags:
        - Agents
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: agentId
          in: path
          required: true
          description: ID of the agent
          schema:
            type: string
        - name: sub_account_id
          in: query
          required: false
          description: Optional. Act on a managed account you administer.
          schema:
            type:
              - string
              - "null"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              description: Supply kb_source_ids. A single kb_source_id is also accepted.
              properties:
                kb_source_ids:
                  type: array
                  description: IDs of the knowledge sources to detach
                  items:
                    type: string
                kb_source_id:
                  type: string
                  description: Alternative to kb_source_ids for detaching exactly one source
            examples:
              many:
                value: { "kb_source_ids": ["kb2QwErTyUi9OpAs", "kb6ZxCvBnM4kLjHg"] }
      responses:
        '200':
          description: Knowledge sources detached
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  agent_id:
                    type: string
                  kb_source_ids:
                    type: array
                    description: Every source detached by this call
                    items:
                      type: string
        '400':
          description: No IDs were supplied
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["Missing required field: kb_source_ids"]
        '404':
          $ref: '#/components/responses/AgentNotFound'
  /agents/{agentId}/kb-sources/{kbSourceId}:
    delete:
      summary: Detach Knowledge Source from Agent
      security:
        - ApiKeyAuth: []
      description: |
        Takes one knowledge source away from the agent. The source itself is not
        deleted and stays available to your other agents. Detaching one that is
        not attached changes nothing.

        To detach several at once use
        POST /agents/{agentId}/kb-sources/bulk-remove.
      tags:
        - Agents
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: agentId
          in: path
          required: true
          description: ID of the agent
          schema:
            type: string
        - name: kbSourceId
          in: path
          required: true
          description: ID of the knowledge source to detach
          schema:
            type: string
        - name: sub_account_id
          in: query
          required: false
          description: Optional. Act on a managed account you administer.
          schema:
            type:
              - string
              - "null"
      responses:
        '200':
          description: Knowledge source detached
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  agent_id:
                    type: string
                  kb_source_id:
                    type: string
        '400':
          description: kbSourceId was missing from the path
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["Missing required parameter: kbSourceId"]
        '404':
          $ref: '#/components/responses/AgentNotFound'
  /agents/{agentId}/tags:
    post:
      summary: Add Agent Tag
      security:
        - ApiKeyAuth: []
      description: |
        Adds one tagging rule to the agent: a tag name plus a description of
        when it applies. During a conversation the agent reads that description
        and tags the contact when it fits, which is how tag-driven automations
        get triggered.

        Link the rule to an existing tag on your account by sending tag_id.
        Without it the rule is linked to the tag on your account with the same
        name (created for you if there is none yet), so every rule can be
        addressed by tag ID afterwards. An optional webhook is called when the
        tag is applied, and ai_can_remove lets the agent take the tag off again.

        To design a whole set of these automatically, use
        POST /agents/{agentId}/tags/generate.
      tags:
        - Agents
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: agentId
          in: path
          required: true
          description: ID of the agent
          schema:
            type: string
        - name: sub_account_id
          in: query
          required: false
          description: Optional. Act on a managed account you administer.
          schema:
            type:
              - string
              - "null"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - tag
              properties:
                tag:
                  $ref: '#/components/schemas/AgentTagRule'
            examples:
              hot:
                summary: Tag contacts who are ready to buy
                value:
                  tag:
                    name: hot-lead
                    description: Apply when the contact asks about pricing or wants to book a call.
                    tag_id: tg8YuIoP2aSdF3gH
                    ai_can_remove: false
      responses:
        '200':
          description: Tag rule added
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  agent_id:
                    type: string
                  tag:
                    $ref: '#/components/schemas/AgentTagRule'
        '400':
          description: tag was missing, was not an object, or had no name
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["Missing required field: tag.name"]
        '404':
          $ref: '#/components/responses/AgentNotFound'
  /agents/{agentId}/tags/generate:
    post:
      summary: Generate Agent Tags with AI
      security:
        - ApiKeyAuth: []
      description: |
        Designs a whole set of tagging rules for the agent - the tag names and
        the "apply when ..." wording behind each - by reading the agent's own
        instructions and goal.

        The work runs in the background: this call returns straight away and the
        rules appear on the agent when it finishes. Read the agent
        (GET /agents/{agentId}) and watch tag_generation.status; the rules
        themselves land in the agent's tags. Expect this to take a short while,
        and note it uses AI credits.

        mode "merge" (the default) keeps the rules already on the agent and adds
        to them; "replace" designs the set from scratch. Only one run at a time
        per agent.
      tags:
        - Agents
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: agentId
          in: path
          required: true
          description: ID of the agent
          schema:
            type: string
        - name: sub_account_id
          in: query
          required: false
          description: Optional. Act on a managed account you administer.
          schema:
            type:
              - string
              - "null"
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                mode:
                  type: string
                  enum: [merge, replace]
                  description: Keep the existing rules and add to them (merge, the default), or design the set from scratch (replace).
            examples:
              replace:
                value: { "mode": "replace" }
      responses:
        '202':
          description: Tag generation started
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  agent_id:
                    type: string
                  mode:
                    type: string
                    enum: [merge, replace]
        '400':
          description: mode was something other than merge or replace
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ['Invalid mode. Use "merge" or "replace".']
        '404':
          $ref: '#/components/responses/AgentNotFound'
        '409':
          description: Tag generation is already running for this agent
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["Tag generation is already running for this agent."]
  /agents/{agentId}/tags/{tagId}:
    put:
      summary: Update Agent Tag
      security:
        - ApiKeyAuth: []
      description: |
        Replaces one of the agent's tagging rules. The rule is found by the ID
        in the path; the tag it points at is preserved even if you leave tag_id
        out, so an edit cannot detach the rule from its tag.

        The rule is replaced wholesale, not merged - send the full rule, not
        just the part you are changing.
      tags:
        - Agents
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: agentId
          in: path
          required: true
          description: ID of the agent
          schema:
            type: string
        - name: tagId
          in: path
          required: true
          description: ID of the tag whose rule should be replaced
          schema:
            type: string
        - name: sub_account_id
          in: query
          required: false
          description: Optional. Act on a managed account you administer.
          schema:
            type:
              - string
              - "null"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - tag
              properties:
                tag:
                  $ref: '#/components/schemas/AgentTagRule'
            examples:
              reword:
                summary: Reword when the tag should be applied
                value:
                  tag:
                    name: hot-lead
                    description: Apply only when the contact asks to book a call.
      responses:
        '200':
          description: Tag rule replaced
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  agent_id:
                    type: string
                  tag_id:
                    type: string
        '400':
          description: tag was missing, was not an object, or had no name
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["Missing required field: tag.name"]
        '404':
          description: No such agent, or the agent has no rule for that tag
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples:
                      - "Agent not found"
                      - "Tag not found in agent tags"
    delete:
      summary: Remove Agent Tag
      security:
        - ApiKeyAuth: []
      description: |
        Removes one tagging rule from the agent, so it stops applying that tag
        during conversations. The tag itself, and any contacts already carrying
        it, are untouched.
      tags:
        - Agents
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: agentId
          in: path
          required: true
          description: ID of the agent
          schema:
            type: string
        - name: tagId
          in: path
          required: true
          description: ID of the tag whose rule should be removed
          schema:
            type: string
        - name: sub_account_id
          in: query
          required: false
          description: Optional. Act on a managed account you administer.
          schema:
            type:
              - string
              - "null"
      responses:
        '200':
          description: Tag rule removed
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  agent_id:
                    type: string
                  tag_id:
                    type: string
        '404':
          description: No such agent, or the agent has no rule for that tag
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples:
                      - "Agent not found"
                      - "Tag not found in agent tags"
  /agents/{agentId}/mcp-servers:
    post:
      summary: Attach MCP Server to Agent
      security:
        - ApiKeyAuth: []
      description: |
        Lets this agent use the tools exposed by one of your MCP servers during
        conversations. Attaching a server that is already attached changes
        nothing.

        Only servers belonging to the same account can be attached - the server
        is always resolved inside that account, whatever ID is sent.
      tags:
        - Agents
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: agentId
          in: path
          required: true
          description: ID of the agent
          schema:
            type: string
        - name: sub_account_id
          in: query
          required: false
          description: Optional. Act on a managed account you administer.
          schema:
            type:
              - string
              - "null"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - mcp_server_id
              properties:
                mcp_server_id:
                  type: string
                  description: ID of the MCP server to attach
            examples:
              attach:
                value: { "mcp_server_id": "ms4TgBnH7yUj2kLp" }
      responses:
        '200':
          description: MCP server attached
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  agent_id:
                    type: string
                  mcp_server_id:
                    type: string
        '400':
          description: Missing or malformed mcp_server_id
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["Missing required field: mcp_server_id"]
        '404':
          $ref: '#/components/responses/AgentNotFound'
  /agents/{agentId}/mcp-servers/{mcpServerId}:
    delete:
      summary: Detach MCP Server from Agent
      security:
        - ApiKeyAuth: []
      description: |
        Stops this agent from using the MCP server's tools. The server itself is
        not deleted and stays available to your other agents. Detaching one that
        is not attached changes nothing.
      tags:
        - Agents
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: agentId
          in: path
          required: true
          description: ID of the agent
          schema:
            type: string
        - name: mcpServerId
          in: path
          required: true
          description: ID of the MCP server to detach
          schema:
            type: string
        - name: sub_account_id
          in: query
          required: false
          description: Optional. Act on a managed account you administer.
          schema:
            type:
              - string
              - "null"
      responses:
        '200':
          description: MCP server detached
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  agent_id:
                    type: string
                  mcp_server_id:
                    type: string
        '400':
          description: Malformed mcpServerId
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["Invalid mcpServerId. Use letters, numbers, underscores, and hyphens only."]
        '404':
          $ref: '#/components/responses/AgentNotFound'
  /agents/{agentId}/template-generation:
    post:
      summary: Generate Agent Follow-up Templates
      security:
        - ApiKeyAuth: []
      description: |
        Writes the agent's follow-up messages for you - the nudges it sends when
        a conversation goes quiet - based on what the agent is for.

        There are two ways this comes back, and the target field tells you
        which:
          - target "agent" with status 200: the messages were written during the
            call and the result is in data. Read them back from the agent's
            follow_up_config. This is the usual case.
          - target "campaign" with status 202: the work was queued against the
            campaign named in campaign_id. Watch that campaign's
            template_generation_status until it finishes.

        type "all" (the default) writes the whole set. type "cold_only" writes
        only the messages for contacts who never replied, and needs an outgoing
        campaign - it is refused on an agent that has none.

        Uses AI credits.
      tags:
        - Agents
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: agentId
          in: path
          required: true
          description: ID of the agent
          schema:
            type: string
        - name: sub_account_id
          in: query
          required: false
          description: Optional. Act on a managed account you administer.
          schema:
            type:
              - string
              - "null"
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                type:
                  type: string
                  enum: [all, cold_only]
                  description: Which follow-ups to write. Defaults to all.
            examples:
              all:
                value: { "type": "all" }
      responses:
        '200':
          description: The follow-ups were written during the call
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  agent_id:
                    type: string
                  campaign_id:
                    type: ['string', 'null']
                    description: Always null on this arm
                  target:
                    type: string
                    enum: [agent]
                  type:
                    type: string
                    enum: [all]
                  data:
                    type: object
                    description: The generated follow-ups
                    additionalProperties: true
        '202':
          description: The work was queued against the agent's campaign
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  agent_id:
                    type: string
                  campaign_id:
                    type: string
                    description: Watch template_generation_status on this campaign for progress
                  target:
                    type: string
                    enum: [campaign]
                  type:
                    type: string
                    enum: [all, cold_only]
        '400':
          description: type was something other than all or cold_only, or the account does not have enough AI credits
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error_code:
                    type: integer
                    examples: [400]
                  error:
                    type: string
                    examples:
                      - "Invalid field: type. Must be one of: all, cold_only"
                      - "Insufficient credits."
        '403':
          description: Automatic follow-ups are not switched on for this account, or the caller may not edit them
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error_code:
                    type: integer
                    examples: [403]
                  error:
                    type: string
        '404':
          $ref: '#/components/responses/AgentNotFound'
        '409':
          description: cold_only was asked for on an agent that has no outgoing campaign
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error_code:
                    type: integer
                    examples: [409]
                  reason:
                    type: string
                    enum: [cold_only_requires_campaign]
                  error:
                    type: string
                    examples: ["Cold-lead follow-up templates need an outgoing campaign. Generate the full follow-up set instead."]
  /agents/{agentId}/optimize:
    post:
      summary: Optimize Agent with AI
      security:
        - ApiKeyAuth: []
      description: |
        Rewrites the agent's configuration from your feedback - "it keeps
        offering discounts", "answers are too long" - and saves the rewrite as a
        draft rather than putting it live.

        Send either user_feedback (a plain instruction) or, when reacting to a
        specific bad reply, thumbs_down_feedback together with the offending
        thumbs_down_message. One of the two is required.

        The work runs in the background: this call returns straight away. Read
        the agent (GET /agents/{agentId}) and watch optimize_run.status; once it
        is back to "Draft" the rewrite is waiting as the agent's draft. Review
        it, then either POST /agents/{agentId}/publish-draft to go live or
        POST /agents/{agentId}/discard-draft to throw it away.

        One run at a time per agent. Uses AI credits.
      tags:
        - Agents
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: agentId
          in: path
          required: true
          description: ID of the agent
          schema:
            type: string
        - name: sub_account_id
          in: query
          required: false
          description: Optional. Act on a managed account you administer.
          schema:
            type:
              - string
              - "null"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              description: Send user_feedback, or thumbs_down_feedback. At least one of the two must be non-empty.
              properties:
                user_feedback:
                  type: string
                  description: What you want the agent to do differently
                  examples: ["Stop offering discounts and keep replies under three sentences."]
                thumbs_down_feedback:
                  type: string
                  description: What was wrong with a particular reply
                  examples: ["This answer invented a delivery time we do not offer."]
                thumbs_down_message:
                  type: string
                  description: The agent reply the feedback is about, so the rewrite has the context
            examples:
              direct:
                summary: Straightforward instruction
                value: { "user_feedback": "Keep replies under three sentences." }
              thumbsDown:
                summary: React to a specific bad reply
                value:
                  thumbs_down_feedback: This answer invented a delivery time we do not offer.
                  thumbs_down_message: We deliver everywhere within 24 hours.
      responses:
        '202':
          description: Optimization started
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  agent_id:
                    type: string
        '400':
          description: Neither user_feedback nor thumbs_down_feedback carried any text
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["Missing required field: provide a non-empty user_feedback or thumbs_down_feedback"]
        '404':
          $ref: '#/components/responses/AgentNotFound'
        '409':
          description: An optimization is already running for this agent
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["An optimization is already running for this agent."]
  /agents/{agentId}/entry-points:
    get:
      summary: List Agent Entry Points
      security:
        - ApiKeyAuth: []
      description: |
        Returns the routing rules that send conversations to this agent, newest
        first - its channel defaults, keyword rules, comment rules and follower
        rules. Both current and retired rules come back; a retired one has
        enabled false.

        This is the per-agent view. For the whole account's channel defaults,
        including a channel deliberately set to nobody, read
        GET /entry-points/channel-defaults instead.
      tags:
        - Agents
        - Entry Points
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: agentId
          in: path
          required: true
          description: ID of the agent
          schema:
            type: string
        - name: sub_account_id
          in: query
          required: false
          description: Optional. Read an agent on a managed account you administer.
          schema:
            type:
              - string
              - "null"
      responses:
        '200':
          description: Entry points retrieved successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  entry_points:
                    type: array
                    items:
                      $ref: '#/components/schemas/EntryPoint'
        '404':
          $ref: '#/components/responses/AgentNotFound'
    post:
      summary: Create Agent Entry Point
      security:
        - ApiKeyAuth: []
      description: |
        Creates a routing rule that sends conversations to this agent. The agent
        in the path always wins, so a rule can never be created for a different
        agent than the one in the URL.

        Pick the kind with type:
          - channel_default - the agent answers every new contact on the listed
            channels. Prefer PUT /entry-points/channel-defaults for this: it
            retires the previous answerer for you, which creating a second
            default here does not.
          - keyword - the agent takes over when the first message contains one
            of match_config.keywords. At least one keyword is required.
          - instagram_comment / facebook_comment - the agent replies to comments
            on your posts. The matching channel must be listed in channels.
          - instagram_follower - the agent greets new followers.

        channels is required and says which channels the rule covers, for
        example whatsapp, whatsapp_web, instagram, messenger, telegram, sms,
        email, chat_widget or custom_channel. New rules are enabled unless you
        say otherwise.

        Which rule wins when several could: an ongoing conversation or a manual
        assignment keeps the agent it already has; otherwise keyword rules beat
        comment rules, which beat follower rules, and a channel default is the
        last resort. Whether these rules decide anything yet on this account is
        reported by GET /entry-points/routing-status.
      tags:
        - Agents
        - Entry Points
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: agentId
          in: path
          required: true
          description: ID of the agent the rule routes to
          schema:
            type: string
        - name: sub_account_id
          in: query
          required: false
          description: Optional. Act on a managed account you administer.
          schema:
            type:
              - string
              - "null"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - $ref: '#/components/schemas/EntryPointWriteRequest'
                - type: object
                  required:
                    - type
                    - channels
            examples:
              keyword:
                summary: Hand the conversation over when someone mentions pricing
                value:
                  type: keyword
                  channels: ["whatsapp", "instagram"]
                  match_config: { "keywords": ["pricing", "quote"] }
              comments:
                summary: Reply to comments on Instagram posts
                value:
                  type: instagram_comment
                  channels: ["instagram"]
                  match_config: {}
      responses:
        '201':
          description: Entry point created
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  entry_point_id:
                    type: string
                    examples: ["ep3KmQ8vTzXr5nWd"]
        '400':
          description: The rule is not usable - unknown type, no channels, a keyword rule without keywords, or a comment rule missing its own channel
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples:
                      - "channels (non-empty array of channel strings) is required."
                      - "A keyword entry point requires at least one keyword in match_config.keywords."
                      - 'A instagram_comment entry point must include "instagram" in channels.'
        '404':
          $ref: '#/components/responses/AgentNotFound'
  /campaigns/{campaignId}/media-library:
    get:
      summary: List Campaign Media
      security:
        - ApiKeyAuth: []
      description: |
        Returns the media items stored on this campaign, newest first.

        This is the campaign's own library. If you work with AI Agents, use
        GET /agents/{agentId}/media-library instead - it shows everything the
        agent can actually send, including items that still live on the campaign
        it was built from.

        The media_url on each item is the download link that was created when the
        file was uploaded, and it expires after 7 days.
      tags:
        - Campaigns
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: campaignId
          in: path
          required: true
          description: ID of the campaign
          schema:
            type: string
        - name: sub_account_id
          in: query
          required: false
          description: Optional. Read the media of a campaign on a managed account you administer.
          schema:
            type:
              - string
              - "null"
      responses:
        '200':
          description: Media retrieved successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  campaign_id:
                    type: string
                  media_items:
                    type: array
                    items:
                      $ref: '#/components/schemas/MediaLibraryItem'
        '404':
          description: Campaign not found
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["Campaign not found"]
    post:
      summary: Upload Campaign Media
      security:
        - ApiKeyAuth: []
      description: |
        Adds a file to the campaign's media library so the bot can send it during
        a conversation. The file is uploaded inline as base64 (maximum 10 MB) and
        the call returns once it is stored, so allow a little longer than for a
        normal request.

        Two things happen automatically: an animated GIF is converted to video so
        it plays on every channel, and the platform writes a short summary of what
        is actually in the file so the bot knows when it fits.

        A campaign can hold at most 50 media items.
      tags:
        - Campaigns
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: campaignId
          in: path
          required: true
          description: ID of the campaign
          schema:
            type: string
        - name: sub_account_id
          in: query
          required: false
          description: Optional. Upload onto a campaign of a managed account you administer.
          schema:
            type:
              - string
              - "null"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/MediaLibraryUploadRequest'
            examples:
              upload:
                summary: Upload a PDF the bot may send
                value:
                  base64Data: "JVBERi0xLjQKJcfs..."
                  mimeType: application/pdf
                  fileName: spring-menu.pdf
                  title: Spring menu
                  description: Send when someone asks what is on the menu.
                  maxSendsPerConversation: 1
      responses:
        '200':
          description: Media item created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MediaLibraryUploadResponse'
        '400':
          description: Missing required fields, unsupported file type, empty or oversized file, or the 50-item limit has been reached
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["File size exceeds maximum allowed size of 10MB"]
        '403':
          description: The AI media library is switched off for this account, or the campaign belongs to another account
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["AI Media Library is disabled for this account."]
        '404':
          description: Campaign not found
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["The specified campaign does not exist."]
  /campaigns/{campaignId}/media-library/{itemId}:
    patch:
      summary: Update Campaign Media Item
      security:
        - ApiKeyAuth: []
      description: |
        Edits a media item's details. The file itself cannot be replaced - upload
        a new item and delete the old one instead.
      tags:
        - Campaigns
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: campaignId
          in: path
          required: true
          description: ID of the campaign
          schema:
            type: string
        - name: itemId
          in: path
          required: true
          description: ID of the media item
          schema:
            type: string
        - name: sub_account_id
          in: query
          required: false
          description: Optional. Act on a managed account you administer.
          schema:
            type:
              - string
              - "null"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/MediaLibraryItemUpdate'
            examples:
              retitle:
                value: { "title": "Summer menu", "max_sends_per_conversation": 2 }
      responses:
        '200':
          description: Media item updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  campaign_id:
                    type: string
                  item_id:
                    type: string
        '400':
          description: Malformed itemId, no fields to update, or a rejected field value
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["Invalid field: max_sends_per_conversation. Must be a non-negative integer or null."]
        '404':
          description: Campaign or media item not found
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["Media library item not found"]
    delete:
      summary: Delete Campaign Media Item
      security:
        - ApiKeyAuth: []
      description: |
        Removes a media item and its stored file. Deleting an item that is
        already gone succeeds and reports deleted false, so the call is safe to
        retry.
      tags:
        - Campaigns
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: campaignId
          in: path
          required: true
          description: ID of the campaign
          schema:
            type: string
        - name: itemId
          in: path
          required: true
          description: ID of the media item
          schema:
            type: string
        - name: sub_account_id
          in: query
          required: false
          description: Optional. Act on a managed account you administer.
          schema:
            type:
              - string
              - "null"
      responses:
        '200':
          description: Media item removed
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  deleted:
                    type: boolean
                    description: False when the item was already gone
        '403':
          description: The campaign belongs to another account
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["Campaign not in your account."]
        '404':
          description: Campaign not found
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["The specified campaign does not exist."]
  /custom-functions:
    get:
      summary: List Custom Functions
      security:
        - ApiKeyAuth: []
      description: |
        Returns every custom function on the account, newest first, with its
        full definition including the headers it sends.
      tags:
        - Custom Functions
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: Optional. List the custom functions of a managed account you administer.
          schema:
            type:
              - string
              - "null"
      responses:
        '200':
          description: Custom functions retrieved successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  custom_functions:
                    type: array
                    items:
                      $ref: '#/components/schemas/CustomFunction'
              examples:
                success:
                  value: {
                    "success": true,
                    "custom_functions": [
                      {
                        "id": "cf7Hk2ZpLxR3mNbV",
                        "custom_function_id": "cf7Hk2ZpLxR3mNbV",
                        "name": "check_order_status",
                        "description": "Looks up the status of an order by its order number.",
                        "url": "https://example.com/api/orders",
                        "method": "GET",
                        "ai_action": "Tell the customer the status and the expected delivery date.",
                        "skip_system_data": false,
                        "headers": [{ "key": "Authorization", "value": "Bearer sk-live-..." }],
                        "input": [
                          { "name": "order_id", "type": "query_param", "description": "The order number the customer mentioned", "required": true }
                        ],
                        "created_at": 1700000000000,
                        "updated_at": 1700000000000
                      }
                    ]
                  }
    post:
      summary: Create Custom Function
      security:
        - ApiKeyAuth: []
      description: |
        Creates a custom function. Every field is optional, so an empty body
        creates a blank function you can fill in later - which is what the
        dashboard does. Whatever you do send is validated.

        Creating a function does not make it available to the AI yet: attach it
        to an agent with POST /agents/{agentId}/custom-functions.
      tags:
        - Custom Functions
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: Optional. Create the function on a managed account you administer.
          schema:
            type:
              - string
              - "null"
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CustomFunctionWrite'
            examples:
              create:
                summary: Create a ready-to-use function
                value:
                  name: check_order_status
                  description: Looks up the status of an order by its order number.
                  url: https://example.com/api/orders
                  method: GET
                  ai_action: Tell the customer the status and the expected delivery date.
                  headers: [{ "key": "Authorization", "value": "Bearer sk-live-..." }]
                  input: [{ "name": "order_id", "type": "query_param", "description": "The order number the customer mentioned", "required": true }]
      responses:
        '201':
          description: Custom function created
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  custom_function_id:
                    type: string
                    examples: ["cf7Hk2ZpLxR3mNbV"]
        '400':
          $ref: '#/components/responses/CustomFunctionBadRequest'
  /custom-functions/test:
    post:
      summary: Test a Custom Function
      security:
        - ApiKeyAuth: []
      description: |
        Calls an endpoint exactly the way a custom function would and hands back
        what it answered, so you can check a configuration before saving it.
        Nothing is stored and no function ID is involved - the URL, method,
        headers and input values all come from the request.

        A call that cannot be made at all (an unsupported method, an unreachable
        URL) still answers 200, with statusCode 0 and the reason in the body.
      tags:
        - Custom Functions
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: Optional. Run the test as a managed account you administer.
          schema:
            type:
              - string
              - "null"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - url
                - method
              properties:
                url:
                  type: string
                  description: The endpoint to call
                method:
                  type: string
                  enum: [GET, POST, PUT, DELETE, PATCH]
                headers:
                  type: array
                  items:
                    $ref: '#/components/schemas/CustomFunctionHeader'
                testInputValues:
                  type: object
                  description: Values to send as the function's inputs
                  additionalProperties: true
            examples:
              test:
                value:
                  url: https://example.com/api/orders
                  method: GET
                  headers: [{ "key": "Authorization", "value": "Bearer sk-live-..." }]
                  testInputValues: { "order_id": "1234" }
      responses:
        '200':
          description: The call was attempted and the result is reported
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  statusCode:
                    type: integer
                    description: HTTP status the endpoint answered with, or 0 when the call could not be made
                    examples: [200]
                  body:
                    type: string
                    description: The response body as text, or the reason the call could not be made
        '400':
          description: Missing url or method
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["Missing required parameter: url"]
        '404':
          description: Custom functions are not available on this account
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["Custom function not found"]
  /custom-functions/{customFunctionId}:
    get:
      summary: Get Custom Function
      security:
        - ApiKeyAuth: []
      description: Returns one custom function's full definition, including the headers it sends.
      tags:
        - Custom Functions
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: customFunctionId
          in: path
          required: true
          description: ID of the custom function
          schema:
            type: string
        - name: sub_account_id
          in: query
          required: false
          description: Optional. Read a function on a managed account you administer.
          schema:
            type:
              - string
              - "null"
      responses:
        '200':
          description: Custom function retrieved successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  custom_function:
                    $ref: '#/components/schemas/CustomFunction'
        '404':
          $ref: '#/components/responses/CustomFunctionNotFound'
    put:
      summary: Update Custom Function
      security:
        - ApiKeyAuth: []
      description: |
        Updates a custom function. Send only the fields you want to change.

        headers and input are lists, so sending one replaces the stored list in
        full - read the function first if you only mean to add or change one
        entry, or use the per-entry endpoints below.

        A function the automations builder manages cannot be edited here; edit
        its automation instead.
      tags:
        - Custom Functions
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: customFunctionId
          in: path
          required: true
          description: ID of the custom function
          schema:
            type: string
        - name: sub_account_id
          in: query
          required: false
          description: Optional. Update a function on a managed account you administer.
          schema:
            type:
              - string
              - "null"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CustomFunctionWrite'
            examples:
              rename:
                value: { "name": "check_order_status", "ai_action": "Give the customer the delivery date." }
      responses:
        '200':
          description: Custom function updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  custom_function_id:
                    type: string
        '400':
          $ref: '#/components/responses/CustomFunctionBadRequest'
        '404':
          $ref: '#/components/responses/CustomFunctionNotFound'
        '409':
          $ref: '#/components/responses/CustomFunctionManagedByAutomation'
    delete:
      summary: Delete Custom Function
      security:
        - ApiKeyAuth: []
      description: |
        Deletes a custom function. Agents that had it attached simply stop
        offering it.

        A function the automations builder manages cannot be deleted here;
        delete its automation instead.
      tags:
        - Custom Functions
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: customFunctionId
          in: path
          required: true
          description: ID of the custom function
          schema:
            type: string
        - name: sub_account_id
          in: query
          required: false
          description: Optional. Delete a function on a managed account you administer.
          schema:
            type:
              - string
              - "null"
      responses:
        '200':
          description: Custom function deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
        '404':
          $ref: '#/components/responses/CustomFunctionNotFound'
        '409':
          $ref: '#/components/responses/CustomFunctionManagedByAutomation'
  /custom-functions/{customFunctionId}/headers:
    post:
      summary: Add Custom Function Header
      security:
        - ApiKeyAuth: []
      description: |
        Appends one header to the function. Convenience for adding a single
        header without re-sending the whole list; the full header list comes
        back in the response. A function may hold at most 200 headers.
      tags:
        - Custom Functions
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: customFunctionId
          in: path
          required: true
          description: ID of the custom function
          schema:
            type: string
        - name: sub_account_id
          in: query
          required: false
          description: Optional. Act on a managed account you administer.
          schema:
            type:
              - string
              - "null"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CustomFunctionHeader'
            examples:
              add:
                value: { "key": "Authorization", "value": "Bearer sk-live-..." }
      responses:
        '200':
          description: Header added
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  custom_function_id:
                    type: string
                  headers:
                    type: array
                    description: The function's headers after the change
                    items:
                      $ref: '#/components/schemas/CustomFunctionHeader'
        '400':
          $ref: '#/components/responses/CustomFunctionBadRequest'
        '404':
          $ref: '#/components/responses/CustomFunctionNotFound'
  /custom-functions/{customFunctionId}/headers/{index}:
    delete:
      summary: Remove Custom Function Header
      security:
        - ApiKeyAuth: []
      description: |
        Removes the header at the given position, counting from 0 in the order
        the headers are returned. The remaining headers come back in the
        response.
      tags:
        - Custom Functions
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: customFunctionId
          in: path
          required: true
          description: ID of the custom function
          schema:
            type: string
        - name: index
          in: path
          required: true
          description: Position of the header to remove, counting from 0
          schema:
            type: integer
            minimum: 0
        - name: sub_account_id
          in: query
          required: false
          description: Optional. Act on a managed account you administer.
          schema:
            type:
              - string
              - "null"
      responses:
        '200':
          description: Header removed
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  custom_function_id:
                    type: string
                  headers:
                    type: array
                    items:
                      $ref: '#/components/schemas/CustomFunctionHeader'
        '400':
          description: The index is not a non-negative whole number, or there is no header at that position
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["index out of range"]
        '404':
          $ref: '#/components/responses/CustomFunctionNotFound'
  /custom-functions/{customFunctionId}/input:
    post:
      summary: Add Custom Function Input
      security:
        - ApiKeyAuth: []
      description: |
        Appends one input parameter to the function. Convenience for adding a
        single input without re-sending the whole list; the full input list comes
        back in the response. A function may hold at most 200 inputs.
      tags:
        - Custom Functions
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: customFunctionId
          in: path
          required: true
          description: ID of the custom function
          schema:
            type: string
        - name: sub_account_id
          in: query
          required: false
          description: Optional. Act on a managed account you administer.
          schema:
            type:
              - string
              - "null"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CustomFunctionInputParameter'
            examples:
              add:
                value: { "name": "order_id", "type": "query_param", "description": "The order number the customer mentioned", "required": true }
      responses:
        '200':
          description: Input added
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  custom_function_id:
                    type: string
                  input:
                    type: array
                    description: The function's inputs after the change
                    items:
                      $ref: '#/components/schemas/CustomFunctionInputParameter'
        '400':
          $ref: '#/components/responses/CustomFunctionBadRequest'
        '404':
          $ref: '#/components/responses/CustomFunctionNotFound'
  /custom-functions/{customFunctionId}/input/{index}:
    put:
      summary: Edit Custom Function Input
      security:
        - ApiKeyAuth: []
      description: |
        Replaces the input parameter at the given position, counting from 0 in
        the order the inputs are returned. Send the whole parameter - it replaces
        the old one rather than merging with it, and path is dropped when you
        leave it out.
      tags:
        - Custom Functions
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: customFunctionId
          in: path
          required: true
          description: ID of the custom function
          schema:
            type: string
        - name: index
          in: path
          required: true
          description: Position of the input to replace, counting from 0
          schema:
            type: integer
            minimum: 0
        - name: sub_account_id
          in: query
          required: false
          description: Optional. Act on a managed account you administer.
          schema:
            type:
              - string
              - "null"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CustomFunctionInputParameter'
            examples:
              edit:
                value: { "name": "order_id", "type": "string", "description": "The order number, digits only", "required": true }
      responses:
        '200':
          description: Input replaced
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  custom_function_id:
                    type: string
                  input:
                    type: array
                    items:
                      $ref: '#/components/schemas/CustomFunctionInputParameter'
        '400':
          description: The parameter was rejected, the index is not a non-negative whole number, or there is no input at that position
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["index out of range"]
        '404':
          $ref: '#/components/responses/CustomFunctionNotFound'
    delete:
      summary: Remove Custom Function Input
      security:
        - ApiKeyAuth: []
      description: |
        Removes the input parameter at the given position, counting from 0 in the
        order the inputs are returned. The remaining inputs come back in the
        response.
      tags:
        - Custom Functions
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: customFunctionId
          in: path
          required: true
          description: ID of the custom function
          schema:
            type: string
        - name: index
          in: path
          required: true
          description: Position of the input to remove, counting from 0
          schema:
            type: integer
            minimum: 0
        - name: sub_account_id
          in: query
          required: false
          description: Optional. Act on a managed account you administer.
          schema:
            type:
              - string
              - "null"
      responses:
        '200':
          description: Input removed
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  custom_function_id:
                    type: string
                  input:
                    type: array
                    items:
                      $ref: '#/components/schemas/CustomFunctionInputParameter'
        '400':
          description: The index is not a non-negative whole number, or there is no input at that position
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["index out of range"]
        '404':
          $ref: '#/components/responses/CustomFunctionNotFound'
  /snapshots/apply-targets:
    get:
      summary: List Snapshot Apply Targets
      description: |
        Lists the accounts a snapshot can be installed onto: your own agency
        account first, then every sub-account you administer. Use the uid of the
        row you want as the sub_account_id when applying a snapshot.

        Agency accounts only.
      tags:
        - Snapshots
      security:
        - ApiKeyAuth: []
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
      responses:
        '200':
          description: Targets retrieved successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  data:
                    type: object
                    properties:
                      targets:
                        type: array
                        items:
                          type: object
                          properties:
                            uid:
                              type: string
                              description: Pass this as sub_account_id when applying a snapshot
                            display_name:
                              type: string
                              description: The account's name, falling back to its email address
                            email:
                              type: string
                            is_self:
                              type: boolean
                              description: True for your own agency account
                            agents_cutover:
                              type: boolean
                              description: Always true. Kept so this response stays interchangeable with the dashboard's own.
              examples:
                success:
                  value: {
                    "success": true,
                    "data": {
                      "targets": [
                        {
                          "uid": "7TgH2v1MzYhVv2S8kRq0bXn3",
                          "display_name": "Your Agency",
                          "email": "you@agency.com",
                          "is_self": true,
                          "agents_cutover": true
                        },
                        {
                          "uid": "9fQ2v1MzYhVv2S8kRq0bXn3TgH2",
                          "display_name": "Bakery client",
                          "email": "owner@bakery.com",
                          "is_self": false,
                          "agents_cutover": true
                        }
                      ]
                    }
                  }
        '403':
          $ref: '#/components/responses/SnapshotsForbidden'
  /snapshots/{snapshotId}:
    patch:
      summary: Rename Snapshot
      description: |
        Renames a snapshot. Nothing about its contents changes.

        Agency accounts only.
      tags:
        - Snapshots
      security:
        - ApiKeyAuth: []
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: snapshotId
          in: path
          required: true
          schema:
            type: string
          description: The snapshot to rename
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - name
              properties:
                name:
                  type: string
                  description: The new name
            examples:
              rename:
                value: { "name": "Real-estate starter kit v2" }
      responses:
        '200':
          description: Snapshot renamed
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  data:
                    type: object
                    properties:
                      snapshot_id:
                        type: string
                      name:
                        type: string
        '400':
          description: Missing or blank name
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error_code:
                    type: integer
                    enum: [400]
                  error:
                    type: string
                    examples: ["name is required."]
        '403':
          $ref: '#/components/responses/SnapshotsForbidden'
        '404':
          description: No such snapshot on this agency
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error_code:
                    type: integer
                    enum: [404]
                  error:
                    type: string
                    examples: ["Snapshot not found."]
    delete:
      summary: Delete Snapshot
      description: |
        Deletes a snapshot and the copies of its files. Accounts it was already
        installed onto keep everything they received - only the template goes
        away.

        If the deleted snapshot was the one new sub-accounts are born with, that
        setting is cleared as well, and the response says so. Otherwise every
        future client account would silently arrive empty.

        Agency accounts only.
      tags:
        - Snapshots
      security:
        - ApiKeyAuth: []
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: snapshotId
          in: path
          required: true
          schema:
            type: string
          description: The snapshot to delete
      responses:
        '200':
          description: Snapshot deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  data:
                    type: object
                    properties:
                      snapshot_id:
                        type: string
                      default_cleared:
                        type: boolean
                        description: True when this snapshot was the default for new sub-accounts and that setting has now been cleared
              examples:
                success:
                  value: {
                    "success": true,
                    "data": { "snapshot_id": "Qk3xR2mLp0aBc9dE", "default_cleared": true }
                  }
        '403':
          $ref: '#/components/responses/SnapshotsForbidden'
        '404':
          description: No such snapshot on this agency
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error_code:
                    type: integer
                    enum: [404]
                  error:
                    type: string
                    examples: ["Snapshot not found."]
  /analytics/series:
    get:
      summary: Get Metric Series
      description: |
        Returns one or more metric time series for your account, grouped by up
        to two dimensions. This is the endpoint to bind a chart to: a single
        call answers questions like "sent and replied per day, per channel, for
        this campaign" that previously needed one request per campaign.

        Every response carries a labels array (the time axis, zero filled across
        the whole range) and one entry in series per group. Each entry has one
        array per requested metric, aligned one to one with labels.

        Series past the limit are not dropped: they are collapsed into
        other_bucket, computed as the range total minus the returned series, so
        a rendered chart always adds up to your real numbers. truncated is true
        whenever that happened.

        Notes on where the numbers come from: sent, delivered, read and replied
        come from message records, which carry the channel and the sending
        number. booked, contact_created and credits_spent come from the event
        stream, which carries no sending number, so those metrics contribute to
        the null number bucket when you group by number.

        The range defaults to the last 30 days and is capped at 92 days for this
        endpoint.
      tags:
        - Analytics
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
        - name: from
          in: query
          required: false
          schema:
            type: string
            format: date
          description: Start of the range (YYYY-MM-DD, inclusive). Defaults to 30 days ago.
          example: "2026-05-01"
        - name: to
          in: query
          required: false
          schema:
            type: string
            format: date
          description: End of the range (YYYY-MM-DD, inclusive). Defaults to today.
          example: "2026-05-31"
        - name: metrics
          in: query
          required: false
          schema:
            type: string
            default: "sent,replied"
          description: |
            Comma separated list of metrics to return. Allowed values are sent,
            delivered, read, replied, booked, contact_created and credits_spent.
            An unknown metric is rejected with a 400 rather than ignored.
          example: "sent,replied,booked"
        - name: group_by
          in: query
          required: false
          schema:
            type: string
          description: |
            Comma separated list of at most two dimensions from date, campaign,
            channel, agent and number. date is accepted but has no effect, since
            every response already carries the time axis. Omit this to get a
            single account wide series.
          example: "campaign,channel"
        - name: granularity
          in: query
          required: false
          schema:
            type: string
            enum: ['day', 'week', 'month']
            default: day
          description: Width of each bucket. Week buckets start on Monday, month buckets on the 1st.
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 50
            default: 12
          description: How many series to return before the rest collapse into other_bucket.
        - name: campaign_id
          in: query
          required: false
          schema:
            type: string
          description: >-
            Only count activity belonging to this campaign. Legacy; prefer
            agent_id.
        - name: agent_id
          in: query
          required: false
          schema:
            type: string
          description: |
            Only count activity belonging to this AI Agent. This is the scope to
            use: the Campaigns surface is hidden on a migrated account, so an
            agent id is normally the only scope you have. Send agent_id OR
            campaign_id, never both (both is a 400). An agent id that is not on
            your account returns 404.
          example: "agent_abc123"
        - name: channel
          in: query
          required: false
          schema:
            type: string
          description: Only count activity on this channel, for example whatsapp.
      responses:
        '200':
          description: The requested series
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  from:
                    type: string
                    format: date
                  to:
                    type: string
                    format: date
                  granularity:
                    type: string
                    enum: ['day', 'week', 'month']
                  labels:
                    type: array
                    description: Bucket start dates, ascending, zero filled across the whole range.
                    items:
                      type: string
                      format: date
                  group_by:
                    type: array
                    description: The dimensions the series keys carry. Never includes date.
                    items:
                      type: string
                  metrics:
                    type: array
                    description: The metrics that were returned, in response order.
                    items:
                      type: string
                  series:
                    type: array
                    items:
                      $ref: '#/components/schemas/AnalyticsSeries'
                  other_bucket:
                    oneOf:
                      - $ref: '#/components/schemas/AnalyticsSeriesOtherBucket'
                      - type: 'null'
                  truncated:
                    type: boolean
                    description: True when at least one series was collapsed into other_bucket.
        '400':
          description: >-
            Invalid date range, metric, group_by dimension or granularity, or
            both agent_id and campaign_id were supplied
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AnalyticsApiError'
        '404':
          description: Campaign not found, or agent not found on this account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AnalyticsApiError'
        '503':
          description: |
            Reporting data is temporarily unavailable for this account. Retry
            shortly. Deliberately not a 200 with zeros, because an all zero
            chart reads as fact.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AnalyticsApiError'
  /analytics/outcomes:
    get:
      summary: Get Conversation Outcomes
      description: |
        Returns how conversations ended over a date range: a per day count for
        each outcome tag the AI assigned, plus range totals for replies,
        bookings, handovers to a human, and conversations that were never
        classified.

        Pass group_by=tag to collapse the time axis and get range totals per tag
        only. In that mode labels is empty and each counts array is empty, while
        total is still populated.

        The range defaults to the last 30 days and is capped at 92 days for this
        endpoint.
      tags:
        - Analytics
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
        - name: from
          in: query
          required: false
          schema:
            type: string
            format: date
          description: Start of the range (YYYY-MM-DD, inclusive). Defaults to 30 days ago.
          example: "2026-05-01"
        - name: to
          in: query
          required: false
          schema:
            type: string
            format: date
          description: End of the range (YYYY-MM-DD, inclusive). Defaults to today.
          example: "2026-05-31"
        - name: campaign_id
          in: query
          required: false
          schema:
            type: string
          description: >-
            Only count conversations with contacts currently on this campaign.
            Legacy; prefer agent_id.
        - name: agent_id
          in: query
          required: false
          schema:
            type: string
          description: |
            Only count outcomes belonging to this AI Agent: events recorded
            against the agent, and conversations whose contact is currently
            assigned to it. This is the scope to use on an agent-native account.
            Send agent_id OR campaign_id, never both (both is a 400). An agent id
            that is not on your account returns 404.
          example: "agent_abc123"
        - name: group_by
          in: query
          required: false
          schema:
            type: string
            enum: ['date', 'tag']
            default: date
          description: date keeps the per day counts. tag collapses the time axis to range totals.
      responses:
        '200':
          description: Conversation outcomes over the range
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  from:
                    type: string
                    format: date
                  to:
                    type: string
                    format: date
                  group_by:
                    type: string
                    enum: ['date', 'tag']
                  labels:
                    type: array
                    description: Days in the range, ascending. Empty when group_by is tag.
                    items:
                      type: string
                      format: date
                  by_tag:
                    type: array
                    description: Outcome tags, highest volume first.
                    items:
                      $ref: '#/components/schemas/AnalyticsOutcomeTag'
                  totals:
                    type: object
                    properties:
                      sessions:
                        type: integer
                        description: Conversations started in the range.
                      replied:
                        type: integer
                        description: Replies received from contacts.
                      booked:
                        type: integer
                        description: Appointments booked.
                      human_alerted:
                        type: integer
                        description: Conversations handed over to a human.
                      unresolved:
                        type: integer
                        description: Conversations the AI never assigned an outcome tag to.
        '400':
          description: >-
            Invalid date range or group_by value, or both agent_id and
            campaign_id were supplied
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AnalyticsApiError'
        '404':
          description: Campaign not found, or agent not found on this account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AnalyticsApiError'
        '503':
          description: Reporting data is temporarily unavailable for this account. Retry shortly.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AnalyticsApiError'
  /analytics/agency-rollup:
    get:
      summary: Get Agency Sub-Account Rollup
      description: |
        Returns messaging and credit activity for every sub-account on your
        agency in ONE request: range totals plus a per day series each. Building
        a multi client report previously meant one call per sub-account, which
        is why a large roster was not reportable within the rate limit.

        **Requires Agency or Dev role.** A sub_account_id that is not on your
        agency simply returns an empty roster rather than an error, so other
        accounts stay unenumerable.

        The range defaults to the last 30 days and is capped at 92 days for this
        endpoint.
      tags:
        - Agency
        - Analytics
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: from
          in: query
          required: false
          schema:
            type: string
            format: date
          description: Start of the range (YYYY-MM-DD, inclusive). Defaults to 30 days ago.
          example: "2026-05-01"
        - name: to
          in: query
          required: false
          schema:
            type: string
            format: date
          description: End of the range (YYYY-MM-DD, inclusive). Defaults to today.
          example: "2026-05-31"
        - name: sub_account_id
          in: query
          required: false
          schema:
            type: string
          description: Narrow the rollup to a single sub-account.
      responses:
        '200':
          description: Per sub-account activity over the range
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  from:
                    type: string
                    format: date
                  to:
                    type: string
                    format: date
                  labels:
                    type: array
                    description: Days in the range, ascending.
                    items:
                      type: string
                      format: date
                  sub_accounts:
                    type: array
                    description: Sub-accounts, busiest first. Sub-accounts with no activity are still listed, with zeroed series.
                    items:
                      $ref: '#/components/schemas/AgencyRollupSubAccount'
                  totals:
                    type: object
                    properties:
                      sent:
                        type: integer
                      replied:
                        type: integer
                      booked:
                        type: integer
                      contacts:
                        type: integer
                      credits_spent:
                        type: number
                      sub_account_count:
                        type: integer
        '400':
          description: Invalid date range
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AnalyticsApiError'
        '403':
          description: Account does not have the Agency or Dev role
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AnalyticsApiError'
        '503':
          description: Reporting data is temporarily unavailable. Retry shortly.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AnalyticsApiError'
  /analytics/dashboard-insights:
    get:
      summary: Get Dashboard Insights
      description: |
        Returns the full dashboard payload for a date range in one call: a reply
        rate heatmap by weekday and hour, the campaign leaderboard, per channel
        volume, exact per connection totals, per day metric breakdowns account
        wide as well as per channel and per number, where contacts are from, the
        inbox response time, and a recent activity feed.

        This is the richest reporting payload available. Several blocks are
        computed from the reporting database and come back as null when it
        cannot answer for your account: null means unavailable, NOT zero
        activity, so do not render a null block as an empty chart.

        Dates use startDate and endDate (not from and to) because this endpoint
        shares its implementation with the in-app dashboard. The range is capped
        at 92 days and is clamped, not rejected, when it is wider.
      tags:
        - Analytics
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
        - name: startDate
          in: query
          required: true
          schema:
            type: string
            format: date
          description: Start of the range (YYYY-MM-DD, inclusive).
          example: "2026-05-01"
        - name: endDate
          in: query
          required: true
          schema:
            type: string
            format: date
          description: End of the range (YYYY-MM-DD, inclusive).
          example: "2026-05-31"
        - name: campaignId
          in: query
          required: false
          schema:
            type: string
          description: >-
            Only include activity belonging to this campaign (campaign_id is
            accepted as an alias). Legacy; prefer agent_id.
        - name: agent_id
          in: query
          required: false
          schema:
            type: string
          description: |
            Only include activity belonging to this AI Agent (agentId is accepted
            as an alias). This is the scope to use on an agent-native account.
            Send agent_id OR campaignId, never both (both is a 400). An agent id
            that is not on your account returns 404. Under an agent scope the
            campaign leaderboard is built from that agent's activity only.
          example: "agent_abc123"
      responses:
        '200':
          description: The dashboard insights payload
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    $ref: '#/components/schemas/DashboardInsights'
        '400':
          description: Missing startDate or endDate, or both agent_id and campaignId were supplied
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AnalyticsApiError'
        '404':
          description: Agent not found on this account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AnalyticsApiError'
  /analytics/aggregate:
    get:
      summary: Get Aggregated Event Counts
      description: |
        Returns the same aggregated event counts as GET /analytics/summary, but
        in the camelCase shape the in-app dashboard parses (contactCreated
        rather than contact_created, byDate rather than by_date). Prefer
        /analytics/summary for new integrations; this endpoint exists so the
        in-app dashboard and the API share one implementation.

        Dates use startDate and endDate in ISO form (not from and to).
      tags:
        - Analytics
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
        - name: startDate
          in: query
          required: false
          schema:
            type: string
          description: Start of the range as an ISO date or date-time. Defaults to the same window /analytics/summary uses.
          example: "2026-05-01"
        - name: endDate
          in: query
          required: false
          schema:
            type: string
          description: End of the range as an ISO date or date-time.
          example: "2026-05-31"
        - name: campaignId
          in: query
          required: false
          schema:
            type: string
          description: >-
            Only count events belonging to this campaign (campaign_id is accepted
            as an alias). Legacy; prefer agent_id.
        - name: agent_id
          in: query
          required: false
          schema:
            type: string
          description: |
            Only count events belonging to this AI Agent (agentId is accepted as
            an alias). This is the scope to use on an agent-native account. Send
            agent_id OR campaignId, never both (both is a 400). An agent id that
            is not on your account returns 404.
          example: "agent_abc123"
      responses:
        '200':
          description: Aggregated event counts in camelCase form
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    type: object
                    properties:
                      from:
                        type: string
                      to:
                        type: string
                      total:
                        type: integer
                        description: Total number of events in the range.
                      byAnalyticType:
                        $ref: '#/components/schemas/AnalyticsAggregateCounts'
                      byDate:
                        type: array
                        description: Per day counters, ascending by date.
                        items:
                          type: object
                          properties:
                            date:
                              type: string
                              format: date
                            byAnalyticType:
                              $ref: '#/components/schemas/AnalyticsAggregateCounts'
        '400':
          description: Invalid date range, or both agent_id and campaignId were supplied
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AnalyticsApiError'
        '404':
          description: Campaign not found, or agent not found on this account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AnalyticsApiError'
  /analytics/dashboard-ai-insights:
    get:
      summary: Get Dashboard AI Insights
      description: |
        Returns three short written insights about the account's messaging over a
        date range: one win, one thing to watch, and one tip. They are generated
        by the AI from your own message metrics, so they read as sentences you
        can paste into a report rather than as numbers you still have to
        interpret.

        Dates use startDate and endDate (not from and to), matching
        /analytics/dashboard-insights, whose implementation this shares. This
        endpoint is account wide - it takes no campaign or agent scope.
      tags:
        - Analytics
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
        - name: startDate
          in: query
          required: true
          schema:
            type: string
            format: date
          description: Start of the range (YYYY-MM-DD, inclusive).
          example: "2026-05-01"
        - name: endDate
          in: query
          required: true
          schema:
            type: string
            format: date
          description: End of the range (YYYY-MM-DD, inclusive).
          example: "2026-05-31"
      responses:
        '200':
          description: The generated insights
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    type: object
                    description: The generated win / watch / tip strip, exactly as the in-app dashboard renders it.
                    additionalProperties: true
        '400':
          description: Missing startDate or endDate
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AnalyticsApiError'
  /analytics/entity-activity:
    get:
      summary: Get Entity Activity Timeline
      description: |
        Returns a single contact's, deal's or task's activity as one timeline,
        newest first: what happened and when, across messages, appointments,
        notes and status changes. Use it to answer "what has happened with this
        person" without stitching several list endpoints together.

        Returns 404 when the entity does not exist on your account, so other
        accounts' ids stay unenumerable.
      tags:
        - Analytics
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
        - name: entityType
          in: query
          required: true
          schema:
            type: string
            enum: ['contact', 'deal', 'task']
          description: Which kind of record entityId refers to.
        - name: entityId
          in: query
          required: true
          schema:
            type: string
          description: ID of the contact, deal or task whose timeline to return.
      responses:
        '200':
          description: The activity timeline
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    type: object
                    description: The timeline, newest entry first.
                    additionalProperties: true
        '400':
          description: Missing or invalid entityType / entityId
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AnalyticsApiError'
        '404':
          description: No such contact, deal or task on this account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AnalyticsApiError'
  /api-keys:
    get:
      summary: List Scoped API Keys
      security:
        - ApiKeyAuth: []
      description: |
        Lists the scoped API keys on your account, including revoked ones so you can see what was
        withdrawn and when. Only masked previews are returned - a scoped key's value is shown once,
        at creation, and is never retrievable afterwards.

        Owner only. A scoped key cannot list or manage API keys, so a restricted key can never be
        used to mint a wider one.
      tags:
        - API Keys
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
      responses:
        '200':
          description: The account's scoped API keys
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ScopedApiKeyList'
        '401':
          description: Missing or invalid authentication
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiKeyErrorResponse'
        '403':
          description: Caller is not the account owner, or authenticated with a scoped key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiKeyErrorResponse'
    post:
      summary: Create Scoped API Key
      security:
        - ApiKeyAuth: []
      description: |
        Creates a new scoped API key and returns its value ONCE. Store it immediately - it cannot be
        retrieved again.

        Omitting "scopes" gives you the safe default: read-only, limited to the Analytics section of
        the API, your own account only. That is the right key to point a dashboard or a reporting
        tool at: it can read your numbers and it cannot send a message, change bot settings or buy a
        number.

        Owner only.
      tags:
        - API Keys
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ScopedApiKeyCreateRequest'
            example:
              label: "Client dashboard - Acme"
              scopes:
                read_only: true
                tags: ["Analytics"]
                sub_account_ids: []
                rate_limit_per_min: 60
      responses:
        '201':
          description: The created key, with its value returned for the only time
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ScopedApiKeyCreateResponse'
        '400':
          description: Invalid expires_at, or the account has no email to derive a key from
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiKeyErrorResponse'
        '401':
          description: Missing or invalid authentication
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiKeyErrorResponse'
        '403':
          description: Caller is not the account owner, or authenticated with a scoped key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiKeyErrorResponse'
  /api-keys/{id}:
    patch:
      summary: Update Scoped API Key
      security:
        - ApiKeyAuth: []
      description: |
        Updates a scoped key's label, scopes and/or expiry. Scopes are REPLACED wholesale, not
        merged, so narrowing a key cannot silently leave the old wider permissions in place.

        The key value itself never changes. To roll a key, create a new one and revoke the old one.

        Owner only.
      tags:
        - API Keys
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: The key id (not the key itself).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ScopedApiKeyUpdateRequest'
            example:
              scopes:
                read_only: true
                tags: ["Analytics", "Campaigns"]
                sub_account_ids: []
                rate_limit_per_min: 30
      responses:
        '200':
          description: The updated key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ScopedApiKeyUpdateResponse'
        '400':
          description: Nothing to update, or an invalid expires_at
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiKeyErrorResponse'
        '404':
          description: No such key on this account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiKeyErrorResponse'
    delete:
      summary: Revoke Scoped API Key
      security:
        - ApiKeyAuth: []
      description: |
        Revokes a scoped key. Revocation is immediate: the very next request using it is rejected.
        The key stays in your list, marked as revoked, so you keep the record of what existed and
        what it could reach. Revoking an already-revoked key succeeds and changes nothing.

        Owner only.
      tags:
        - API Keys
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: The key id (not the key itself).
      responses:
        '200':
          description: The key is revoked
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ScopedApiKeyRevokeResponse'
        '404':
          description: No such key on this account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiKeyErrorResponse'
  # NOTE FOR INTEGRATION: the /contacts path key already exists in openapi.js with
  # post (create) and get (single lookup by phoneNumber/email). The get operation
  # below REPLACES the existing get (it now documents both the single-lookup mode
  # and the new paginated list mode) and the delete operation is NEW. Merge both
  # into the existing /contacts path entry manually — the splice script dedupes
  # whole path keys, so do not rely on it here. Keep the existing post untouched.
  # NOTE FOR INTEGRATION: the /contacts/{contactId} path key already exists in
  # openapi.js (with the PUT update operation). The get operation below is NEW
  # and must be MANUALLY merged into the existing /contacts/{contactId} path
  # entry — the splice script dedupes whole path keys, so do not rely on it.
  /account-config/byok-sms:
    put:
      summary: Save Own Twilio SMS Credentials
      security:
        - ApiKeyAuth: []
      description: |
        Stores your own Twilio Account SID and Auth Token on the account, so SMS is
        sent from your Twilio account instead of a platform number.

        This only saves the credentials. It does not verify them with Twilio, buy a
        number, or install message webhooks; that happens when the number is connected
        in the app. The Auth Token is stored securely and is never returned by any endpoint.
      tags:
        - Account Config
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ByokSmsCredentialsInput'
            example:
              accountSid: ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
              authToken: your_twilio_auth_token
              phoneNumber: "+31612345678"
      responses:
        '200':
          description: Credentials saved
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  uid:
                    type: string
                    description: The account the credentials were saved on.
                    examples: ["user_abc123"]
              example:
                success: true
                uid: user_abc123
        '400':
          description: The Account SID, Auth Token, or phone number is missing or malformed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                success: false
                error: "accountSid must be a Twilio Account SID (AC...)."
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The caller may not edit integration settings on this account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /account-config/whatsapp-web-settings:
    put:
      summary: Update WhatsApp Web Sending Limits
      security:
        - ApiKeyAuth: []
      description: |
        Updates the WhatsApp Web sending limits on the account: how many new contacts
        may be messaged per day and per month, the pause between messages, the warning
        threshold, and whether the limits are enforced.

        Send only the fields you want to change; anything you leave out keeps its current
        value. Sending no recognised field at all succeeds and reports an empty updated list.
      tags:
        - Account Config
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WhatsAppWebSettingsInput'
            example:
              dailyNewContactLimit: 50
              delayBetweenNewMessages: 45
              enabled: true
      responses:
        '200':
          description: Settings saved
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  uid:
                    type: string
                    examples: ["user_abc123"]
                  updated:
                    type: array
                    description: The settings that were written, by their stored field name. Empty when nothing changed.
                    items:
                      type: string
              example:
                success: true
                uid: user_abc123
                updated:
                  - whatsapp_web_settings.daily_new_contact_limit
                  - whatsapp_web_settings.delay_between_new_messages
                  - whatsapp_web_settings.enabled
        '400':
          description: A value is out of range or of the wrong type
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                success: false
                error: "dailyNewContactLimit must be a non-negative integer."
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The caller may not edit integration settings on this account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /account-config/custom-channel-webhook:
    put:
      summary: Set Custom Channel Webhook URL
      security:
        - ApiKeyAuth: []
      description: |
        Sets the URL that outgoing messages on custom channels are posted to.

        The URL must be https and publicly reachable; addresses on private networks
        and addresses pointing back at this API are refused. Send an empty string to
        remove the webhook again.
      tags:
        - Account Config
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - webhookUrl
              properties:
                webhookUrl:
                  type: string
                  description: The https URL to post outgoing custom-channel messages to. An empty string clears it.
                  examples: ["https://example.com/hooks/dmchamp"]
            example:
              webhookUrl: https://example.com/hooks/dmchamp
      responses:
        '200':
          description: Webhook URL saved or cleared
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  uid:
                    type: string
                    examples: ["user_abc123"]
                  webhookUrl:
                    type: string
                    description: The saved URL, or an empty string when it was cleared.
                    examples: ["https://example.com/hooks/dmchamp"]
              example:
                success: true
                uid: user_abc123
                webhookUrl: https://example.com/hooks/dmchamp
        '400':
          description: The URL is missing, is not a string, or was refused by the safety check
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                success: false
                error: "Webhook URL rejected: only https URLs are allowed"
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The caller may not edit integration settings on this account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /account-config/instagram-handle:
    get:
      summary: Get Connected Instagram Handle
      security:
        - ApiKeyAuth: []
      description: |
        Returns the username of the Instagram business account connected to the
        Facebook page selected on this account. The name is read live from Meta.
      tags:
        - Account Config
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
      responses:
        '200':
          description: The connected Instagram username
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  data:
                    type: string
                    description: The Instagram username, without the at sign.
                    examples: ["dmchamp"]
              example:
                success: true
                data: dmchamp
        '400':
          description: No Instagram business account is connected to the selected page, or the page has no access token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                success: false
                error: No Instagram business account is connected to the selected page
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The caller may not view channel settings on this account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          description: Meta could not be reached, or refused the lookup
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                success: false
                error: Failed to retrieve Instagram handle from Meta API

  /account-config/proxy-locations:
    get:
      summary: List WhatsApp Web Proxy Locations
      security:
        - ApiKeyAuth: []
      description: |
        Lists the countries a WhatsApp Web connection can be routed through, together
        with a recommendation based on where the request came from. Pick the country
        where the phone is normally used, so WhatsApp does not see the session move.

        This endpoint always answers. If the recommendation cannot be worked out, it
        returns a shorter default list and a warning asking you to choose a country yourself.
      tags:
        - Account Config
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
      responses:
        '200':
          description: The available locations and the recommendation
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  locations:
                    type: array
                    items:
                      $ref: '#/components/schemas/ProxyLocation'
                  recommendedLocation:
                    type: string
                    description: Country code we suggest, based on where the request came from.
                    examples: ["nl"]
                  nearestAvailable:
                    type: string
                    description: Closest country that can actually be used right now.
                    examples: ["de"]
                  userDetectedCountry:
                    type: string
                    description: Country the request appeared to come from.
                    examples: ["nl"]
                  warning:
                    type: ['string', 'null']
                    description: Present when the detected country is unavailable or could not be determined.
              example:
                success: true
                locations:
                  - code: nl
                    name: Netherlands
                    flag: "🇳🇱"
                    available: true
                    region: europe
                recommendedLocation: nl
                nearestAvailable: nl
                userDetectedCountry: nl
                warning: null
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The caller may not view channel settings on this account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /account-config/meta-app:
    get:
      summary: Get Own Meta App Settings
      security:
        - ApiKeyAuth: []
      description: |
        Returns the Meta app stored on this account, together with the callback URLs
        and verify token needed to finish the webhook setup inside that app.

        App secrets are never returned. When no own Meta app is stored, the answer is
        simply configured false.
      tags:
        - Account Config
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
      responses:
        '200':
          description: The stored Meta app summary, or configured false
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MetaAppConfigResponse'
              examples:
                configured:
                  summary: An own Meta app is stored
                  value:
                    success: true
                    configured: true
                    app_id: "1234567890123456"
                    config_id: "9876543210987654"
                    verify_token: a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718
                    webhook_urls:
                      instagram: https://api.dmchamp.com/v1/incoming-instagram-message/byo/user_abc123
                      messenger: https://api.dmchamp.com/v1/incoming-messenger-message/byo/user_abc123
                notConfigured:
                  summary: No own Meta app stored
                  value:
                    success: true
                    configured: false
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The caller may not view integration settings on this account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    put:
      summary: Save Own Meta App Settings
      security:
        - ApiKeyAuth: []
      description: |
        Stores your own Meta app, so people connecting Instagram or Messenger see YOUR
        brand on the Facebook consent screen instead of ours.

        The App ID and App Secret are checked against Meta before anything is stored, so
        a wrong pair fails immediately. Secrets are stored encrypted and are never returned.
        The answer contains the callback URLs and the verify token to enter in your app's
        webhook settings; the verify token is created on the first save and stays the same
        afterwards, so an existing subscription keeps working.

        Two credential sets share this setting and a save needs at least one of them: the
        Facebook Login set (app_id, app_secret, config_id) and the Instagram login pair
        (instagram_app_id, instagram_app_secret), which enables the Facebook-free Instagram
        token push lane on its own - an Instagram-only integrator does not need the Facebook
        fields. Each set is sent complete or not at all, and a save always describes the whole
        setting: whichever set you leave out is removed from the stored settings.
      tags:
        - Account Config
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/MetaAppConfigInput'
            examples:
              facebookLane:
                summary: Facebook login for business only
                value:
                  app_id: "1234567890123456"
                  app_secret: your_meta_app_secret
                  config_id: "9876543210987654"
              withInstagramLogin:
                summary: Also enable the Instagram login lane
                value:
                  app_id: "1234567890123456"
                  app_secret: your_meta_app_secret
                  config_id: "9876543210987654"
                  instagram_app_id: "2233445566778899"
                  instagram_app_secret: your_instagram_app_secret
              instagramLoginOnly:
                summary: Instagram login token push lane only (no Facebook fields)
                value:
                  instagram_app_id: "2233445566778899"
                  instagram_app_secret: your_instagram_app_secret
      responses:
        '200':
          description: The Meta app was verified and saved
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MetaAppSaveResponse'
              example:
                success: true
                app_id: "1234567890123456"
                config_id: "9876543210987654"
                verify_token: a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718
                webhook_urls:
                  instagram: https://api.dmchamp.com/v1/incoming-instagram-message/byo/user_abc123
                  messenger: https://api.dmchamp.com/v1/incoming-messenger-message/byo/user_abc123
                setup:
                  - "In your Meta app: add the webhook Callback URLs returned in webhook_urls with the verify_token above - instagram and messenger on the Instagram and Messenger products, instagram_login on the Instagram-Login product."
        '400':
          description: Neither credential set was supplied, a field is malformed, a set is incomplete, or Meta rejected the App ID and App Secret pair
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                success: false
                error: Meta rejected this App ID / App Secret pair. Copy both from your app's basic settings and try again.
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The caller may not edit integration settings on this account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    delete:
      summary: Remove Own Meta App Settings
      security:
        - ApiKeyAuth: []
      description: |
        Removes the own Meta app from this account. New Instagram and Messenger
        connections then use the platform app again.

        Existing connections keep working, but messages will no longer arrive on the
        per-account callback URLs, so remove the webhook subscription in your Meta app
        as well. The call succeeds even when nothing was stored; removed then reports false.
      tags:
        - Account Config
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
      responses:
        '200':
          description: Removal completed
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  removed:
                    type: boolean
                    description: True when an own Meta app was stored and has now been removed.
                    examples: [true]
              example:
                success: true
                removed: true
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The caller may not edit integration settings on this account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /account-config/sub-accounts/{subAccountId}/byok-spending-limit:
    put:
      summary: Set Sub-Account Provider Spending Limit
      security:
        - ApiKeyAuth: []
      description: |
        Sets the monthly spending limit, in US dollars, for a sub-account that uses its
        own provider keys. Send null to remove the limit again.

        Agency accounts only, and only for a sub-account that belongs to the calling agency.
      tags:
        - Account Config
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
        - name: subAccountId
          in: path
          required: true
          schema:
            type: string
          description: The sub-account to set the limit on.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                byokMonthlyLimitUsd:
                  type: ['number', 'null']
                  minimum: 0
                  description: Monthly limit in US dollars. Null removes the limit.
                  examples: [250]
            example:
              byokMonthlyLimitUsd: 250
      responses:
        '200':
          description: Limit saved
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  subAccountId:
                    type: string
                    examples: ["user_sub456"]
                  byokMonthlyLimitUsd:
                    type: ['number', 'null']
                    examples: [250]
              example:
                success: true
                subAccountId: user_sub456
                byokMonthlyLimitUsd: 250
        '400':
          description: The sub-account id is missing, or the limit is negative or not a number
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                success: false
                error: byokMonthlyLimitUsd must be a positive number or null to remove the limit.
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The caller is not an agency account, or the sub-account belongs to another agency
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                success: false
                error: The specified sub-account does not belong to this agency.
        '404':
          description: The sub-account is not linked to any agency
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                success: false
                error: The specified sub-account is not linked to any agency.
  /broadcasts:
    get:
      summary: List Broadcasts
      security:
        - ApiKeyAuth: []
      description: |
        Returns every broadcast on the account, newest first. Pass status to
        return only the broadcasts in that state, for example only the ones
        currently sending. There is no pagination on this endpoint: the whole
        set is returned.
      tags:
        - Broadcasts
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
        - name: status
          in: query
          required: false
          description: Return only broadcasts in this state.
          schema:
            type: string
            enum: ['Draft', 'Pending Approval', 'Scheduled', 'Sending', 'Paused', 'Sent', 'Failed']
      responses:
        '200':
          description: Broadcasts retrieved successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  broadcasts:
                    type: array
                    items:
                      $ref: '#/components/schemas/Broadcast'
    post:
      summary: Create Broadcast
      security:
        - ApiKeyAuth: []
      description: |
        Creates a new broadcast. It always starts as a Draft: status is set by
        the server and any status, id, user_id, or source_campaign_id you send
        is ignored. Everything else you send is stored, so you can create a
        fully configured broadcast in one call or start with a name and channel
        and fill the rest in later with the update endpoint.

        Creating a broadcast never sends anything. Use the launch endpoint when
        it is ready to go out.
      tags:
        - Broadcasts
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
              properties:
                name:
                  type: string
                  description: Broadcast name.
                channel:
                  type: string
                  description: The single channel this broadcast sends on.
                  examples: ["whatsapp"]
                agent_id:
                  type: ['string', 'null']
                  description: AI agent that handles replies. Omit or send null to leave replies to the team inbox.
                list_id:
                  type: ['string', 'null']
                  description: Contact list to send to.
                contacts:
                  type: array
                  description: Explicit contact audience, as contact document paths. An alternative to list_id.
                  items:
                    type: string
                send_to_new_list_members:
                  type: boolean
                  description: Keep sending to contacts added to the list later. Defaults to false.
                whats_app_template:
                  $ref: '#/components/schemas/BroadcastOpenerTemplate'
                opener_media:
                  oneOf:
                    - $ref: '#/components/schemas/BroadcastOpenerMedia'
                    - type: 'null'
                execution_date:
                  type: ['integer', 'null']
                  description: When the send should start, as epoch milliseconds. Leave unset to send as soon as it is launched.
                drip_mode:
                  type: boolean
                  description: Pace the send over time. Defaults to false.
                time_critical:
                  type: boolean
                  description: Skip the automatic pacing applied to large sends.
                follow_up_config:
                  type: object
                  additionalProperties: true
            examples:
              create_broadcast:
                summary: Create a WhatsApp broadcast to a list
                value:
                  name: Spring menu announcement
                  channel: whatsapp
                  list_id: list_abc123
                  whats_app_template:
                    body: "Hi {{first_name}}, our spring menu just went live."
                    category: marketing
                    language: en
      responses:
        '201':
          description: Broadcast created successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  broadcast_id:
                    type: string
                    description: ID of the newly created broadcast.
        '400':
          description: The request body was not a JSON object, or an attachment on it was malformed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /broadcasts/{broadcastId}:
    get:
      summary: Get Broadcast
      security:
        - ApiKeyAuth: []
      description: |
        Returns one broadcast in full, including its audience, opening message,
        schedule, send progress counters, and pause or completion details.
      tags:
        - Broadcasts
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
        - name: broadcastId
          in: path
          required: true
          description: ID of the broadcast to fetch.
          schema:
            type: string
      responses:
        '200':
          description: Broadcast retrieved successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  broadcast:
                    $ref: '#/components/schemas/Broadcast'
        '404':
          description: |
            Broadcast not found. The same response is returned for a broadcast
            that belongs to another account, so an id cannot be probed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    put:
      summary: Update Broadcast
      security:
        - ApiKeyAuth: []
      description: |
        Partially updates a broadcast. Send only the fields you want to change;
        everything else is left alone. Object fields can also be addressed leaf
        by leaf with a dotted key such as whats_app_template.body, which merges
        into the existing object instead of replacing it. The one exception is
        opener_media, which must always be sent as a whole object, or as null to
        remove the attachment.

        Identity and lifecycle fields cannot be changed here: id, user_id, and
        status are ignored, and source_campaign_id is rejected outright. Use
        launch, pause, and resume to move a broadcast through its lifecycle.

        On a broadcast that mirrors a classic campaign, the fields the campaign
        engine owns are rejected, and the definitional fields you change are
        written through to the source campaign and reflected back.
      tags:
        - Broadcasts
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
        - name: broadcastId
          in: path
          required: true
          description: ID of the broadcast to update.
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
              description: Any writable broadcast field. At least one field is required.
            examples:
              rename_and_schedule:
                summary: Rename a draft and schedule it
                value:
                  name: Spring menu announcement (v2)
                  execution_date: 1789000000000
              update_opener_body:
                summary: Change only the opening message text
                value:
                  whats_app_template.body: "Hi {{first_name}}, the spring menu is live today."
      responses:
        '200':
          description: Broadcast updated successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  broadcast_id:
                    type: string
        '400':
          description: |
            No fields to update, an attachment written with a dotted key or in a
            malformed shape, an attempt to set or clear source_campaign_id, or a
            field that is controlled by the source campaign of a mirrored
            broadcast.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Broadcast not found on this account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    delete:
      summary: Delete Broadcast
      security:
        - ApiKeyAuth: []
      description: |
        Deletes a broadcast. Drafts and broadcasts that are paused, sent, or
        failed can always be deleted. A broadcast that is currently sending or
        scheduled must be paused first, so that a live send is never removed out
        from under itself.

        A broadcast that mirrors a classic campaign is deleted by deleting that
        campaign instead. The only exception is a mirror whose source campaign no
        longer exists, which can be deleted directly.
      tags:
        - Broadcasts
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
        - name: broadcastId
          in: path
          required: true
          description: ID of the broadcast to delete.
          schema:
            type: string
      responses:
        '200':
          description: Broadcast deleted successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  broadcast_id:
                    type: string
        '400':
          description: The broadcast is sending or scheduled, or it is a mirror of a campaign that still exists
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Broadcast not found on this account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /broadcasts/{broadcastId}/duplicate:
    post:
      summary: Duplicate Broadcast
      security:
        - ApiKeyAuth: []
      description: |
        Copies a broadcast into a new Draft. The audience, opening message,
        schedule settings, and follow-up settings come along; all send progress,
        batches, queues, safety state, and analytics are reset so the copy
        starts clean.

        Pass to_channel to copy the broadcast onto a different channel, which is
        how one message is sent on more than one channel. An approved WhatsApp
        template is never carried over as approved: copying to another channel
        drops the template, and copying to WhatsApp demotes it so it is
        confirmed again before the copy can launch. Copying onto text messages
        also drops any attachment, since text messages cannot carry one.
        Generated follow-up templates are cleared and regenerated for the copy.
      tags:
        - Broadcasts
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
        - name: broadcastId
          in: path
          required: true
          description: ID of the broadcast to duplicate.
          schema:
            type: string
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                to_channel:
                  type: string
                  description: Channel for the copy. Defaults to the source broadcast's channel.
                  examples: ["whatsapp_web"]
            examples:
              duplicate_to_channel:
                summary: Copy the broadcast onto another channel
                value:
                  to_channel: whatsapp_web
      responses:
        '201':
          description: Broadcast duplicated successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  broadcast_id:
                    type: string
                    description: ID of the new copy.
                  source_broadcast_id:
                    type: string
                    description: ID of the broadcast that was copied.
        '400':
          description: to_channel was sent but was not a non-empty string
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Broadcast not found on this account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /broadcasts/{broadcastId}/launch:
    post:
      summary: Launch Broadcast
      security:
        - ApiKeyAuth: []
      description: |
        Starts the broadcast. Everything is checked first, and the call either
        launches or fails with a message explaining exactly what to fix. The
        checks are: the broadcast is still a draft (or waiting on template
        approval) and has nothing already armed; the audience is not empty; an
        opening message is set; any attachment is supported on the channel and,
        on WhatsApp, is the one inside the approved template; the WhatsApp
        template is approved; the channel has a connected sender; and, for
        accounts on their own WhatsApp Business Account, that Meta has not
        blocked business-initiated conversations for it.

        On success the broadcast becomes Scheduled when execution_date is in the
        future, or Sending otherwise. A WhatsApp broadcast whose template has
        been submitted but not yet decided becomes Pending Approval instead: no
        send is armed, and it starts by itself once the template is approved.

        A broadcast that mirrors a classic campaign cannot be launched here; use
        the campaign it came from.
      tags:
        - Broadcasts
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
        - name: broadcastId
          in: path
          required: true
          description: ID of the broadcast to launch.
          schema:
            type: string
      responses:
        '200':
          description: Broadcast launched
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  broadcast_id:
                    type: string
                  status:
                    type: string
                    enum: ['Scheduled', 'Sending', 'Pending Approval']
                    description: The state the broadcast moved into.
        '400':
          description: |
            The broadcast is not launchable. The error message names the reason:
            empty audience, missing opening message, attachment not supported on
            this channel, attachment does not match the approved template,
            template not approved or rejected or not registered on WhatsApp,
            channel or sender not connected, WhatsApp Business Account blocked
            for payment, a send already armed, still awaiting approval, the
            broadcast is not a draft, or it is a mirror of a classic campaign.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Broadcast not found on this account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /broadcasts/{broadcastId}/pause:
    post:
      summary: Pause Broadcast
      security:
        - ApiKeyAuth: []
      description: |
        Pauses a broadcast that is sending or scheduled, and tears down whatever
        was armed to send so nothing goes out while it is paused. A broadcast
        that is only waiting on template approval reverts to Draft instead,
        because nothing has been armed for it yet.

        On a broadcast that mirrors a classic campaign, this pauses the source
        campaign, which is what actually stops the send.
      tags:
        - Broadcasts
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
        - name: broadcastId
          in: path
          required: true
          description: ID of the broadcast to pause.
          schema:
            type: string
      responses:
        '200':
          description: Broadcast paused
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  broadcast_id:
                    type: string
        '400':
          description: The broadcast is not sending, scheduled, or awaiting approval
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Broadcast not found on this account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /broadcasts/{broadcastId}/resume:
    post:
      summary: Resume Broadcast
      security:
        - ApiKeyAuth: []
      description: |
        Resumes a paused broadcast. It goes back to Scheduled when
        execution_date is still in the future and to Sending otherwise, and the
        pause reason, pause details, and any safety verdict are cleared.
        Resuming also acknowledges a pause that came from the send safety
        checks, so the very next batch does not stop it again on the same
        evidence.

        Resuming does not clear a low reply rate. If the broadcast was paused
        for low engagement, waive that with the override endpoint instead, or it
        will be paused again at the next check.

        On a broadcast that mirrors a classic campaign, this resumes the source
        campaign, which must itself be paused.
      tags:
        - Broadcasts
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
        - name: broadcastId
          in: path
          required: true
          description: ID of the broadcast to resume.
          schema:
            type: string
      responses:
        '200':
          description: |
            Broadcast resumed. The new state is not included in the response;
            fetch the broadcast to read it.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  broadcast_id:
                    type: string
        '400':
          description: The broadcast is not paused
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Broadcast not found on this account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /broadcasts/{broadcastId}/override-engagement-guard:
    post:
      summary: Override Broadcast Engagement Guard
      security:
        - ApiKeyAuth: []
      description: |
        Waives the low-engagement safety pause for this one broadcast.

        While a broadcast sends, its reply rate is measured batch by batch. A
        rate that stays very low usually means recipients are ignoring or
        reporting the messages, which damages the sending number's reputation,
        so the send is paused and the owner is emailed. That judgement can be
        wrong for a list the owner knows, and the number is theirs, which is why
        this endpoint exists.

        Calling it records the decision to keep sending and, when the broadcast
        is currently paused for low engagement, resumes it in the same call.
        Only a low-engagement pause is lifted this way: a manual pause or a pause
        caused by errors or limits is left alone, because this waiver says
        nothing about those. It is safe to call on a running broadcast, where it
        arms the waiver for future checks, and safe to repeat.

        The waiver applies to this broadcast only. It is not an account setting.
      tags:
        - Broadcasts
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
        - name: broadcastId
          in: path
          required: true
          description: ID of the broadcast to waive the guard for.
          schema:
            type: string
      responses:
        '200':
          description: Waiver recorded
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  broadcast_id:
                    type: string
                  status:
                    type: string
                    description: The broadcast's state after the call. Unchanged unless the call also lifted a low-engagement pause.
                  resumed:
                    type: boolean
                    description: True when this call also lifted an existing low-engagement pause.
        '404':
          description: Broadcast not found on this account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /broadcasts/{broadcastId}/estimate-cost:
    post:
      summary: Estimate Broadcast Cost
      security:
        - ApiKeyAuth: []
      description: |
        Estimates what the broadcast will cost before it is launched. Nothing is
        sent and nothing is charged.

        The shape of data depends on the channel. A WhatsApp broadcast is priced
        per destination country from the audience's phone numbers and the
        template category, including any agency markup on the account. A
        text-message broadcast is priced from live Twilio rates using the
        account's own Twilio credentials, after working out how many message
        segments the opening message takes; without those credentials only the
        segment counts come back. Other channels have no per-message price model
        and are rejected.

        Read billing_mode before showing a number. On the lanes where Meta bills
        the account's own WhatsApp Business Account, the credit figures come back
        null rather than zero, because zero would read as free.

        The audience must be a saved contact list: a broadcast whose audience is
        an explicit set of contacts cannot be estimated.
      tags:
        - Broadcasts
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
        - name: broadcastId
          in: path
          required: true
          description: ID of the broadcast to estimate.
          schema:
            type: string
      responses:
        '200':
          description: Estimate computed
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  channel:
                    type: string
                    description: The broadcast's channel, which decides the shape of data.
                    examples: ["whatsapp"]
                  billing_mode:
                    type: string
                    description: Who pays for this send, lifted out of data so it can be read without unwrapping.
                    examples: ["credits"]
                  data:
                    oneOf:
                      - $ref: '#/components/schemas/BroadcastWhatsAppCostEstimate'
                      - $ref: '#/components/schemas/BroadcastSmsCostEstimate'
        '400':
          description: The broadcast has no contact list assigned, or its channel has no cost estimate
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Broadcast not found on this account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /broadcasts/{broadcastId}/template:
    post:
      summary: Submit Broadcast Template
      security:
        - ApiKeyAuth: []
      description: |
        Sets the broadcast's opening message and, on WhatsApp Business, submits
        it for approval. Placeholders may be written in single or square
        brackets and are normalised to double curly braces. The body is limited
        to 1024 characters.

        When variables is omitted, the placeholder names are read out of the
        body and stored in the order they appear, because the send path fills
        each contact's values in exactly that order.

        A resubmission is refused while a previous submission is still awaiting
        a decision. Resubmitting over an approved template keeps the approved one
        live until the new one is decided.

        On channels that do not use approved templates, and on WhatsApp accounts
        with no provider credentials connected, the message is simply stored and
        marked approved without any provider submission; the real gate in that
        case is the launch check, which requires a connected sender.

        If the broadcast has an attachment, it is submitted as part of the
        template, since on WhatsApp the media is approved together with the
        message. Attachments are not supported yet on WhatsApp numbers connected
        directly through Meta.
      tags:
        - Broadcasts
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
        - name: broadcastId
          in: path
          required: true
          description: ID of the broadcast whose opening message is being submitted.
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [body]
              properties:
                body:
                  type: string
                  maxLength: 1024
                  description: The opening message text, with placeholders in double curly braces.
                  examples: ["Hi {{first_name}}, our spring menu just went live."]
                name:
                  type: string
                  description: Template name. Defaults to the previous template's name, then to the broadcast's name.
                language:
                  type: string
                  description: Template language code. Defaults to the previous template's language, then to en.
                  examples: ["en"]
                category:
                  type: string
                  enum: [marketing, utility, authentication]
                  description: Template category. Defaults to the previous template's category, then to marketing.
                variables:
                  type: array
                  description: Placeholder names in the order they appear. Derived from the body when omitted.
                  items:
                    type: string
            examples:
              submit_template:
                summary: Submit an opening message for approval
                value:
                  body: "Hi {{first_name}}, our spring menu just went live. Want the highlights?"
                  category: marketing
                  language: en
      responses:
        '200':
          description: Opening message stored, and submitted for approval where the channel requires it
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  broadcast_id:
                    type: string
                  template_status:
                    type: string
                    description: Approval status right after submission.
                    examples: ["pending"]
                  template_sid:
                    type: ['string', 'null']
                    description: Provider content id, or null when the message was stored without a provider submission.
        '400':
          description: |
            body was missing or empty, the body was longer than 1024
            characters, a previous submission is still awaiting a decision, or
            the attachment cannot be submitted on this WhatsApp connection.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Broadcast not found on this account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /broadcasts/{broadcastId}/template/select:
    post:
      summary: Select Broadcast Template
      security:
        - ApiKeyAuth: []
      description: |
        Uses a template that is already approved in the account's template
        library as the broadcast's opening message, skipping the submit and wait
        cycle entirely. Only the template id is sent: the approval status and the
        provider content id are read from the library, never taken from the
        request.

        The broadcast must still be a draft, and it must be on WhatsApp or have
        no channel chosen yet. The template must be approved, must be a general
        template rather than a follow-up one, and must carry a provider content
        id. Library templates are text-only, so any attachment on the broadcast
        has to be removed first.

        The response echoes the copied message so an editor can be populated
        without a second request.
      tags:
        - Broadcasts
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
        - name: broadcastId
          in: path
          required: true
          description: ID of the broadcast to set the opening message on.
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [template_id]
              properties:
                template_id:
                  type: string
                  description: ID of an approved template in the account's template library.
            examples:
              select_template:
                summary: Reuse an approved library template
                value:
                  template_id: tmpl_abc123
      responses:
        '200':
          description: Approved template copied onto the broadcast
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  broadcast_id:
                    type: string
                  template_status:
                    type: string
                    examples: ["approved"]
                  template_sid:
                    type: ['string', 'null']
                    description: Provider content id copied from the library template.
                  body:
                    type: string
                    description: The copied message text.
                  name:
                    type: ['string', 'null']
                  language:
                    type: ['string', 'null']
                  variables:
                    type: ['array', 'null']
                    items:
                      type: string
                  category:
                    type: string
                    examples: ["marketing"]
        '400':
          description: |
            template_id was missing, the broadcast is not a draft, the broadcast
            is on a channel that does not use approved templates, an attachment
            is still on the broadcast, or the template is not approved, is a
            follow-up template, or has no provider content id.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: |
            The broadcast was not found on this account, or the template id does
            not exist in the account's template library. The error message says
            which.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  # NOTE FOR INTEGRATION: the /contacts path key already exists in openapi.js with
  # post (create) and get (single lookup by phoneNumber/email). The get operation
  # below REPLACES the existing get (it now documents both the single-lookup mode
  # and the new paginated list mode) and the delete operation is NEW. Merge both
  # into the existing /contacts path entry manually — the splice script dedupes
  # whole path keys, so do not rely on it here. Keep the existing post untouched.
  # NOTE FOR INTEGRATION: the /contacts/{contactId} path key already exists in
  # openapi.js (with the PUT update operation). The get operation below is NEW
  # and must be MANUALLY merged into the existing /contacts/{contactId} path
  # entry — the splice script dedupes whole path keys, so do not rely on it.
  /deals:
    post:
      summary: Create Deal
      security:
        - ApiKeyAuth: []
      description: |
        Create a deal and place it at the TOP of its stage. Both a title and a stage are required,
        and the stage must be one of the stages configured on your account's pipeline.
        The rest of the pipeline is left untouched: the new card gets a position above the current
        first card instead of pushing every other card down.
      tags:
        - Deals
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - $ref: '#/components/schemas/DealFields'
                - type: object
                  required:
                    - title
                    - stage
            examples:
              minimal:
                summary: Create a deal in the first column
                value:
                  title: "Acme website rebuild"
                  stage: "new_lead"
              full:
                summary: Create a fully specified deal
                value:
                  title: "Acme website rebuild"
                  stage: "proposal_sent"
                  value: 4500
                  company: "Acme BV"
                  priority: "high"
                  close_date: "2026-07-01T09:00:00Z"
                  contact_id: "contact123"
      responses:
        '201':
          description: Deal created
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  deal_id:
                    type: string
                    description: ID of the newly created deal.
        '400':
          description: Missing title or stage, a value that is not a number, or a stage that is not one of your configured pipeline stages
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /deals/reorder:
    post:
      summary: Reorder Deals In Stage
      security:
        - ApiKeyAuth: []
      description: |
        Set the exact order of the deals in one stage (column) by sending the full list of deal IDs
        in the order you want them. The first ID gets position 0, the second position 1, and so on.
        Every ID must exist on your account, so send a fresh list rather than a stale one.
        Up to 1000 IDs per request.
      tags:
        - Deals
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - stage_id
                - ordered_deal_ids
              properties:
                stage_id:
                  type: string
                  description: ID of the stage (column) whose deals are being reordered.
                ordered_deal_ids:
                  type: array
                  items:
                    type: string
                  minItems: 1
                  maxItems: 1000
                  description: All deal IDs in the stage, in the desired top-to-bottom order.
            examples:
              reorder:
                summary: Reorder three deals in a column
                value:
                  stage_id: "proposal_sent"
                  ordered_deal_ids: ["deal2", "deal1", "deal3"]
      responses:
        '200':
          description: Deals reordered
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  stage_id:
                    type: string
                    description: ID of the reordered stage.
        '400':
          description: Missing stage_id, an empty ordered_deal_ids list, entries that are not strings, or more than 1000 IDs
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: One of the deal IDs does not exist on your account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /deals/{dealId}:
    put:
      summary: Update Deal
      security:
        - ApiKeyAuth: []
      description: |
        Update one or more fields on a deal. Only the fields you send are changed, and at least one
        field must be sent. Send contact_id as null to unlink the contact. A stage sent here is
        checked against your configured pipeline stages, and moving a deal with this endpoint does
        not renumber the other cards in the column - use the move endpoint for that.
      tags:
        - Deals
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
        - name: dealId
          in: path
          required: true
          description: ID of the deal to update.
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DealFields'
            examples:
              update:
                summary: Raise the value and set a close date
                value:
                  value: 5200
                  close_date: "2026-07-15T10:00:00Z"
      responses:
        '200':
          description: Deal updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  deal_id:
                    type: string
                    description: ID of the updated deal.
        '400':
          description: Empty request body, a value that is not a number, or a stage that is not one of your configured pipeline stages
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Deal not found on this account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    delete:
      summary: Delete Deal
      security:
        - ApiKeyAuth: []
      description: Permanently delete a deal. This cannot be undone.
      tags:
        - Deals
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
        - name: dealId
          in: path
          required: true
          description: ID of the deal to delete.
          schema:
            type: string
      responses:
        '200':
          description: Deal deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
        '404':
          description: Deal not found on this account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /deals/{dealId}/move:
    post:
      summary: Move Deal
      security:
        - ApiKeyAuth: []
      description: |
        Move a deal to another stage (column) and to a specific position inside it, exactly like
        dragging the card on the board. Deals in the source and destination columns are renumbered
        automatically so positions stay in order. Use the deal's current stage to reposition it
        within the same column.
      tags:
        - Deals
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
        - name: dealId
          in: path
          required: true
          description: ID of the deal to move.
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - new_stage_id
                - new_position
              properties:
                new_stage_id:
                  type: string
                  description: ID of the destination stage (column). Must be one of your configured pipeline stages.
                new_position:
                  type: integer
                  minimum: 0
                  description: Zero-based target position inside the destination stage. 0 places the deal at the top.
            examples:
              move:
                summary: Move a deal to the top of Proposal Sent
                value:
                  new_stage_id: "proposal_sent"
                  new_position: 0
      responses:
        '200':
          description: Deal moved
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  deal_id:
                    type: string
                    description: ID of the moved deal.
                  stage:
                    type: string
                    description: The stage the deal is now in.
                  position:
                    type: integer
                    description: The position the deal now occupies.
        '400':
          description: Missing new_stage_id, a new_position that is not a non-negative integer, or a stage that is not one of your configured pipeline stages
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Deal not found on this account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /appointments/available-slots:
    get:
      summary: Get Available Appointment Slots
      security:
        - ApiKeyAuth: []
      description: |
        Computes the open booking windows for one event type between two dates,
        grouped by day and room. Mirrors the room assignment logic used at booking
        time (pinned/preferred room, round-robin, per-room capacity), so a window
        returned here is exactly what booking that same window would honor.
      tags:
        - Appointments
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
        - name: event_id
          in: query
          required: true
          description: The event type to compute availability for.
          schema:
            type: string
        - name: start_time
          in: query
          required: true
          description: Start of the window to search, ISO 8601.
          schema:
            type: string
          example: "2026-06-15T00:00:00.000Z"
        - name: end_time
          in: query
          required: true
          description: End of the window to search, ISO 8601. The search runs through the end of this day.
          schema:
            type: string
          example: "2026-06-20T00:00:00.000Z"
      responses:
        '200':
          description: Available slots computed successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/AvailableSlotDay'
        '400':
          description: Missing required query parameters
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["Missing required query parameters: event_id, start_time and end_time are required"]
        '404':
          description: |
            Event not found. The same response is returned for an event that
            belongs to another account, so an id cannot be probed.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["Event not found"]
  /appointments/google-calendars:
    get:
      summary: List Google Calendars
      security:
        - ApiKeyAuth: []
      description: |
        Lists the calendars on the account's connected Google Calendar account.
        Requires a completed Google Calendar OAuth connection with the calendar
        read-only scope granted; use this to let the caller pick which calendar an
        event type should sync appointments to.
      tags:
        - Appointments
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
      responses:
        '200':
          description: Calendars retrieved successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/GoogleCalendarListEntry'
        '400':
          description: Google Calendar is not connected for this account, or the required scope was not granted
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["Google Calendar is not connected for this account. Please connect Google Calendar in your settings."]
  /appointments/import-calendar-events:
    post:
      summary: Import Calendar Events
      security:
        - ApiKeyAuth: []
      description: |
        Enqueues a background sync that reads a connected Google Calendar and
        creates matching contacts and appointments from its events. Long-running
        (per-event AI extraction plus Google Calendar API calls), so this call only
        queues the job and returns immediately; poll its status with Get Calendar
        Import Job.

        Exactly one of campaign_id or agent_id is required — the campaign or agent
        whose booking configuration and Google Calendar connection the import
        reads. Sending both, or neither, is rejected. identifier controls how
        calendar attendees are matched to contacts.
      tags:
        - Appointments
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - identifier
              properties:
                campaign_id:
                  type: string
                  description: Campaign whose calendar connection to import from. Exactly one of campaign_id or agent_id is required.
                agent_id:
                  type: string
                  description: AI agent whose calendar connection to import from. Exactly one of campaign_id or agent_id is required.
                identifier:
                  type: string
                  enum: [EMAIL, PHONE_NUMBER]
                  description: Which attendee field to match calendar events to existing contacts by.
            examples:
              import_by_agent:
                summary: Import events linked to an agent, matching by email
                value:
                  agent_id: agent_abc123
                  identifier: EMAIL
      responses:
        '202':
          description: Import job queued
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  job_id:
                    type: string
                    description: Id to poll with Get Calendar Import Job.
                  status:
                    type: string
                    examples: ["queued"]
                  campaign_id:
                    type: ['string', 'null']
                    description: Echoes the resolved campaign id, or null when the import was keyed by agent_id.
                  agent_id:
                    type: ['string', 'null']
                    description: Echoes the resolved agent id, or null when the import was keyed by campaign_id.
        '400':
          description: |
            Neither or both of campaign_id/agent_id were supplied, or identifier was
            not EMAIL or PHONE_NUMBER.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: |
            The named campaign or agent was not found. The same response is
            returned for one belonging to another account, so an id cannot be
            probed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /appointments/import-calendar-events/{jobId}:
    get:
      summary: Get Calendar Import Job
      security:
        - ApiKeyAuth: []
      description: Polls the status of a calendar-events import job started by Import Calendar Events.
      tags:
        - Appointments
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
        - name: jobId
          in: path
          required: true
          description: Id returned by Import Calendar Events.
          schema:
            type: string
      responses:
        '200':
          description: Job status retrieved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CalendarImportJob'
        '404':
          description: Import job not found on this account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /appointments/zenchef-restaurants/check:
    post:
      summary: Check Zenchef Restaurant Exists
      security:
        - ApiKeyAuth: []
      description: |
        First step of connecting a Zenchef restaurant: checks (without revealing
        its name) whether a Zenchef restaurant id resolves. Follow with Verify
        Zenchef Restaurant Name before storing the restaurant. Rate-limited to 10
        checks per 5 minutes per account.
      tags:
        - Appointments
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - restaurant_id
              properties:
                restaurant_id:
                  type: string
                  description: The Zenchef restaurant id to check.
      responses:
        '200':
          description: Existence check completed. Success is true whether or not the id resolves; check data.exists.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  data:
                    type: object
                    properties:
                      exists:
                        type: boolean
                      requiresNameVerification:
                        type: boolean
                        description: True when exists is true — call Verify Zenchef Restaurant Name next.
        '400':
          description: Missing restaurant_id
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '429':
          description: Too many verification attempts. Retry after 5 minutes.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /appointments/zenchef-restaurants/verify-name:
    post:
      summary: Verify Zenchef Restaurant Name
      security:
        - ApiKeyAuth: []
      description: |
        Second step of connecting a Zenchef restaurant: verifies that
        user_input_name matches the restaurant's actual name on Zenchef, proving
        the caller actually knows the restaurant before it can be stored. On a
        match, the full restaurant details are returned so they can be passed
        straight to Add Zenchef Restaurant. Rate-limited to 3 attempts per 5
        minutes per account.
      tags:
        - Appointments
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - restaurant_id
                - user_input_name
              properties:
                restaurant_id:
                  type: string
                  description: The Zenchef restaurant id, already confirmed to exist.
                user_input_name:
                  type: string
                  description: The name the caller claims the restaurant has.
      responses:
        '200':
          description: Verification completed. Success is true whether or not the name matched; check data.verified.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  data:
                    type: object
                    properties:
                      verified:
                        type: boolean
                      restaurantDetails:
                        $ref: '#/components/schemas/ZenchefRestaurantDetails'
        '400':
          description: Missing restaurant_id or user_input_name
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Restaurant not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '429':
          description: Too many name-verification attempts. Retry after 5 minutes.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /appointments/zenchef-restaurants:
    post:
      summary: Add Zenchef Restaurant
      security:
        - ApiKeyAuth: []
      description: |
        Persists a Zenchef restaurant to the account after it has passed Check
        Zenchef Restaurant Exists and Verify Zenchef Restaurant Name. Stored
        restaurants become active immediately and can be referenced from an event
        type's booking configuration.
      tags:
        - Appointments
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - restaurant_id
                - restaurant_name
              properties:
                restaurant_id:
                  type: string
                  pattern: '^[A-Za-z0-9_-]{1,64}$'
                  description: The verified Zenchef restaurant id.
                restaurant_name:
                  type: string
                  description: Display name to store for the restaurant.
      responses:
        '201':
          description: Restaurant added successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  data:
                    type: object
                    properties:
                      restaurantId:
                        type: string
        '400':
          description: restaurant_id was not a 1-64 character alphanumeric/underscore/hyphen string, or restaurant_name was missing or empty
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /appointments/zenchef-restaurants/{restaurantId}:
    put:
      summary: Update Zenchef Restaurant
      security:
        - ApiKeyAuth: []
      description: Updates the display name and/or active status of a Zenchef restaurant already stored on the account. Send only the fields you want to change.
      tags:
        - Appointments
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
        - name: restaurantId
          in: path
          required: true
          description: Id of the stored Zenchef restaurant to update.
          schema:
            type: string
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                restaurant_name:
                  type: string
                  description: New display name. Ignored if empty.
                is_active:
                  type: boolean
                  description: Whether the restaurant is active and selectable in booking configuration.
      responses:
        '200':
          description: Restaurant updated successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  data:
                    type: object
                    properties:
                      restaurantId:
                        type: string
        '400':
          description: restaurantId in the URL was not a 1-64 character alphanumeric/underscore/hyphen string
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: No Zenchef restaurant with this id is stored on the account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    delete:
      summary: Delete Zenchef Restaurant
      security:
        - ApiKeyAuth: []
      description: Removes a Zenchef restaurant from the account. Any event type still referencing it stops offering Zenchef booking for that restaurant.
      tags:
        - Appointments
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
        - name: restaurantId
          in: path
          required: true
          description: Id of the stored Zenchef restaurant to delete.
          schema:
            type: string
      responses:
        '200':
          description: Restaurant deleted successfully. Deleting an id that was never stored also returns success.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  data:
                    type: object
                    properties:
                      restaurantId:
                        type: string
        '400':
          description: restaurantId in the URL was not a 1-64 character alphanumeric/underscore/hyphen string
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /appointments/formitable-restaurants/verify:
    post:
      summary: Verify Formitable Restaurant
      security:
        - ApiKeyAuth: []
      description: |
        Verifies a Formitable restaurant id by probing its public products API for
        the next day. Formitable has no private lookup API, so this single call is
        the whole verification step — a successful probe means the id is usable,
        whether or not the restaurant currently has bookable products. Rate-limited
        to 10 attempts per 5 minutes per account.
      tags:
        - Appointments
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - restaurant_id
              properties:
                restaurant_id:
                  type: string
                  description: The Formitable restaurant id to verify.
                language:
                  type: string
                  description: Language tag used for the probe request.
                  default: nl
                  examples: ["nl"]
      responses:
        '200':
          description: Verification completed
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  data:
                    type: object
                    properties:
                      verified:
                        type: boolean
                      restaurantDetails:
                        $ref: '#/components/schemas/FormitableVerifiedRestaurantDetails'
        '400':
          description: Missing restaurant_id
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Restaurant not found. Check the restaurant id and try again.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '429':
          description: Too many verification attempts. Retry after 5 minutes.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /appointments/formitable-restaurants/{restaurantId}/details:
    get:
      summary: Get Formitable Restaurant Details
      security:
        - ApiKeyAuth: []
      description: |
        Fetches a Formitable restaurant's public profile, including its website
        URL. Used during restaurant setup to cache the website URL onto the stored
        restaurant, since Formitable's products API alone does not carry it.
      tags:
        - Appointments
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
        - name: restaurantId
          in: path
          required: true
          description: The Formitable restaurant id.
          schema:
            type: string
        - name: language
          in: query
          required: false
          description: Language tag for the details request.
          schema:
            type: string
            default: en
      responses:
        '200':
          description: Restaurant details retrieved successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  data:
                    $ref: '#/components/schemas/FormitableRestaurantDetails'
        '404':
          description: Restaurant not found. Check the restaurant id and try again.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /appointments/formitable-restaurants:
    post:
      summary: Add Formitable Restaurant
      security:
        - ApiKeyAuth: []
      description: |
        Persists a Formitable restaurant to the account after it has passed Verify
        Formitable Restaurant. Stored restaurants become active immediately and can
        be referenced from an event type's booking configuration.
      tags:
        - Appointments
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - restaurant_id
                - restaurant_name
                - language
              properties:
                restaurant_id:
                  type: string
                  pattern: '^[A-Za-z0-9_-]{1,64}$'
                  description: The verified Formitable restaurant id.
                restaurant_name:
                  type: string
                  description: Display name to store for the restaurant.
                language:
                  type: string
                  description: ISO language tag, e.g. en or en-GB.
                  examples: ["en"]
                website_url:
                  type: string
                  description: Restaurant website URL, http(s) only. Silently dropped if not a valid http(s) URL.
                  examples: ["https://restaurant.example.com"]
      responses:
        '201':
          description: Restaurant added successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  data:
                    type: object
                    properties:
                      restaurantId:
                        type: string
        '400':
          description: restaurant_id, restaurant_name or language was missing or invalid
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /appointments/formitable-restaurants/{restaurantId}:
    put:
      summary: Update Formitable Restaurant
      security:
        - ApiKeyAuth: []
      description: Updates a Formitable restaurant already stored on the account. Send only the fields you want to change.
      tags:
        - Appointments
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
        - name: restaurantId
          in: path
          required: true
          description: Id of the stored Formitable restaurant to update.
          schema:
            type: string
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                restaurant_name:
                  type: string
                  description: New display name. Ignored if empty.
                language:
                  type: string
                  description: New ISO language tag.
                is_active:
                  type: boolean
                  description: Whether the restaurant is active and selectable in booking configuration.
                website_url:
                  type: string
                  description: New website URL, http(s) only. Ignored if not a valid http(s) URL.
      responses:
        '200':
          description: Restaurant updated successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  data:
                    type: object
                    properties:
                      restaurantId:
                        type: string
        '400':
          description: restaurantId in the URL was invalid, or language was not a valid ISO language tag
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: No Formitable restaurant with this id is stored on the account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    delete:
      summary: Delete Formitable Restaurant
      security:
        - ApiKeyAuth: []
      description: Removes a Formitable restaurant from the account. Any event type still referencing it stops offering Formitable booking for that restaurant.
      tags:
        - Appointments
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
        - name: restaurantId
          in: path
          required: true
          description: Id of the stored Formitable restaurant to delete.
          schema:
            type: string
      responses:
        '200':
          description: Restaurant deleted successfully. Deleting an id that was never stored also returns success.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  data:
                    type: object
                    properties:
                      restaurantId:
                        type: string
        '400':
          description: restaurantId in the URL was not a 1-64 character alphanumeric/underscore/hyphen string
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /campaigns/limits/campaigns:
    get:
      summary: Check Subscription Campaign Limit
      security:
        - ApiKeyAuth: []
      description: |
        Checks whether the account has reached its subscription's monthly
        campaign-creation limit. Account-level, not campaign-scoped. An agency
        account with an active subscription, or a plan named "AI Unlimited" or
        an Agency plan, is always unlimited. A plan that does not configure a
        monthly cap is also treated as unlimited. Throws (400) once the
        account has reached the limit that plan does configure.
      tags:
        - Campaigns
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
      responses:
        '200':
          description: Limit check passed
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  data:
                    type: string
                    description: Human-readable pass message.
                    examples: ["Campaign creation is within the subscription limit (3/30 this month)"]
        '400':
          description: The account has reached its plan's monthly campaign-creation limit
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error_code:
                    type: integer
                    examples: [400]
                  error:
                    type: string
        '404':
          description: The account has no subscription on record
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error_code:
                    type: integer
                    examples: [404]
                  error:
                    type: string
                    examples: ["Subscription not found"]
  /campaigns/limits/contacts:
    get:
      summary: Check Subscription Contact Limit
      security:
        - ApiKeyAuth: []
      description: |
        Checks whether the account has reached its subscription's contact
        limit. Account-level, not campaign-scoped. Same unlimited-plan
        exceptions as the campaign limit check above.
      tags:
        - Campaigns
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
      responses:
        '200':
          description: Limit check passed
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  data:
                    type: string
                    description: Human-readable pass message.
        '400':
          description: The account has reached its plan's contact limit
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error_code:
                    type: integer
                    examples: [400]
                  error:
                    type: string
        '404':
          description: The account has no subscription on record
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error_code:
                    type: integer
                    examples: [404]
                  error:
                    type: string
                    examples: ["Subscription not found"]
  /campaigns/stats/totals:
    get:
      summary: List Campaign Stat Totals
      security:
        - ApiKeyAuth: []
      description: |
        Returns bulk per-campaign and per-agent sent/replied delivery totals
        for the account, over a trailing window. Backed by the analytics
        mirror; on an account where the mirror isn't reachable this answers
        200 with empty totals rather than failing, so a list page shows its
        pre-existing figures instead of erroring.
      tags:
        - Campaigns
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
        - name: days
          in: query
          required: false
          description: Trailing window size in days (1-365). Defaults to the platform's standard analytics window.
          schema:
            type: integer
            minimum: 1
            maximum: 365
      responses:
        '200':
          description: Totals retrieved successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  byCampaign:
                    type: object
                    description: Keyed by campaign ID.
                    additionalProperties:
                      type: object
                      properties:
                        sent:
                          type: integer
                        replied:
                          type: integer
                  byAgent:
                    type: object
                    description: Keyed by agent ID. Covers agent-native traffic that carries no campaign, in addition to campaign-scoped traffic rolled up by its agent.
                    additionalProperties:
                      type: object
                      properties:
                        sent:
                          type: integer
                        replied:
                          type: integer
                  windowDays:
                    type: integer
        '400':
          description: Invalid days value
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /campaigns/{campaignId}/channels:
    post:
      summary: Set Campaign Channels
      security:
        - ApiKeyAuth: []
      description: |
        Adds or removes one or more channels from the campaign's
        enabled_channels list, server-side. Accepts either a single toggle
        ({channel, action}) or a batch ({add, remove}) in one call, and
        performs the whole mutation as a single write so any downstream
        channel-ownership-conflict handling fires exactly once. Adding a
        channel that is already enabled, or removing one that isn't, is a
        harmless no-op.
      tags:
        - Campaigns
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
        - name: campaignId
          in: path
          required: true
          description: ID of the campaign
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              oneOf:
                - type: object
                  required: [channel, action]
                  properties:
                    channel:
                      type: string
                      enum: [whatsapp, whatsapp_web, sms, instagram, messenger, facebook, chat_widget, custom_channel, imessage, telegram, instagram_private, line, viber, tiktok, email, linkedin, skool]
                    action:
                      type: string
                      enum: [add, remove]
                - type: object
                  properties:
                    add:
                      type: array
                      items:
                        type: string
                    remove:
                      type: array
                      items:
                        type: string
            examples:
              single_toggle:
                summary: Turn on WhatsApp
                value:
                  channel: whatsapp
                  action: add
              batch_toggle:
                summary: Enable Instagram, disable SMS
                value:
                  add: [instagram]
                  remove: [sms]
      responses:
        '200':
          description: Channels updated successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  campaign_id:
                    type: string
                  added:
                    type: array
                    items:
                      type: string
                  removed:
                    type: array
                    items:
                      type: string
        '400':
          description: Malformed body, or an unrecognized channel value
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Campaign not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /campaigns/{campaignId}/tags:
    post:
      summary: Add Campaign Tag
      security:
        - ApiKeyAuth: []
      description: |
        Appends a tag the AI (or a human) can apply to contacts on this
        campaign. Sending a tag_id resolves (or creates) the owner's backing
        tag document by name; re-posting an identical tag is a harmless
        no-op, and posting the same tag_id with different fields appends a
        second entry (use the update endpoint to replace one in place).
      tags:
        - Campaigns
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
        - name: campaignId
          in: path
          required: true
          description: ID of the campaign
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [tag]
              properties:
                tag:
                  type: object
                  required: [name]
                  properties:
                    tag_id:
                      type: string
                      description: Existing tag document ID to resolve/reuse. Omit to create a new tag by name.
                    name:
                      type: string
                    description:
                      type: string
                    webhook:
                      type: string
                    ai_can_remove:
                      type: boolean
            examples:
              add_tag:
                summary: Tag interested leads
                value:
                  tag:
                    name: Interested
                    description: Apply when the contact asks about pricing or a demo.
                    ai_can_remove: true
      responses:
        '200':
          description: Tag added successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  campaign_id:
                    type: string
                  tag:
                    $ref: '#/components/schemas/CampaignTag'
        '400':
          description: Missing campaignId, or a tag object without a non-empty name
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Campaign not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /campaigns/{campaignId}/tags/{tagId}:
    put:
      summary: Update Campaign Tag
      security:
        - ApiKeyAuth: []
      description: |
        Replaces the campaign tag entry backed by the given tag document ID
        in place. The tagId path parameter is authoritative for which entry
        is replaced — any tag_id in the body is ignored.
      tags:
        - Campaigns
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
        - name: campaignId
          in: path
          required: true
          description: ID of the campaign
          schema:
            type: string
        - name: tagId
          in: path
          required: true
          description: ID of the backing tag document to replace.
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [tag]
              properties:
                tag:
                  type: object
                  required: [name]
                  properties:
                    name:
                      type: string
                    description:
                      type: string
                    webhook:
                      type: string
                    ai_can_remove:
                      type: boolean
      responses:
        '200':
          description: Tag updated successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  campaign_id:
                    type: string
                  tag:
                    $ref: '#/components/schemas/CampaignTag'
        '400':
          description: Missing parameters, or a tag object without a non-empty name
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Campaign not found, or no tag with that ID is present on the campaign
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    delete:
      summary: Remove Campaign Tag
      security:
        - ApiKeyAuth: []
      description: |
        Removes the campaign tag entry backed by the given tag document ID.
      tags:
        - Campaigns
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
        - name: campaignId
          in: path
          required: true
          description: ID of the campaign
          schema:
            type: string
        - name: tagId
          in: path
          required: true
          description: ID of the backing tag document to remove.
          schema:
            type: string
      responses:
        '200':
          description: Tag removed successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  campaign_id:
                    type: string
                  tag_id:
                    type: string
        '400':
          description: Missing campaignId or tagId
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Campaign not found, or no tag with that ID is present on the campaign
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /campaigns/{campaignId}/kb-sources:
    post:
      summary: Link Campaign KB Source
      security:
        - ApiKeyAuth: []
      description: |
        Links a knowledge-base source to a campaign so its content is
        available as FAQ context. Linking an already-linked source is a
        harmless no-op.
      tags:
        - Campaigns
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
        - name: campaignId
          in: path
          required: true
          description: ID of the campaign
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [kb_source_id]
              properties:
                kb_source_id:
                  type: string
                  description: ID of the knowledge-base source to link.
      responses:
        '200':
          description: KB source linked successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  campaign_id:
                    type: string
                  kb_source_id:
                    type: string
        '400':
          description: Missing campaignId or kb_source_id
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Campaign not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /campaigns/{campaignId}/kb-sources/{kbSourceId}:
    delete:
      summary: Unlink Campaign KB Source
      security:
        - ApiKeyAuth: []
      description: |
        Unlinks a knowledge-base source from a campaign. Unlinking a source
        that isn't linked is a harmless no-op.
      tags:
        - Campaigns
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
        - name: campaignId
          in: path
          required: true
          description: ID of the campaign
          schema:
            type: string
        - name: kbSourceId
          in: path
          required: true
          description: ID of the knowledge-base source to unlink.
          schema:
            type: string
      responses:
        '200':
          description: KB source unlinked successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  campaign_id:
                    type: string
                  kb_source_id:
                    type: string
        '400':
          description: Missing campaignId or kbSourceId
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Campaign not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /campaigns/{campaignId}/mcp-servers:
    post:
      summary: Link Campaign MCP Server
      security:
        - ApiKeyAuth: []
      description: |
        Links an MCP server to a campaign so the AI can use its tools during
        conversations. Linking an already-linked server is a harmless no-op.
      tags:
        - Campaigns
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
        - name: campaignId
          in: path
          required: true
          description: ID of the campaign
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [mcp_server_id]
              properties:
                mcp_server_id:
                  type: string
                  description: ID of the MCP server to link.
      responses:
        '200':
          description: MCP server linked successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  campaign_id:
                    type: string
                  mcp_server_id:
                    type: string
        '400':
          description: Missing campaignId or mcp_server_id, or an mcp_server_id containing characters other than letters, numbers, underscores, and hyphens
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Campaign not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /campaigns/{campaignId}/mcp-servers/{mcpServerId}:
    delete:
      summary: Unlink Campaign MCP Server
      security:
        - ApiKeyAuth: []
      description: |
        Unlinks an MCP server from a campaign. Unlinking a server that isn't
        linked is a harmless no-op.
      tags:
        - Campaigns
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
        - name: campaignId
          in: path
          required: true
          description: ID of the campaign
          schema:
            type: string
        - name: mcpServerId
          in: path
          required: true
          description: ID of the MCP server to unlink.
          schema:
            type: string
      responses:
        '200':
          description: MCP server unlinked successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  campaign_id:
                    type: string
                  mcp_server_id:
                    type: string
        '400':
          description: Missing campaignId or mcpServerId, or an mcpServerId containing characters other than letters, numbers, underscores, and hyphens
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Campaign not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /campaigns/{campaignId}/custom-functions/{customFunctionId}:
    delete:
      summary: Unlink Campaign Custom Function
      security:
        - ApiKeyAuth: []
      description: |
        Unlinks a custom function from a campaign. Unlinking a function that
        isn't linked is a harmless no-op. To link a custom function to a
        campaign, POST to /campaigns/{campaignId}/custom-functions with
        {custom_function_id} in the body.
      tags:
        - Campaigns
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
        - name: campaignId
          in: path
          required: true
          description: ID of the campaign
          schema:
            type: string
        - name: customFunctionId
          in: path
          required: true
          description: ID of the custom function to unlink.
          schema:
            type: string
      responses:
        '200':
          description: Custom function unlinked successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  campaign_id:
                    type: string
                  custom_function_id:
                    type: string
        '400':
          description: Missing campaignId or customFunctionId, or a customFunctionId containing characters other than letters, numbers, underscores, and hyphens
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Campaign not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /campaigns/{campaignId}/incoming-routing:
    post:
      summary: Assign Campaign Incoming Routing
      security:
        - ApiKeyAuth: []
      description: |
        Bulk-assigns this campaign as the catch-all for one or more incoming
        channels: each channel's default routing is pointed at this
        campaign's agent (an agent is minted for the campaign if it has
        none). Only "Incoming from Unknown Contacts" or "Combined" campaigns
        can be routed this way, and a campaign with trigger keywords is
        refused outright — keyword campaigns are matched before the
        channel default and can never BE the default.

        Partial failures are reported rather than hidden: channels lists only
        the channels that actually got routed (and is what the campaign's
        enabled_channels list was unioned with); failed lists any requested
        channel whose routing write threw. If every requested channel fails,
        the call itself fails (500) instead of silently returning success
        with nothing routed.
      tags:
        - Campaigns
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
        - name: campaignId
          in: path
          required: true
          description: ID of the campaign
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [channels]
              properties:
                channels:
                  type: array
                  description: Channels to route to this campaign.
                  items:
                    type: string
                    enum: [whatsapp, whatsapp_web, telegram, instagram_private, instagram, messenger, chat_widget, custom_channel, sms, email, imessage, tiktok, linkedin, skool]
            examples:
              route_whatsapp:
                value:
                  channels: [whatsapp, whatsapp_web]
      responses:
        '200':
          description: Routing assigned (possibly with some channels failed)
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  uid:
                    type: string
                  campaignId:
                    type: string
                  channels:
                    type: array
                    description: Channels actually routed to this campaign.
                    items:
                      type: string
                  failed:
                    type: array
                    description: Requested channels whose routing write threw.
                    items:
                      type: string
        '400':
          description: Missing campaignId/channels, an unrecognized channel, or too many channels in one call
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error_code:
                    type: integer
                    examples: [400]
                  error:
                    type: string
        '403':
          description: Campaign belongs to another account
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error_code:
                    type: integer
                    examples: [403]
                  error:
                    type: string
        '404':
          description: Campaign not found
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error_code:
                    type: integer
                    examples: [404]
                  error:
                    type: string
                    examples: ["Campaign not found."]
    delete:
      summary: Clear Campaign Incoming Routing
      security:
        - ApiKeyAuth: []
      description: |
        Stops this campaign's agent answering one (or, when
        channelToUnassign is omitted, all) of the incoming channels it
        currently holds. Only clears a channel's routing when it currently
        points at THIS campaign's agent — a channel another agent answers is
        left alone and simply doesn't appear in channelsRemoved.
      tags:
        - Campaigns
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
        - name: campaignId
          in: path
          required: true
          description: ID of the campaign
          schema:
            type: string
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                channelToUnassign:
                  type: string
                  description: When provided, only this channel's routing is cleared. When omitted, every channel the campaign currently advertises as enabled is cleared.
                  enum: [whatsapp, whatsapp_web, telegram, instagram_private, instagram, messenger, chat_widget, custom_channel, sms, email, imessage, tiktok, linkedin, skool]
      responses:
        '200':
          description: Routing cleared
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  uid:
                    type: string
                  campaignId:
                    type: string
                  channelsRemoved:
                    type: array
                    items:
                      type: string
        '400':
          description: Missing campaignId, or an unrecognized channelToUnassign
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error_code:
                    type: integer
                    examples: [400]
                  error:
                    type: string
        '403':
          description: Campaign belongs to another account
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error_code:
                    type: integer
                    examples: [403]
                  error:
                    type: string
        '404':
          description: Campaign not found
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error_code:
                    type: integer
                    examples: [404]
                  error:
                    type: string
                    examples: ["Campaign not found."]
  /campaigns/{campaignId}/reactivate:
    post:
      summary: Reactivate Campaign
      security:
        - ApiKeyAuth: []
      description: |
        Reactivates a dormant (Ended, Completed, Paused, or Draft)
        incoming-capable campaign. For each channel the campaign advertises,
        this takes over the channel's routing when nobody answers it or when
        this campaign's own agent already does; a channel another agent
        answers is reported as blocked rather than stolen, and in that case
        the campaign is NOT flipped Live — resolve the conflict (for example
        by ending the other campaign) and call again. Calling this on an
        already-Live campaign is a harmless no-op.
      tags:
        - Campaigns
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
        - name: campaignId
          in: path
          required: true
          description: ID of the campaign
          schema:
            type: string
      responses:
        '200':
          description: |
            Request processed. Check data.success: a channel conflict reports
            data.success false with the blocked channels listed, and the
            campaign is left as-is.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  data:
                    type: object
                    properties:
                      success:
                        type: boolean
                        description: Whether the campaign was actually flipped Live.
                      channelsReactivated:
                        type: array
                        items:
                          type: string
                      channelsBlockedByConflict:
                        type: array
                        items:
                          type: string
                      campaignType:
                        type: string
        '400':
          description: Missing campaignId, or the campaign type/status doesn't support reactivation
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error_code:
                    type: integer
                    examples: [400]
                  error:
                    type: string
        '403':
          description: Campaign belongs to another account
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error_code:
                    type: integer
                    examples: [403]
                  error:
                    type: string
        '404':
          description: Campaign not found
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error_code:
                    type: integer
                    examples: [404]
                  error:
                    type: string
                    examples: ["Campaign not found."]
  /campaigns/{campaignId}/stop-incoming:
    post:
      summary: Stop Conflicting Incoming Campaigns
      security:
        - ApiKeyAuth: []
      description: |
        Frees up this campaign's own channels by releasing any of them
        currently answered by a DIFFERENT agent. Used right before launching
        an incoming-capable campaign so its channels are guaranteed free.
        Channels already answered by nobody, or already answered by this
        campaign's own agent, are left untouched. When enabled_channels is
        empty, or none of them are actually held by another agent, this is a
        harmless no-op.
      tags:
        - Campaigns
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
        - name: campaignId
          in: path
          required: true
          description: ID of the campaign
          schema:
            type: string
      responses:
        '200':
          description: Request processed
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  campaign_id:
                    type: string
                  ended_campaign_ids:
                    type: array
                    description: Reserved for a legacy shape; always empty on this endpoint.
                    items:
                      type: string
                  released_channels:
                    type: array
                    description: Channels that were released from another agent.
                    items:
                      type: string
                  cleared_entire_field:
                    type: boolean
                    description: Reserved for a legacy shape; always false on this endpoint.
        '400':
          description: Missing campaignId
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Campaign not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /campaigns/{campaignId}/optimize:
    post:
      summary: Optimize Campaign
      security:
        - ApiKeyAuth: []
      description: |
        Runs an AI optimization pass over the campaign's bot configuration
        using the supplied feedback, and records a version-history diff.
        Requires at least one of user_feedback (from the optimize modal) or
        thumbs_down_feedback (from a thumbs-down on a specific reply, with an
        optional thumbs_down_message giving the offending message). Returns
        202 immediately; the result lands in the campaign's version history.
        A run that was left stranded mid-flight by a previous crash is
        automatically unstuck before the new run starts. Calling this while
        a run is genuinely still in progress returns 409.
      tags:
        - Campaigns
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
        - name: campaignId
          in: path
          required: true
          description: ID of the campaign
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                user_feedback:
                  type: string
                  description: Free-text feedback describing what to improve.
                thumbs_down_feedback:
                  type: string
                  description: Feedback tied to a specific thumbs-down reaction.
                thumbs_down_message:
                  type: string
                  description: The bot message the thumbs-down feedback is about.
            examples:
              direct_feedback:
                value:
                  user_feedback: Reply faster to pricing questions and mention the current promo.
      responses:
        '202':
          description: Optimization run started
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  campaign_id:
                    type: string
        '400':
          description: Missing campaignId, or neither user_feedback nor thumbs_down_feedback was provided
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Campaign not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: An optimization run is already in progress for this campaign
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /campaigns/{campaignId}/insight-runs:
    post:
      summary: Create Campaign Insight Run
      security:
        - ApiKeyAuth: []
      description: |
        Enqueues an AI chat-insights analysis run over this campaign's recent
        conversations. Restricted to a single allowlisted platform operator
        account — every other caller gets 403. Fire-and-forget: this call
        only creates the run and publishes it to the background worker; the
        result is written onto the campaign's insight_runs record as the
        worker progresses, and there is no REST poll endpoint for it.
      tags:
        - Campaigns
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
        - name: campaignId
          in: path
          required: true
          description: ID of the campaign
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [mode]
              properties:
                mode:
                  type: string
                  enum: [insights, optimize]
                source:
                  type: string
                  enum: [daily-summary, recent]
                  description: Defaults to daily-summary.
                count:
                  type: integer
                  description: How many chats to analyze, clamped to a per-mode maximum.
                apply:
                  type: boolean
                  description: Whether an "optimize" run should apply its suggested changes automatically.
      responses:
        '200':
          description: Run enqueued
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  runId:
                    type: string
                  status:
                    type: string
                    examples: ["pending"]
        '400':
          description: Missing campaignId, or mode is not "insights" or "optimize"
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The caller is not the allowlisted operator account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Campaign not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /campaigns/{campaignId}/template-cost-estimate:
    get:
      summary: Estimate Campaign Template Cost
      security:
        - ApiKeyAuth: []
      description: |
        Estimates what launching this campaign's WhatsApp template send will
        cost, broken down by destination country, against the campaign's
        assigned contact list. billing_mode tells you who the fee lands on:
        "credits" (billed from account credits — cost figures are real
        numbers), or "meta_waba_direct" / a Meta Cloud API lane (Meta bills
        the account's own WhatsApp Business Account directly — cost figures
        come back null rather than a made-up number, never 0).
      tags:
        - Campaigns
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
        - name: campaignId
          in: path
          required: true
          description: ID of the campaign
          schema:
            type: string
      responses:
        '200':
          description: Estimate computed
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  billing_mode:
                    type: string
                    enum: [credits, twilio_direct, meta_waba_direct]
                  data:
                    type: object
                    properties:
                      countries:
                        type: array
                        items:
                          type: object
                          properties:
                            countryCode:
                              type: string
                            name:
                              type: string
                            iso:
                              type: string
                            flag:
                              type: string
                            contactCount:
                              type: integer
                            costPerContact:
                              type: ['number', 'null']
                            subtotal:
                              type: ['number', 'null']
                      totalContacts:
                        type: integer
                      totalTemplateCost:
                        type: ['number', 'null']
                      templateCategory:
                        type: string
                      billing_mode:
                        type: string
                      lane:
                        type: string
                      service_messages_billable_soon:
                        type: boolean
        '400':
          description: Campaign has no contact list assigned
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error_code:
                    type: integer
                    examples: [400]
                  error:
                    type: string
        '403':
          description: Campaign belongs to another account
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error_code:
                    type: integer
                    examples: [403]
                  error:
                    type: string
  /campaigns/{campaignId}/sms-cost-estimate:
    get:
      summary: Estimate Campaign SMS Cost
      security:
        - ApiKeyAuth: []
      description: |
        Estimates the SMS cost of launching this campaign, using live Twilio
        pricing for the campaign's assigned contact list. SMS is always
        bring-your-own Twilio, so billing_mode is always "twilio_direct" -
        the account's own Twilio account is charged, not platform credits.
      tags:
        - Campaigns
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
        - name: campaignId
          in: path
          required: true
          description: ID of the campaign
          schema:
            type: string
      responses:
        '200':
          description: Estimate computed
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  billing_mode:
                    type: string
                    examples: ["twilio_direct"]
                  data:
                    type: object
                    properties:
                      totalContacts:
                        type: integer
                      messageLength:
                        type: integer
                      segmentsPerMessage:
                        type: integer
                      totalSegments:
                        type: integer
                      estimatedCostUsd:
                        type: number
                      priceUnit:
                        type: string
                      billedByTwilio:
                        type: boolean
                        examples: [true]
                      billing_mode:
                        type: string
                      lane:
                        type: string
                      service_messages_billable_soon:
                        type: boolean
        '400':
          description: Campaign has no contact list assigned
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error_code:
                    type: integer
                    examples: [400]
                  error:
                    type: string
        '403':
          description: Campaign belongs to another account
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error_code:
                    type: integer
                    examples: [403]
                  error:
                    type: string
  /campaigns/{campaignId}/limits/ai-credit-messaging:
    get:
      summary: Check Campaign AI Credit Messaging Limit
      security:
        - ApiKeyAuth: []
      description: |
        Checks whether launching or scheduling this campaign would exceed the
        account's AI-credit messaging limit. A BYOK account launching on a
        free-messaging channel (WhatsApp Web or Telegram) always passes,
        since neither the AI call nor the transport costs platform credits.
      tags:
        - Campaigns
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
        - name: campaignId
          in: path
          required: true
          description: ID of the campaign
          schema:
            type: string
      responses:
        '200':
          description: Limit check passed
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  data:
                    type: string
                    description: Human-readable pass message.
        '400':
          description: The campaign would exceed the account's AI-credit messaging limit, or has drip batches with none in range
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error_code:
                    type: integer
                    examples: [400]
                  error:
                    type: string
        '404':
          description: Campaign not found or belongs to another account
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error_code:
                    type: integer
                    examples: [404]
                  error:
                    type: string
                    examples: ["Campaign not found or access denied"]
  /campaigns/{campaignId}/limits/messaging:
    get:
      summary: Check Campaign Messaging Limit
      security:
        - ApiKeyAuth: []
      description: |
        Checks whether launching or scheduling this campaign would exceed the
        account's daily messaging limit.
      tags:
        - Campaigns
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
        - name: campaignId
          in: path
          required: true
          description: ID of the campaign
          schema:
            type: string
      responses:
        '200':
          description: Limit check passed
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  data:
                    type: string
                    description: Human-readable pass message.
        '400':
          description: The campaign would exceed the account's daily messaging limit, or has drip batches with none in range
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error_code:
                    type: integer
                    examples: [400]
                  error:
                    type: string
        '404':
          description: Campaign not found or belongs to another account
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error_code:
                    type: integer
                    examples: [404]
                  error:
                    type: string
                    examples: ["Campaign not found or access denied"]
  /campaigns/{campaignId}/template-generation:
    post:
      summary: Start Campaign Template Generation
      security:
        - ApiKeyAuth: []
      description: |
        Kicks off AI generation of the campaign's WhatsApp opening template
        (or its follow-up templates, depending on type). Returns 202
        immediately; generation runs in the background and the result lands
        on the campaign's template_generation_status field, which the client
        polls.
      tags:
        - Campaigns
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
        - name: campaignId
          in: path
          required: true
          description: ID of the campaign
          schema:
            type: string
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                type:
                  type: string
                  description: Which generation job to run. Defaults to the campaign's opening template.
      responses:
        '202':
          description: Generation job started
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  campaign_id:
                    type: string
                  type:
                    type: string
        '400':
          description: Missing campaignId, or an unrecognized type
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Campaign not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /campaigns/{campaignId}/contacts/{contactId}/assign:
    post:
      summary: Assign Contact To Campaign
      security:
        - ApiKeyAuth: []
      description: |
        Manually assigns an existing contact to a Live campaign so the AI
        starts responding to their conversation. The campaign must be Live,
        and the contact's channel must be compatible with one of the
        campaign's enabled channels (a contact on WhatsApp API auto-coerces
        onto a campaign that only enables WhatsApp Web, and vice versa, when
        the campaign enables exactly one WhatsApp variant).
      tags:
        - Campaigns
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
        - name: campaignId
          in: path
          required: true
          description: ID of the campaign
          schema:
            type: string
        - name: contactId
          in: path
          required: true
          description: ID of the contact
          schema:
            type: string
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                triggerAIResponse:
                  type: boolean
                  description: Whether the AI should immediately respond to the contact's most recent message. Defaults to false.
                sendOpeningMessage:
                  type: boolean
                  description: Whether to send the campaign's opening message to the contact. Defaults to false.
      responses:
        '200':
          description: Contact assigned
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  data:
                    type: object
                    properties:
                      campaignId:
                        type: string
                      campaignName:
                        type: string
                      contactId:
                        type: string
                      aiResponseTriggered:
                        type: boolean
                      aiTriggerError:
                        type: string
                      openingMessageSent:
                        type: boolean
                      openingMessageError:
                        type: string
        '400':
          description: Missing campaignId or contactId, or the campaign's status/channel is not compatible with the contact
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error_code:
                    type: integer
                    examples: [400]
                  error:
                    type: string
        '403':
          description: The contact or the campaign belongs to another account
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error_code:
                    type: integer
                    examples: [403]
                  error:
                    type: string
  /contacts/{contactId}/assign:
    post:
      summary: Assign Contact to Team Member
      security:
        - ApiKeyAuth: []
      description: |
        Assigns (or clears) the team member responsible for a contact. Send assigneeUid with the
        member to assign, or null to clear the assignment.

        Distinct from POST /:contactId/department: this answers "which PERSON owns this", the
        department endpoint answers "which TEAM owns this" - the two compose without either
        clearing the other.

        Distinct from POST /:contactId/assign-agent: this assigns a human team member; assign-agent
        hands the conversation to an AI Agent.

        A member scoped to "only assigned" contacts may only reassign a contact currently inside
        their own visibility boundary - reassigning it away from themselves is allowed (that is the
        point of handing a lead over), they simply lose sight of it afterwards.
      tags:
        - Contacts
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
        - name: contactId
          in: path
          required: true
          schema:
            type: string
          description: ID of the contact to assign.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - assigneeUid
              properties:
                assigneeUid:
                  type: ['string', 'null']
                  description: >-
                    UID of the team member to assign, or null to clear. Required - an empty string
                    is rejected.
            examples:
              assign:
                summary: Assign the contact to a team member
                value:
                  assigneeUid: "team_member_abc123"
              clear:
                summary: Clear the assignment
                value:
                  assigneeUid: null
      responses:
        '200':
          description: The contact was assigned
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContactAssignResponse'
        '400':
          description: assigneeUid missing or not a non-empty string or null, or the assignee is not an active team member on this account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContactsApiError'
        '403':
          description: The contact does not belong to the authenticated account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContactsApiError'
        '404':
          description: Contact not found, or outside the caller's visibility scope
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContactsApiError'
  /contacts/{contactId}/profile-pic:
    post:
      summary: Fetch Contact Profile Picture
      security:
        - ApiKeyAuth: []
      description: |
        Fetches a contact's profile picture on demand from the provider (Meta Graph API for
        Instagram/Messenger, or the WhatsApp Web connection service), with a 7-day cache. A contact
        the provider reports has no reachable picture (privacy setting, or upstream 404) is
        negative-cached for 24 hours; a transient upstream failure is negative-cached for 15 minutes.

        Not supported for WhatsApp Business API contacts (Meta does not expose a profile picture for
        that channel) - those return 400.
      tags:
        - Contacts
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
        - name: contactId
          in: path
          required: true
          schema:
            type: string
          description: ID of the contact to fetch a picture for.
      responses:
        '200':
          description: Lookup completed - check avatar_url for the outcome
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContactProfilePicResponse'
              examples:
                found:
                  summary: Picture retrieved (or served from cache)
                  value:
                    success: true
                    avatar_url: "https://scontent.xx.fbcdn.net/v/..."
                    cached: false
                unavailable:
                  summary: No picture could be retrieved
                  value:
                    success: true
                    message: "No profile picture available for this contact"
        '400':
          description: >-
            The contact has no channel id to look up, the channel does not support profile pictures
            (for example WhatsApp Business API), or the contact has no active WhatsApp Web
            connection / no phone number
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContactsApiError'
        '404':
          description: Contact not found on this account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContactsApiError'
  /contacts/{contactId}/linked:
    get:
      summary: List Linked Contacts
      security:
        - ApiKeyAuth: []
      description: |
        Returns the other conversations that are the same person as this contact (same person_id,
        set by POST /:contactId/link-channel). Returns an empty array, never 404, for a contact with
        no linked channels - "this person has no other channels" is a normal state.
      tags:
        - Contacts
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
        - name: contactId
          in: path
          required: true
          schema:
            type: string
          description: ID of the contact whose linked siblings are requested.
      responses:
        '200':
          description: The contact's linked siblings, possibly empty
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/LinkedContactSummary'
        '404':
          description: Contact not found on this account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContactsApiError'
  /contacts/{contactId}/link:
    delete:
      summary: Unlink Contact
      security:
        - ApiKeyAuth: []
      description: |
        Unlinks THIS contact from its person: clears person_id and linked_from on this contact only.
        Deliberately one-sided - any other contact that shared the same person keeps its person_id,
        so unlinking one of three linked contacts does not dissolve the other two.
      tags:
        - Contacts
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
        - name: contactId
          in: path
          required: true
          schema:
            type: string
          description: ID of the contact to unlink.
      responses:
        '200':
          description: The contact was unlinked
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
        '404':
          description: Contact not found on this account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContactsApiError'
  /contacts/auto-tag:
    post:
      summary: Run Auto-Tag with AI
      security:
        - ApiKeyAuth: []
      description: |
        Runs the AI tag rules over one or more contacts' whole conversation and applies or removes
        tags exactly like the realtime evaluator - same rules, same per-applied-tag credit charge.

        Send scope "contacts" with an explicit contact_ids array (max 500), or scope "agent" with
        agent_id to target every conversation currently handled by that agent (most recently active
        first, capped at 500). Ids outside the caller's team visibility boundary are dropped.

        A single contact runs inline and the response is the result immediately. More than one
        contact starts a background run and returns 202 with a run_id - poll GET /contacts/auto-tag/run
        for progress. Only one bulk run may be in progress per account at a time.
      tags:
        - Contacts
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - scope
              properties:
                scope:
                  type: string
                  enum: [contacts, agent]
                contact_ids:
                  type: array
                  description: Required when scope is "contacts". Non-empty, max 500 contact IDs. Deduplicated.
                  items:
                    type: string
                agent_id:
                  type: string
                  description: Required when scope is "agent" (the agent whose conversations are evaluated). Optional override of which agent's tag rules to use when scope is "contacts".
            examples:
              single_contact:
                summary: Evaluate one contact inline
                value:
                  scope: "contacts"
                  contact_ids: ["contact_abc123"]
              agent_scope:
                summary: Evaluate every conversation this agent currently handles
                value:
                  scope: "agent"
                  agent_id: "agent_xyz789"
      responses:
        '200':
          description: A single contact was evaluated inline, or an agent-scope run found no conversations to evaluate
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  result:
                    $ref: '#/components/schemas/AutoTagContactResult'
                  run_id:
                    type: 'null'
                    description: Present (and null) only on the no-conversations-found outcome for scope "agent".
                  total:
                    type: integer
                    examples: [0]
                  message:
                    type: string
                    examples: ["No conversations found for this agent."]
        '202':
          description: A bulk run was started
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  run_id:
                    type: string
                  total:
                    type: integer
                    description: Number of contacts the run will evaluate.
        '400':
          description: Invalid body (bad scope, missing/oversized contact_ids)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContactsApiError'
        '402':
          description: Insufficient credits to tag the single contact
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error_code:
                    type: string
                    examples: ["insufficient_credits"]
                  error:
                    type: string
        '404':
          description: The supplied agent_id does not belong to this account, or no contact was left after applying the caller's visibility scope
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContactsApiError'
        '409':
          description: A bulk auto-tag run is already in progress on this account
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error_code:
                    type: string
                    examples: ["auto_tag_run_in_progress"]
                  error:
                    type: string
  /contacts/auto-tag/run:
    get:
      summary: Get Auto-Tag Run Status
      security:
        - ApiKeyAuth: []
      description: |
        Returns the account's current or most recently finished on-demand auto-tag run, so the UI can
        poll progress after POST /contacts/auto-tag returns a run_id. run is null when the account has
        never started one.
      tags:
        - Contacts
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
      responses:
        '200':
          description: The account's auto-tag run band
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  run:
                    oneOf:
                      - $ref: '#/components/schemas/AutoTagRunStatus'
                      - type: 'null'
  /contacts/lists/batch:
    post:
      summary: Add Contacts To List in Bulk
      security:
        - ApiKeyAuth: []
      description: |
        Adds multiple contacts to a list in one call. Every contact ID must belong to the
        authenticated account and be inside the caller's team visibility boundary - a single
        cross-tenant or out-of-scope ID fails the whole batch with 404 (no partial writes).

        Refused with 409 on a smart list: its membership is computed from rules, so it cannot be
        hand-curated.
      tags:
        - Contacts
        - Lists
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - list_id
                - contact_ids
              properties:
                list_id:
                  type: string
                  description: ID of the list to add every contact to.
                contact_ids:
                  type: array
                  description: Non-empty array of contact IDs. Deduplicated before writing.
                  items:
                    type: string
            examples:
              add_batch:
                summary: Add three contacts to a list
                value:
                  list_id: "list456"
                  contact_ids: ["contact1", "contact2", "contact3"]
      responses:
        '200':
          description: Contacts added to the list
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  added_count:
                    type: integer
                    examples: [3]
                  message:
                    type: string
                    examples: ["3 contact(s) successfully added to list My List"]
        '400':
          description: Missing or invalid list_id / contact_ids
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  message:
                    type: string
        '404':
          description: List not found, or one of the contact IDs does not exist / is out of the caller's scope
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  message:
                    type: string
        '409':
          description: The list is a smart list and cannot be hand-curated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  message:
                    type: string
  # NOTE FOR INTEGRATION: the /contacts path key already exists in openapi.js with
  # post (create) and get (single lookup by phoneNumber/email). The get operation
  # below REPLACES the existing get (it now documents both the single-lookup mode
  # and the new paginated list mode) and the delete operation is NEW. Merge both
  # into the existing /contacts path entry manually — the splice script dedupes
  # whole path keys, so do not rely on it here. Keep the existing post untouched.
  # NOTE FOR INTEGRATION: the /contacts/{contactId} path key already exists in
  # openapi.js (with the PUT update operation). The get operation below is NEW
  # and must be MANUALLY merged into the existing /contacts/{contactId} path
  # entry — the splice script dedupes whole path keys, so do not rely on it.
  /faqs/dedupe:
    post:
      summary: Start FAQ Deduplication
      security:
        - ApiKeyAuth: []
      description: |
        Kicks off a background deduplication pass against the caller's FAQ library and returns
        immediately with a job id. Only one dedup job may run per account at a time - a second call
        while one is queued or processing is refused with 409. Poll the job's progress on the user's
        dedup_job field (surfaced through GET /faqs), and dismiss a finished job with
        POST /faqs/dedupe/dismiss.
      tags:
        - FAQs
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                sourceIds:
                  type: array
                  description: Optional knowledge-base source IDs to scope the dedup to. Omit to run on the full library.
                  items:
                    type: string
      responses:
        '202':
          description: Deduplication job queued
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  job_id:
                    type: string
                    description: ID of the queued job, written to the account's dedup_job field.
        '400':
          description: sourceIds was supplied but was not an array of non-empty strings
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
        '409':
          description: A deduplication job is already queued or processing for this account
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["A FAQ deduplication is already running. Please wait for it to finish."]
  /faqs/dedupe/dismiss:
    post:
      summary: Dismiss FAQ Deduplication Job
      security:
        - ApiKeyAuth: []
      description: |
        Clears the account's dedup_job field so the dedup progress/result indicator disappears.
        Idempotent - returns success whether or not a job existed. Refused with 409 while the job is
        still queued or processing; wait for it to reach a terminal state first.
      tags:
        - FAQs
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
      responses:
        '200':
          description: The job was dismissed (or there was nothing to dismiss)
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
        '409':
          description: The dedup job is still queued or processing
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
                    examples: ["Cannot dismiss a dedup job that is still running."]
  /faqs/similar-for-task:
    post:
      summary: Find Similar FAQs for a Task
      security:
        - ApiKeyAuth: []
      description: |
        Ranks the caller's FAQs by relevance to a faq_update task's question - semantic similarity
        first when embeddings are available, BM25 keyword overlap as the fallback and tiebreak. Used
        to surface the likely duplicate at the top of an "use an existing FAQ" picker.

        Read-only and soft-fails: expected failure modes (missing taskId, task not found, task not
        owned by the caller, task is not a FAQ suggestion) are returned as HTTP 200 with
        success:false and an error_code field, not as an error status.
      tags:
        - FAQs
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - taskId
              properties:
                taskId:
                  type: string
                  description: ID of the faq_update task to compare FAQs against.
                limit:
                  type: integer
                  description: Maximum number of matches to return (1-50). Defaults to 20.
      responses:
        '200':
          description: Always 200 - check success for the outcome
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    type: object
                    properties:
                      task_id:
                        type: string
                      matches:
                        type: array
                        items:
                          $ref: '#/components/schemas/FaqSimilarMatch'
                  error:
                    type: string
                    description: Present when success is false.
                  error_code:
                    type: integer
                    description: Present when success is false. One of 400 (bad request), 404 (task not found), 403 (task belongs to another account).
              examples:
                found:
                  summary: Matches found
                  value:
                    success: true
                    data:
                      task_id: "task_abc123"
                      matches: []
                soft_fail:
                  summary: Task not found
                  value:
                    success: false
                    error: "Task not found"
                    error_code: 404
  /faqs/resolve-task:
    post:
      summary: Resolve Task with Existing FAQ
      security:
        - ApiKeyAuth: []
      description: |
        Resolves a faq_update task by linking it to an EXISTING FAQ instead of creating a new one,
        sends the FAQ's answer to the linked contact through the standard AI follow-up pipeline, and
        marks the task complete.

        Read-only-safe and soft-fails: expected failure modes (missing fields, task or FAQ not found,
        task not owned by the caller, wrong task type, FAQ has no answer) are returned as HTTP 200
        with success:false and an error_code field, not as an error status. A follow-up-send queue
        being full surfaces as error_code 429 the same way.
      tags:
        - FAQs
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - taskId
                - faqId
              properties:
                taskId:
                  type: string
                  description: ID of the faq_update task to resolve.
                faqId:
                  type: string
                  description: ID of the existing FAQ to link and send.
      responses:
        '200':
          description: Always 200 - check success for the outcome
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    type: object
                    properties:
                      task_id:
                        type: string
                      faq_id:
                        type: string
                      follow_up_status:
                        type: string
                        enum: [published, queued, skipped_no_contact, skipped_no_campaign]
                        description: >-
                          Whether the FAQ answer was sent right away, queued behind an in-flight AI
                          run on the same contact, or skipped because the task has no linked contact
                          or campaign.
                  error:
                    type: string
                    description: Present when success is false.
                  error_code:
                    type: integer
                    description: Present when success is false.
  /faqs/apply-optimization:
    post:
      summary: Apply FAQ Optimization Changes
      security:
        - ApiKeyAuth: []
      description: |
        Applies a set of user-approved changes from a FAQ optimization review to a campaign, or - on
        an agents-workspace account - to an AI Agent. Send exactly one of campaignId or agentId.

        Only actions present in acceptedChanges touch the target's FAQ links - a suggested change the
        caller does not send is left completely alone (neither applied nor deleted).
      tags:
        - FAQs
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - acceptedChanges
              properties:
                campaignId:
                  type: string
                  description: Campaign to apply the changes to. Send this or agentId, not both.
                agentId:
                  type: string
                  description: AI Agent to apply the changes to. Send this or campaignId, not both.
                acceptedChanges:
                  type: array
                  items:
                    $ref: '#/components/schemas/FaqOptimizationChange'
            examples:
              apply_to_campaign:
                summary: Keep one FAQ, remove another
                value:
                  campaignId: "campaign123"
                  acceptedChanges:
                    - action: "keep"
                      faq_id: "faq_abc123"
                    - action: "remove"
                      faq_id: "faq_def456"
      responses:
        '200':
          description: Changes applied
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  message:
                    type: string
                    examples: ["Applied 2 FAQ changes"]
                  faq_count:
                    type: integer
                    description: Number of FAQs linked to the target after the apply.
        '400':
          description: Missing campaignId/agentId, both were supplied, or the id names neither a campaign nor an agent
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
        '404':
          description: >-
            The campaign or agent was not found, has no data, or does not belong to this account.
            Ownership mismatches are also reported here (as "Campaign not found"), not as 403.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
  /faqs/generate-from-documents:
    post:
      summary: Generate FAQs from Documents
      security:
        - ApiKeyAuth: []
      description: |
        Downloads the given already-uploaded document files from Storage, extracts their text, and
        runs AI generation against the caller's existing FAQ library to propose new or updated FAQs
        for the given campaign. The proposal is stored as the campaign's pending_faq_generation
        change set for user review - nothing is written to the FAQ library until it is applied with
        POST /faqs/apply-optimization. Uploaded files are deleted from Storage once processed.
      tags:
        - FAQs
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - campaignId
                - uploadedFiles
              properties:
                campaignId:
                  type: string
                  description: Campaign the generated FAQs will be proposed for.
                uploadedFiles:
                  type: array
                  description: Non-empty array of already-uploaded files to extract FAQs from.
                  items:
                    $ref: '#/components/schemas/FaqUploadedFile'
      responses:
        '202':
          description: Generation ran and a pending change set was stored for review
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  faqCount:
                    type: integer
                    description: Total number of pending FAQ changes proposed.
                  reusedCount:
                    type: integer
                    description: Of those, how many reuse an existing FAQ from the library.
                  modifiedCount:
                    type: integer
                    description: Of those, how many modify an existing FAQ.
                  newCount:
                    type: integer
                    description: Of those, how many are brand new FAQs.
        '400':
          description: Missing campaignId/uploadedFiles, or an uploaded file is missing storagePath/fileName/mimeType
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
        '404':
          description: >-
            The campaign was not found, or does not belong to this account (ownership mismatches are
            also reported here, not as 403)
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
        '500':
          description: >-
            An unexpected error, OR one of the following account-state conditions that currently also
            surface as a generic server error rather than a dedicated status: generation is already
            in progress for this campaign, insufficient credits for generation, an uploaded storage
            path failed the ownership check, or no text could be extracted from any of the files
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
  /faqs/{faqId}/fork-for-campaign:
    post:
      summary: Fork FAQ for Campaign
      security:
        - ApiKeyAuth: []
      description: |
        Forks an existing (typically global) FAQ into a campaign-scoped duplicate and re-points that
        campaign's link from the original FAQ to the new one. The backend equivalent of "edit this
        campaign only" in the FAQ editor. The original FAQ document is preserved and keeps any other
        campaign links it had; only this campaign's link is swapped.
      tags:
        - FAQs
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
        - name: faqId
          in: path
          required: true
          description: ID of the FAQ to fork from.
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - campaign_id
                - question
                - answer
              properties:
                campaign_id:
                  type: string
                  description: Campaign whose link is re-pointed to the new duplicate.
                question:
                  type: string
                  description: Question for the campaign-scoped duplicate.
                answer:
                  type: string
                  description: Answer for the campaign-scoped duplicate.
      responses:
        '201':
          description: The FAQ was forked
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  faq_id:
                    type: string
                    description: ID of the newly created, campaign-scoped FAQ.
                  campaign_id:
                    type: string
                  original_faq_id:
                    type: string
        '400':
          description: Missing campaign_id, question, or answer
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
        '404':
          description: The FAQ or the campaign was not found on this account
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error:
                    type: string
  /mcp-servers:
    get:
      summary: List MCP Servers
      security:
        - ApiKeyAuth: []
      description: Lists the account's registered MCP servers, newest first.
      tags:
        - MCP Servers
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
      responses:
        '200':
          description: MCP servers listed
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  servers:
                    type: array
                    items:
                      $ref: '#/components/schemas/McpServer'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The account's plan does not include the MCP servers feature
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    post:
      summary: Create MCP Server
      security:
        - ApiKeyAuth: []
      description: |
        Registers a new MCP server. name and url are always required, even for an OAuth 2.1 server -
        OAuth authorization itself happens afterwards, in the dashboard, so a server created with
        auth_type "oauth2" saves with oauth_connected false and an empty tool list until that
        completes. For a header-auth server, the initial connection is attempted immediately; an
        unreachable endpoint still saves, with last_error describing why.
      tags:
        - MCP Servers
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - $ref: '#/components/schemas/McpServerFields'
                - type: object
                  required:
                    - name
                    - url
            examples:
              headerAuth:
                summary: Register a server with a static API key header
                value:
                  name: "Order Lookup"
                  url: "https://mcp.example.com/sse"
                  auth_header_name: "X-API-Key"
                  auth_header_value: "sk_live_example"
              oauth:
                summary: Register an OAuth 2.1 server (authorize later in the dashboard)
                value:
                  name: "Support Desk"
                  url: "https://mcp.supportdesk.example.com"
                  auth_type: "oauth2"
      responses:
        '201':
          description: MCP server created
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  server_id:
                    type: string
                  tools:
                    type: array
                    items:
                      $ref: '#/components/schemas/McpServerToolSummary'
                  last_error:
                    type: ['string', 'null']
                  auth_type:
                    type: string
                    description: Only present when creating an OAuth 2.1 server (always "oauth2" in that case).
                  oauth_connected:
                    type: boolean
                    description: Only present when creating an OAuth 2.1 server (always false at creation).
                  server:
                    allOf:
                      - $ref: '#/components/schemas/McpServer'
                      - nullable: true
                    description: The freshly saved, scrubbed server record. null only if the post-save re-read itself failed.
        '400':
          description: Missing name or url, a field over its length limit, an unreachable/SSRF-rejected url, an invalid auth_type, a malformed Basic auth value, or invalid enabled_tools/tool_policies
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The account's plan does not include the MCP servers feature
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /mcp-servers/{serverId}:
    put:
      summary: Update MCP Server
      security:
        - ApiKeyAuth: []
      description: |
        Updates one or more fields on a registered MCP server. At least one field is required; fields
        you don't send keep their saved value. Changing url, auth_header_name or auth_header_value
        re-tests the connection and refreshes the cached tool list (best-effort - a failed
        reconnection updates last_error rather than rejecting the update). Switching auth_type to
        "oauth2" from a header server resets its connection state so it needs to be re-authorized.
      tags:
        - MCP Servers
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
        - name: serverId
          in: path
          required: true
          description: ID of the MCP server to update.
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/McpServerFields'
            examples:
              rotateKey:
                summary: Rotate the saved API key
                value:
                  auth_header_value: "sk_live_new_key"
              disable:
                summary: Temporarily disable the server
                value:
                  enabled: false
      responses:
        '200':
          description: MCP server updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  server_id:
                    type: string
                  tools:
                    type: array
                    items:
                      $ref: '#/components/schemas/McpServerToolSummary'
                  last_error:
                    type: ['string', 'null']
                  server:
                    allOf:
                      - $ref: '#/components/schemas/McpServer'
                      - nullable: true
        '400':
          description: No fields sent, a field over its length limit, an unreachable/SSRF-rejected url, an invalid auth_type, a malformed Basic auth value, or invalid enabled_tools/tool_policies
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The account's plan does not include the MCP servers feature
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: MCP server not found on this account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    delete:
      summary: Delete MCP Server
      security:
        - ApiKeyAuth: []
      description: |
        Permanently deletes an MCP server registration. Also removes it from any campaigns or agents
        that had it enabled, so no agent is left holding a dangling reference.
      tags:
        - MCP Servers
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
        - name: serverId
          in: path
          required: true
          description: ID of the MCP server to delete.
          schema:
            type: string
      responses:
        '200':
          description: MCP server deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  server_id:
                    type: string
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The account's plan does not include the MCP servers feature
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: MCP server not found on this account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /mcp-servers/test-connection:
    post:
      summary: Test MCP Server Connection
      security:
        - ApiKeyAuth: []
      description: |
        Tests connectivity to an MCP server and returns the tools it exposes. Pass server_id to test a
        SAVED server - this refreshes its cached tool list and last_connected_at/last_error on both
        success and failure. Or pass url (plus auth fields) inline to test before saving - nothing is
        persisted in that case. A connection failure resolves normally with success: false rather than
        an error response; only a missing url or an SSRF-rejected url is rejected as invalid input.
      tags:
        - MCP Servers
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              oneOf:
                - type: object
                  required:
                    - server_id
                  properties:
                    server_id:
                      type: string
                      description: ID of a previously saved MCP server to re-test.
                - type: object
                  required:
                    - url
                  properties:
                    url:
                      type: string
                      description: MCP server URL to test without saving it.
                    auth_header_name:
                      type: string
                    auth_header_value:
                      type: string
            examples:
              savedServer:
                summary: Re-test a saved server
                value:
                  server_id: "abc123"
              inline:
                summary: Test before saving
                value:
                  url: "https://mcp.example.com/sse"
                  auth_header_name: "X-API-Key"
                  auth_header_value: "sk_live_example"
      responses:
        '200':
          description: Connection test result (success may be false on a failed connection - this is still a 200)
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  server_name:
                    type: ['string', 'null']
                    description: The server's self-reported name, when the connection succeeded.
                  tools:
                    type: array
                    items:
                      $ref: '#/components/schemas/McpServerToolSummary'
                  error:
                    type: string
                    description: Present only when success is false.
        '400':
          description: Missing url (when server_id is not given) or an SSRF-rejected url
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The account's plan does not include the MCP servers feature
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: server_id given but no such MCP server exists on this account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /meta-templates:
    get:
      summary: List Meta Cloud API Templates
      security:
        - ApiKeyAuth: []
      description: |
        Lists the account's WhatsApp templates from its own WABA, with the
        freshest known approval status. A live Graph listing is merged with
        any newer status the account's inbound webhook has already observed,
        so a just-approved template is never shown as pending.
      tags:
        - Meta Templates
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
        - name: name
          in: query
          required: false
          schema:
            type: string
          description: Exact template name to filter to.
      responses:
        '200':
          description: The account's templates
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  templates:
                    type: array
                    items:
                      $ref: '#/components/schemas/MetaTemplateSummary'
        '400':
          description: No own Meta app (or embedded WhatsApp) connection is set up on this account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                success: false
                error: "Connect your own Meta app first (Settings → Channels → WhatsApp Cloud API), then manage templates here."
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The caller's plan does not include WhatsApp templates, or may not view campaign settings on this account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          description: Meta could not be reached, or refused the request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    post:
      summary: Create Meta Cloud API Template
      security:
        - ApiKeyAuth: []
      description: |
        Creates a WhatsApp template on the account's own WABA and submits it
        for Meta review. Nothing is stored on our side; the returned id and
        status come straight from Meta.
      tags:
        - Meta Templates
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/MetaTemplateCreateInput'
      responses:
        '201':
          description: The template was submitted to Meta for review
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  template:
                    $ref: '#/components/schemas/MetaTemplateCreated'
        '400':
          description: A required field is missing or malformed, no own Meta app connection exists, or Meta rejected the template
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                success: false
                error: "The template was rejected: Param body:1 is not present"
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The caller's plan does not include WhatsApp templates, or may not edit campaign settings on this account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          description: Meta could not be reached, or refused the request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /meta-templates/{name}:
    delete:
      summary: Delete Meta Cloud API Template
      security:
        - ApiKeyAuth: []
      description: |
        Deletes a template from the account's own WABA by its Meta name.
        Without hsm_id, Meta removes EVERY language of that template name;
        pass Meta's template id as hsm_id to delete just one localization.
      tags:
        - Meta Templates
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
        - name: name
          in: path
          required: true
          schema:
            type: string
          description: The template's Meta name.
          examples: ["order_confirmation"]
        - name: hsm_id
          in: query
          required: false
          schema:
            type: string
          description: Meta's template id, to delete only that one localization instead of every language.
      responses:
        '200':
          description: The template was deleted (or Meta reported it was not deleted)
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    description: Meta's own deletion result.
                    examples: [true]
                  name:
                    type: string
                    examples: ["order_confirmation"]
              example:
                success: true
                name: order_confirmation
        '400':
          description: The template name path parameter is missing, no own Meta app connection exists, or Meta rejected the deletion
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                success: false
                error: "Missing required parameter: name"
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The caller's plan does not include WhatsApp templates, or may not edit campaign settings on this account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          description: Meta could not be reached, or refused the request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /webhooks/validate:
    post:
      summary: Test Unsaved Webhook URL
      security:
        - ApiKeyAuth: []
      description: |
        Sends a sample payload to a webhook URL that has not been saved yet - the "test connection"
        step of the create/edit flow. Distinct from POST /webhooks/{webhookId}/test, which tests an
        already-saved subscription by id. Optionally pass event to control which event type the
        sample payload simulates.
      tags:
        - Webhooks
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - webhookUrl
              properties:
                webhookUrl:
                  type: string
                  description: The URL to test.
                event:
                  type: string
                  description: Event type to simulate. Must be one of the values from GET /webhooks/events. Defaults to a delivery event.
            examples:
              validate:
                value:
                  webhookUrl: "https://example.com/webhooks/dmchamp"
                  event: "Contact Created"
      responses:
        '200':
          description: The receiver accepted the test payload
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookTestResultResponse'
              example:
                success: true
                message: "Webhook URL is working correctly."
        '400':
          description: webhookUrl is missing, or the receiver rejected or failed to answer the test delivery
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Webhooks are not enabled for this account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /webhooks/test-campaign-tag:
    post:
      summary: Test Campaign Tag Webhook
      security:
        - ApiKeyAuth: []
      description: |
        Sends a test payload to the webhook URL configured on a specific campaign tag. The agent-side
        equivalent is POST /webhooks/test-agent-tag.
      tags:
        - Webhooks
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - campaignId
                - tagName
              properties:
                campaignId:
                  type: string
                tagName:
                  type: string
      responses:
        '200':
          description: The receiver accepted the test payload
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookTestResultResponse'
        '400':
          description: campaignId or tagName missing, the tag has no webhook URL configured, or the receiver rejected or failed to answer the test delivery
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Webhooks are not enabled for this account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Campaign not found, or the campaign has no such tag
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /webhooks/test-agent-tag:
    post:
      summary: Test Agent Tag Webhook
      security:
        - ApiKeyAuth: []
      description: |
        Sends a test payload to the webhook URL configured on a specific AI Agent tag - the
        agent-side equivalent of POST /webhooks/test-campaign-tag, for accounts where the agent has
        no reachable campaign. Tag name matching is exact first, then case-insensitive.

        A refused or failed delivery is reported as a normal 200 response with success:false, not as
        an error status.
      tags:
        - Webhooks
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - agentId
                - tagName
              properties:
                agentId:
                  type: string
                tagName:
                  type: string
      responses:
        '200':
          description: Test attempted - check success for the delivery outcome
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  message:
                    type: string
              examples:
                delivered:
                  value:
                    success: true
                    message: 'Test payload successfully sent to webhook URL for tag "Booked".'
                rejected:
                  value:
                    success: false
                    message: "Webhook responded with 404: Not Found"
        '400':
          description: agentId or tagName missing, or the tag has no webhook URL configured
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Webhooks are not enabled for this account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Agent not found, or the agent has no such tag
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /phone-numbers/byo:
    post:
      summary: Add Bring-Your-Own Phone Number
      security:
        - ApiKeyAuth: []
      description: |
        Records a bring-your-own WhatsApp phone number you already control. Nothing is provisioned
        with a provider here - this creates a placeholder record (is_active false) that becomes live
        once you complete WhatsApp OAuth to register a Sender for it. No credits are charged.

        The record's id is the E.164 number, so adding a number that already exists on the account
        (in any of its equivalent spellings) is refused with 409.
      tags:
        - Phone Numbers
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ByoPhoneNumberRequest'
            example:
              phone_number: "+14155551234"
              country_code: "US"
              display_name: "Support line"
      responses:
        '201':
          description: The record was created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ByoPhoneNumberResponse'
        '400':
          description: phone_number/country_code missing or invalid, or the number matches Meta's WhatsApp test number
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChannelsErrorResponse'
        '409':
          description: This phone number (or an equivalent spelling of it) already exists on the account
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error_code:
                    type: integer
                    examples: [409]
                  reason:
                    type: string
                    examples: ["phone_number_exists"]
                  error:
                    type: string
  /phone-numbers/{phoneNumber}/set-primary:
    post:
      summary: Set Primary Phone Number
      security:
        - ApiKeyAuth: []
      description: |
        Atomically makes this the account's single active phone number: this number's is_active flag
        is set true and every other number's is_active flag is set false in one write, so a reader
        never sees two active numbers or none.
      tags:
        - Phone Numbers
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
        - name: phoneNumber
          in: path
          required: true
          description: The phone number in E.164 format. A missing leading plus sign is tolerated.
          schema:
            type: string
          example: "+31612345678"
      responses:
        '200':
          description: The number is now the account's active number
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PhoneNumberDetailResponse'
        '400':
          description: Missing phoneNumber parameter
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChannelsErrorResponse'
        '404':
          description: The number does not exist on this account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChannelsErrorResponse'
  /phone-numbers/{phoneNumber}/record:
    delete:
      summary: Delete Phone Number Record
      security:
        - ApiKeyAuth: []
      description: |
        Deletes a phone-number document from the account with no provider-side release and no
        cooldown - the REST equivalent of removing the row directly. Use this for BYO / WhatsApp Web
        / Telegram / LINE records and stale entries. For a purchased/managed WhatsApp number that
        should also be de-registered with the provider and enter the 7-day repurchase cooldown, use
        DELETE /phone-numbers/{phoneNumber} instead. A missing number is a 404, not a silent no-op.
      tags:
        - Phone Numbers
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
        - name: phoneNumber
          in: path
          required: true
          description: The phone number in E.164 format. A missing leading plus sign is tolerated.
          schema:
            type: string
          example: "+31612345678"
      responses:
        '200':
          description: The record was deleted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PhoneNumberRecordDeleteResponse'
        '400':
          description: Missing phoneNumber parameter
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChannelsErrorResponse'
        '404':
          description: The number does not exist on this account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChannelsErrorResponse'
  /chat-exports/{contactId}:
    get:
      summary: Export Contact Chat Messages
      security:
        - ApiKeyAuth: []
      description: |
        Exports a single contact's chat history as either a downloadable text transcript or
        structured JSON. Distinct from GET /chat-exports/recent, which exports every contact with
        recent activity in one call.
      tags:
        - Chat Exports
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
        - name: contactId
          in: path
          required: true
          schema:
            type: string
        - name: format
          in: query
          required: false
          schema:
            type: string
            enum: [txt, json]
            default: txt
        - name: filter
          in: query
          required: false
          schema:
            type: string
            enum: [all, media, tool_use, text]
            default: all
          description: |
            Filter messages by type:
            - all: include all messages
            - media: only messages with media attachments
            - tool_use: only tool use / AI action messages
            - text: only plain text messages (no media, no tool use)
      responses:
        '200':
          description: The chat export
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  data:
                    description: A plain-text transcript when format=txt (the default), or structured data when format=json.
                    oneOf:
                      - type: string
                      - $ref: '#/components/schemas/ChatExportJsonData'
        '400':
          description: contactId missing
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The contact does not belong to the authenticated account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Contact not found, or outside the caller's visibility scope
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /chat-exports/{contactId}/email:
    post:
      summary: Email Chat Transcript
      security:
        - ApiKeyAuth: []
      description: |
        Emails a contact their own conversation transcript. Returns 200 (not 202) meaning ACCEPTED -
        the recipient resolved and the transcript was built and queued for delivery - not that it has
        been delivered; a billing block or a missing send configuration can still drop it downstream
        and is not reported here.
      tags:
        - Chat Exports
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
        - name: contactId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                recipient_email:
                  type: string
                  description: Override the destination. Defaults to the contact's stored email.
                via:
                  type: string
                  enum: [auto, transactional, email_channel]
                  default: auto
                  description: Which pipe to send through. auto picks email_channel when the conversation is on the email channel and a matching inbox is connected, else transactional.
                note:
                  type: string
                  maxLength: 1000
                  description: Optional line from the sender, shown above the transcript.
      responses:
        '200':
          description: The transcript was queued for delivery
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  data:
                    $ref: '#/components/schemas/ChatExportEmailResult'
        '400':
          description: contactId missing, the note is too long, or recipient_email is not a valid address
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The contact does not belong to the authenticated account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Contact not found, or outside the caller's visibility scope
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /chat-exports/data-export:
    post:
      summary: Request Full Account Data Export
      security:
        - ApiKeyAuth: []
      description: |
        GDPR data-portability entry point. Enqueues a full-account export covering the requested
        categories and emails the account owner a download link when it is ready. Owner-only - a
        team-member session is rejected. Rate-limited per account; a repeat request while one is
        already being prepared, or shortly after the last one finished, is refused.
      tags:
        - Chat Exports
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                categories:
                  type: array
                  description: Subset of the supported export categories. Omit for the default set.
                  items:
                    type: string
      responses:
        '202':
          description: The export was queued
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  data:
                    type: object
                    properties:
                      exportId:
                        type: string
        '400':
          description: No valid category was selected, or the account has no email address to deliver the export to
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Account not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: An export is already being prepared for this account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '429':
          description: An export was requested too recently - wait a few minutes and try again
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /chat-exports/media/download-url:
    post:
      summary: Get Chat Media Download URL
      security:
        - ApiKeyAuth: []
      description: |
        Issues a short-lived (1 hour) signed URL for a message's media object. Use this to re-fetch
        media after a previously issued URL has expired.
      tags:
        - Chat Exports
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - messagePath
              properties:
                messagePath:
                  type: string
                  description: Firestore path of the message document, of the form contacts/{contactId}/messages/{messageId}.
                  examples: ["contacts/contact_abc123/messages/msg_def456"]
      responses:
        '200':
          description: The signed URL
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  data:
                    $ref: '#/components/schemas/MediaDownloadUrlResponse'
        '400':
          description: messagePath missing or not of the required form
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The caller does not have permission to access this contact's media
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Message not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /chat-exports/media/upload:
    post:
      summary: Upload Chat Media
      security:
        - ApiKeyAuth: []
      description: |
        Uploads base64-encoded media (for example a pasted clipboard image) to be attached to an
        outbound chat message, and returns a 7-day signed URL. Persist storagePath on the outbound
        message so the URL can be re-minted with POST /chat-exports/media/download-url once the
        7-day TTL elapses. Supports images (JPEG, PNG, GIF, WebP), video (MP4, WebM), audio (OGG,
        MP3, WAV, M4A, AAC), PDF, and common office document formats, up to 10 MB.
      tags:
        - Chat Exports
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - base64Data
                - mimeType
              properties:
                base64Data:
                  type: string
                  description: Base64-encoded file data, without a data URL prefix.
                mimeType:
                  type: string
                  examples: ["image/png"]
                fileName:
                  type: string
      responses:
        '201':
          description: The media was uploaded
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  data:
                    $ref: '#/components/schemas/ChatMediaUploadResult'
        '400':
          description: base64Data/mimeType missing, mimeType is not supported, the base64 data is invalid or empty, or the file exceeds 10 MB
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /summaries/daily/{date}:
    get:
      summary: Get Daily Summary
      security:
        - ApiKeyAuth: []
      description: |
        Returns the account's AI-generated daily summary for one date, or a null summary if it has
        not been generated yet. Requires the daily_summaries feature to be enabled on the account.
      tags:
        - Summaries
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
        - name: date
          in: path
          required: true
          description: Date in YYYY-MM-DD form. Cannot be in the future.
          schema:
            type: string
          example: "2026-06-08"
      responses:
        '200':
          description: The daily summary (possibly null) and the account's section configs
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DailySummaryResponse'
        '400':
          description: date is not in YYYY-MM-DD form, or is in the future
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Daily summaries are not enabled for this account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /summaries/daily/{date}/regenerate:
    post:
      summary: Regenerate Daily Summary
      security:
        - ApiKeyAuth: []
      description: |
        Kicks off (re)generation of a daily summary in the background and returns a generating
        placeholder immediately - poll GET /summaries/daily/{date} until the status flips to
        completed or failed. deleteTasks controls whether the tasks auto-created from the previous
        version of this summary are deleted before regenerating; defaults to true.
      tags:
        - Summaries
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
        - name: date
          in: path
          required: true
          description: Date in YYYY-MM-DD form. Cannot be in the future.
          schema:
            type: string
          example: "2026-06-08"
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                deleteTasks:
                  type: boolean
                  default: true
                  description: Delete the tasks auto-created from the previous version of this summary before regenerating.
      responses:
        '200':
          description: Regeneration started - the summary's status is now generating
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DailySummaryResponse'
        '400':
          description: date is not in YYYY-MM-DD form, or is in the future
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Daily summaries are not enabled for this account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /summaries/chat-summary:
    post:
      summary: Generate On-Demand Chat Summary
      security:
        - ApiKeyAuth: []
      description: |
        Generates an AI summary of a contact's most recent chat session (the last closed session, or
        the currently open one if there is no closed session yet) and attaches it to that session.
        Distinct from POST /summaries, the legacy endpoint that looks the contact up by phone number
        or email instead of by id.
      tags:
        - Summaries
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - contactId
              properties:
                contactId:
                  type: string
      responses:
        '200':
          description: The summary was generated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  data:
                    type: string
                    examples: ["Chat summary generated successfully"]
        '400':
          description: contactId missing
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The contact does not belong to the authenticated account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Contact not found, outside the caller's visibility scope, or the contact has no chat session / no messages yet
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /notifications/{id}/read:
    patch:
      summary: Mark Notification Read
      security:
        - ApiKeyAuth: []
      description: Marks a single notification as read.
      tags:
        - Notifications
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: id
          in: path
          required: true
          description: ID of the notification to mark read.
          schema:
            type: string
      responses:
        '200':
          description: Notification marked read
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  id:
                    type: string
        '400':
          description: Missing required parameter id
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Notification not found on this account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /notifications/read-all:
    post:
      summary: Mark All Notifications Read
      security:
        - ApiKeyAuth: []
      description: |
        Marks every unread notification on the account as read, up to 500 rows in a single call. An
        account with more than 500 unread notifications needs to call this again to clear the rest.
      tags:
        - Notifications
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
      responses:
        '200':
          description: Notifications marked read (updated is 0 when there was nothing unread)
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  updated:
                    type: integer
                    description: Number of notifications marked read, capped at 500.
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /notifications/read-by-contact:
    post:
      summary: Mark Contact Notifications Read
      security:
        - ApiKeyAuth: []
      description: |
        Marks every unread notification about ONE contact as read, up to 500 rows in a single call.
        Call this when the user opens that contact's chat, or clicks any single one of their unread
        notification rows - it settles the whole conversation's notifications, not just the one
        clicked.
      tags:
        - Notifications
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - contact_id
              properties:
                contact_id:
                  type: string
                  description: ID of the contact whose unread notifications should be marked read.
            examples:
              markContact:
                summary: Settle one contact's notifications
                value:
                  contact_id: "contact123"
      responses:
        '200':
          description: Notifications marked read (updated is 0 when there was nothing unread for this contact)
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  updated:
                    type: integer
                    description: Number of notifications marked read, capped at 500.
        '400':
          description: Missing required parameter contact_id
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /notifications/{id}:
    delete:
      summary: Dismiss Notification
      security:
        - ApiKeyAuth: []
      description: Permanently deletes a single notification. This cannot be undone.
      tags:
        - Notifications
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: id
          in: path
          required: true
          description: ID of the notification to dismiss.
          schema:
            type: string
      responses:
        '200':
          description: Notification dismissed
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  id:
                    type: string
        '400':
          description: Missing required parameter id
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Notification not found on this account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /scheduled-tasks/force-execute:
    post:
      summary: Force-Execute Scheduled Task
      security:
        - ApiKeyAuth: []
      description: |
        Runs a contact's scheduled task immediately instead of waiting for its scheduled time - a
        delayed AI response, a scheduled message, or a follow-up task tracked on the contact. Pass
        taskId to run one specific task; otherwise the platform looks for an active task, preferring
        taskType when given, and otherwise trying (in order) a delayed AI response, a scheduled
        message, then any tracked follow-up task.
      tags:
        - Scheduled Tasks
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - contactId
              properties:
                contactId:
                  type: string
                  description: ID of the contact whose task should be executed.
                taskId:
                  type: string
                  description: ID of a specific task to force-execute. When omitted, the platform searches for an active task instead.
                taskType:
                  type: string
                  enum: [delayed_ai_response, scheduled_message, ai_chat_retry]
                  description: Restricts the search (or, with taskId, validates the found task's type) to one task type.
            examples:
              specificTask:
                summary: Force-run a known task
                value:
                  contactId: "contact123"
                  taskId: "task456"
              anyActive:
                summary: Force-run whatever active task exists
                value:
                  contactId: "contact123"
      responses:
        '200':
          description: Task executed
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  message:
                    type: string
        '400':
          description: Missing contactId, an invalid taskType, or taskId's task is of a different type than the taskType sent
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The contact does not belong to this account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: taskId not found, or no active (matching) task exists for this contact
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /scheduled-tasks/batch:
    post:
      summary: Generate Drip Campaign Batches
      security:
        - ApiKeyAuth: []
      description: |
        Splits a drip campaign's contacts into scheduled send batches according to its batch_size,
        frequency and trigger_days settings, then writes them onto the campaign. A campaign that
        already has batches is rejected unless forceClear is sent - except a Draft campaign, whose
        existing batches are cleared and regenerated automatically so it can be relaunched without an
        explicit forceClear.
        Note: a campaign whose drip settings are themselves invalid or missing (no batch_size, no
        frequency, an hourly frequency, or a batch_size over 500) currently fails as a plain 500
        Internal server error rather than a 400 - the underlying validation error is not surfaced to
        the response body. Check the campaign's drip configuration first if you hit this.
      tags:
        - Scheduled Tasks
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - campaignId
              properties:
                campaignId:
                  type: string
                  description: ID of the drip campaign to generate batches for.
                forceClear:
                  type: boolean
                  description: When true, clears any existing batches (and processing state) before regenerating them. Defaults to false.
            examples:
              generate:
                summary: Generate batches for a new drip campaign
                value:
                  campaignId: "campaign123"
              relaunch:
                summary: Clear existing batches and regenerate
                value:
                  campaignId: "campaign123"
                  forceClear: true
      responses:
        '200':
          description: Batches created, or a validation failure reported without a thrown error (still HTTP 200 body shape, but success is false)
          content:
            application/json:
              schema:
                oneOf:
                  - type: object
                    properties:
                      success:
                        type: boolean
                        examples: [true]
                      data:
                        type: string
                        description: Human-readable summary, e.g. "Success creating 4 batch tasks for campaign campaign123 with 120 unique contacts".
                  - $ref: '#/components/schemas/ErrorResponse'
        '400':
          description: Missing campaignId, the campaign is not a drip campaign, or it already has batches and forceClear was not sent
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The campaign does not belong to this account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          description: The campaign's drip settings (batch_size, frequency, trigger_days) are missing or invalid - see the note above
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /scheduled-tasks/follow-up:
    post:
      summary: Schedule Manual Follow-Up
      security:
        - ApiKeyAuth: []
      description: |
        Schedules a one-shot follow-up for a contact from a natural-language instruction, for example
        "follow up in 2 days saying hey just checking in" - the instruction is parsed by AI into a
        delay (capped at 30 days) and a message. If the instruction only describes intent ("check if
        they're still interested") rather than dictating exact wording, the returned messagePreview is
        a placeholder and the AI composes the real message at send time instead of sending the
        placeholder verbatim.
        Scheduling a new manual follow-up cancels any other pending follow-up on the contact first
        (an existing manual follow-up, any queued automatic follow-up tasks, and a pending cold-lead
        follow-up) - the operator taking over replaces whatever was queued.
      tags:
        - Scheduled Tasks
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - contactId
                - campaignId
                - instruction
              properties:
                contactId:
                  type: string
                  description: ID of the contact to follow up with.
                campaignId:
                  type: string
                  description: ID of the campaign context the follow-up runs under.
                instruction:
                  type: string
                  description: Natural-language follow-up instruction for the AI to parse.
            examples:
              followUp:
                summary: Schedule a follow-up in two days
                value:
                  contactId: "contact123"
                  campaignId: "campaign123"
                  instruction: "follow up in 2 days saying hey just checking in"
      responses:
        '200':
          description: Follow-up scheduled
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    type: object
                    properties:
                      scheduledAt:
                        type: string
                        format: date-time
                        description: When the follow-up will fire.
                      messagePreview:
                        type: string
                        description: The message that will be sent, or a stand-in note when the AI will compose it at send time.
        '400':
          description: Missing contactId/campaignId/instruction, the instruction could not be understood by the AI parser, or the parsed delay exceeds 30 days
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The contact does not belong to this account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: The contact is outside the caller's visible scope (team contact-visibility restrictions)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /scheduled-tasks/follow-up/{contactId}:
    delete:
      summary: Cancel Manual Follow-Up
      security:
        - ApiKeyAuth: []
      description: |
        Cancels a contact's pending manual follow-up. By default only the one-shot manual follow-up
        is cancelled; pass scope=all to also cancel the automatic follow-up engine's queued tasks and
        any pending cold-lead follow-up - everything the chat UI's "Next follow-up in X" would show.
        A contact with no pending follow-up to cancel still returns success.
      tags:
        - Scheduled Tasks
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
        - name: contactId
          in: path
          required: true
          description: ID of the contact whose follow-up should be cancelled.
          schema:
            type: string
        - name: scope
          in: query
          required: false
          description: 'Sending "all" also cancels automatic-engine and cold-lead follow-ups; omitted cancels only the manual one-shot.'
          schema:
            type: string
            enum: [all]
      responses:
        '200':
          description: Follow-up(s) cancelled (or there was nothing to cancel)
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    type: 'null'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The contact does not belong to this account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /short-links:
    post:
      summary: Create Short Link
      security:
        - ApiKeyAuth: []
      description: |
        Creates a new WhatsApp short link. The slug is always generated by the server - do not send a
        slug field on create: if one is present it is treated as an update to that existing slug
        instead of creating a new link.
      tags:
        - Short Links
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - $ref: '#/components/schemas/ShortLinkFields'
                - type: object
                  required:
                    - phoneNumbers
            examples:
              minimal:
                summary: Create a link routing to one number
                value:
                  phoneNumbers: ["+14155551234"]
              full:
                summary: Create a link with a prefilled message and no sticky routing
                value:
                  phoneNumbers: ["+14155551234", "+14155555678"]
                  prefillMessage: "Hi, I saw your ad and wanted to ask about..."
                  stickyRouting: false
      responses:
        '201':
          description: Short link created
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    $ref: '#/components/schemas/ShortLinkResult'
        '400':
          description: phoneNumbers missing/empty, an entry that is not a non-empty string, or a number that is not owned by this account or is not WhatsApp-reachable
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /short-links/{slug}:
    put:
      summary: Update Short Link
      security:
        - ApiKeyAuth: []
      description: |
        Updates a short link owned by this account. The slug comes from the URL path, never the
        request body. This is a full-resource replace for stickyRouting and disabled specifically -
        omitting either one resets it to its default (stickyRouting true, disabled false) rather than
        keeping the previously saved value, so always send the full desired state for those two
        fields. prefillMessage is the exception: omit it and the existing value is left unchanged.
      tags:
        - Short Links
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
        - name: slug
          in: path
          required: true
          description: Slug of the short link to update.
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - $ref: '#/components/schemas/ShortLinkFields'
                - type: object
                  required:
                    - phoneNumbers
            examples:
              swapNumbers:
                summary: Point the link at a different number and keep sticky routing on
                value:
                  phoneNumbers: ["+14155559999"]
                  stickyRouting: true
      responses:
        '200':
          description: Short link updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    $ref: '#/components/schemas/ShortLinkResult'
        '400':
          description: Missing slug, phoneNumbers missing/empty, an entry that is not a non-empty string, or a number that is not owned by this account or is not WhatsApp-reachable
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: This short link belongs to a different account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: No short link with this slug exists
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    delete:
      summary: Delete Short Link
      security:
        - ApiKeyAuth: []
      description: Permanently deletes a short link owned by this account. This cannot be undone.
      tags:
        - Short Links
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
        - name: slug
          in: path
          required: true
          description: Slug of the short link to delete.
          schema:
            type: string
      responses:
        '200':
          description: Short link deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    type: string
                    examples: ["Short link deleted"]
        '400':
          description: Missing slug
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: This short link belongs to a different account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: No short link with this slug exists
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /subaccounts/agency-pricing:
    get:
      summary: Get Agency Pricing For SubAccount
      security:
        - ApiKeyAuth: []
      description: |
        Returns the caller's own parent agency's credit pricing configuration —
        the plans and optional per-credit top-up rate a sub-account can buy.

        Called BY a sub-account about its OWN agency, not by the agency about a
        client, so it is authenticated as a plain account owner rather than
        through team_management: the caller must itself be a sub-account
        (is_agency_sub_account true, linked to an agency_account).
      tags:
        - SubAccounts
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
      responses:
        '200':
          description: Pricing retrieved successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  data:
                    $ref: '#/components/schemas/SubAccountAgencyPricingData'
        '400':
          description: The caller is not linked to an agency
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The caller is not an agency sub-account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: The caller's linked agency account no longer exists
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /subaccounts/credit-usage:
    get:
      summary: Get SubAccount Credit Usage
      security:
        - ApiKeyAuth: []
      description: |
        Credit usage for an agency's sub-accounts over a date range.

        Without subAccountId, returns a per-sub-account summary (totals, a
        breakdown by usage reason, and top campaigns). With subAccountId, switches
        to detail mode and additionally returns the raw, paginated usage rows for
        that one sub-account.

        Cost and token figures are redacted (costsRedacted: true) when the viewer
        is a sub-account spending against its agency's own BYOK key rather than a
        key of its own — the underlying provider cost is the agency's business,
        not the client's.

        Requires the team_management area at view level. Agency-wide, so an
        agency-scoped sub-account grant does not reach this endpoint.
      tags:
        - SubAccounts
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: from
          in: query
          required: true
          description: Start of the date range, ISO 8601.
          schema:
            type: string
          example: "2026-05-01T00:00:00.000Z"
        - name: to
          in: query
          required: true
          description: End of the date range, ISO 8601.
          schema:
            type: string
          example: "2026-06-01T00:00:00.000Z"
        - name: subAccountId
          in: query
          required: false
          description: Switch to detail mode for this one sub-account and include its raw usage rows.
          schema:
            type: string
        - name: limitCount
          in: query
          required: false
          description: Max raw rows to return in detail mode (1-2000, default 500). Ignored outside detail mode.
          schema:
            type: integer
            minimum: 1
            maximum: 2000
            default: 500
        - name: startAfterTimestamp
          in: query
          required: false
          description: Pagination cursor for detail mode, from a previous response's lastTimestamp.
          schema:
            type: string
      responses:
        '200':
          description: Credit usage retrieved successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  data:
                    $ref: '#/components/schemas/SubAccountsCreditUsageData'
        '400':
          description: dateRange was missing, invalid, or from was after to
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: subAccountId names a sub-account other than the caller's own, and the caller is not an agency or dev account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /subaccounts/campaign-status:
    get:
      summary: Get SubAccount Campaign Status
      security:
        - ApiKeyAuth: []
      description: |
        Campaign health overview across every sub-account of the caller's agency:
        counts of live, paused, draft and completed campaigns, whether each
        sub-account has issues (like paused campaigns), and when it was last
        active. Paginated, newest-created sub-account first.

        Requires the team_management area at view level. Agency-wide, so an
        agency-scoped sub-account grant does not reach this endpoint.
      tags:
        - SubAccounts
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: pageSize
          in: query
          required: false
          description: Sub-accounts per page (1-50, default 10).
          schema:
            type: integer
            minimum: 1
            maximum: 50
            default: 10
        - name: lastDocumentId
          in: query
          required: false
          description: Pagination cursor from a previous response's lastDocumentId.
          schema:
            type: string
        - name: searchQuery
          in: query
          required: false
          description: Filter sub-accounts by name or email, case-insensitive.
          schema:
            type: string
      responses:
        '200':
          description: Campaign status retrieved successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  data:
                    $ref: '#/components/schemas/SubAccountsCampaignStatusData'
        '403':
          description: Caller is not an agency or dev account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /subaccounts/campaigns/copy:
    post:
      summary: Copy Campaign To SubAccount
      security:
        - ApiKeyAuth: []
      description: |
        Copies a campaign from the agency's own account or any of its
        sub-accounts to another sub-account. Referenced data (FAQs and the
        knowledge base, and optionally custom functions and MCP servers) is
        deep-copied into the target account so nothing is shared across accounts.
        Runtime and account-specific fields are cleared, and the source's
        WhatsApp templates and connected social posts are dropped from the copy.
        If any step fails, everything already created is rolled back.

        Requires the team_management area at edit level.
      tags:
        - SubAccounts
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - campaignId
                - targetUserId
              properties:
                campaignId:
                  type: string
                  description: Id of the source campaign to copy.
                targetUserId:
                  type: string
                  description: Id of the sub-account to copy the campaign into.
                newName:
                  type: string
                  description: Name for the copy. Defaults to the source campaign's name.
                copyFaqs:
                  type: boolean
                  default: true
                  description: Whether to deep-copy FAQs and the knowledge base into the target account.
                copyCustomFunctions:
                  type: boolean
                  default: false
                  description: Whether to deep-copy custom functions and MCP servers into the target account.
      responses:
        '200':
          description: Campaign copied successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  data:
                    $ref: '#/components/schemas/CopyCampaignToSubAccountResult'
        '400':
          description: campaignId or targetUserId was missing
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The caller is not an agency, or the source or target account does not belong to it
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: The source campaign was not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /subaccounts/{subAccountUid}/sso-link:
    post:
      summary: Create SubAccount SSO Link
      security:
        - ApiKeyAuth: []
      description: |
        Mints a one-hour, ready-to-open login link that signs the named
        sub-account straight into its own dashboard — no password step. Built for
        integrators embedding the dashboard for their end clients.

        The link opens the auth handoff page, which consumes the token and lands
        on redirect. deep_link_url is the same in-app destination, for
        integrators that navigate a frame in two steps instead of opening the
        full URL. The token itself rides the URL fragment so it never reaches
        server logs.

        Requires the team_management area at edit level. Deliberately NOT
        reachable through a sub-account grant — it mints a full sign-in session,
        so admitting a role-limited grant here would let it act as the sub-account
        owner regardless of the grant's role.
      tags:
        - SubAccounts
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: subAccountUid
          in: path
          required: true
          description: The sub-account's user id.
          schema:
            type: string
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                redirect:
                  type: string
                  description: In-app path to land on after login. Defaults to the app root.
                  examples: ["/chats"]
                app_base_url:
                  type: string
                  format: uri
                  description: Dashboard host override, must be a plain https URL. Defaults to the agency's white-label app host, or the platform host.
      responses:
        '200':
          description: Login link minted
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  url:
                    type: string
                    description: The ready-to-open login URL. Open it directly, or in a new tab/window.
                  deep_link_url:
                    type: ['string', 'null']
                    description: The in-app destination the link lands on after login.
                  expires_at:
                    type: string
                    description: ISO 8601 expiry of the underlying login token, one hour from minting.
                  sub_account_uid:
                    type: string
        '400':
          description: app_base_url was not a plain https URL
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: This sub-account does not belong to your agency
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /subaccounts/{subAccountUid}/max-tier:
    put:
      summary: Set SubAccount Max Tier
      security:
        - ApiKeyAuth: []
      description: |
        Turns the Max AI model tier on or off in the named sub-account's tier
        picker. Enabling it on a BYOK agency changes that sub-account's AI cost
        from free (via your API key) to 0.25 credits/action drawn from your
        credit pool, so it is a deliberate per-sub-account decision.

        Requires the team_management area at edit level. Reachable through a
        sub-account grant at editor/admin role.
      tags:
        - SubAccounts
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: subAccountUid
          in: path
          required: true
          description: The sub-account's user id.
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - enabled
              properties:
                enabled:
                  type: boolean
      responses:
        '200':
          description: Max tier setting updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  data:
                    type: object
                    properties:
                      subAccountUid:
                        type: string
                      enabled:
                        type: boolean
        '400':
          description: enabled was missing or not a boolean, or the target user is not an agency sub-account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Caller is not an agency, or the sub-account does not belong to it
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Sub-account not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /subaccounts/{subAccountUid}/max-rate:
    put:
      summary: Set SubAccount Max Rate
      security:
        - ApiKeyAuth: []
      description: |
        Sets what the named sub-account's OWN balance burns per Max-model AI
        action — your client-facing rate. Your agency pool independently pays
        platform list x your Insider Rate; the difference is your margin.

        rate must be null (clears back to platform list, 0.25) or a number
        greater than zero, at least your own cost per Max action, and at most 10.
        A rate below your own cost is rejected — it would lose credits on every
        action.

        Requires the team_management area at edit level. Reachable through a
        sub-account grant at editor/admin role.
      tags:
        - SubAccounts
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: subAccountUid
          in: path
          required: true
          description: The sub-account's user id.
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - rate
              properties:
                rate:
                  type: ['number', 'null']
                  minimum: 0
                  maximum: 10
                  description: Credits charged per Max action. Null clears the override back to platform list.
      responses:
        '200':
          description: Max rate updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  data:
                    type: object
                    properties:
                      subAccountUid:
                        type: string
                      rate:
                        type: ['number', 'null']
        '400':
          description: rate was out of range or below your own cost per action, or the target user is not an agency sub-account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Caller is not an agency, or the sub-account does not belong to it
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Sub-account not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /subaccounts/{subAccountUid}/insider-rate:
    put:
      summary: Set SubAccount Insider Rate
      security:
        - ApiKeyAuth: []
      description: |
        Turns your Champions Circle Insider Rate (0.2 credits instead of the
        standard 0.25) on or off for one named sub-account's Max-model AI actions
        and Lead Finder leads.

        Enabling requires the caller to currently hold a Circle membership.
        Disabling is always allowed, so a lapsed member can wind sub-accounts back.

        Requires the team_management area at edit level. Reachable through a
        sub-account grant at editor/admin role.
      tags:
        - SubAccounts
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: subAccountUid
          in: path
          required: true
          description: The sub-account's user id.
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - enabled
              properties:
                enabled:
                  type: boolean
      responses:
        '200':
          description: Insider rate setting updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  data:
                    type: object
                    properties:
                      subAccountUid:
                        type: string
                      enabled:
                        type: boolean
                      rate:
                        type: number
                        description: The now-effective rate in credits.
                        examples: [0.2]
        '400':
          description: enabled was missing or not a boolean, or the target user is not an agency sub-account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: |
            Caller is not an agency, the caller tried to enable it without holding a
            Circle membership, or the sub-account does not belong to the caller's agency
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Sub-account not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /subaccounts/{subAccountUid}/action-pricing:
    put:
      summary: Set SubAccount Action Pricing
      security:
        - ApiKeyAuth: []
      description: |
        Merges per-action client rates onto the named sub-account — the
        generalisation of Set SubAccount Max Rate to every billable action, plus
        the WhatsApp fee markup. Only the keys you send change; a key mapped to
        null unsets it, falling back to max_action_rate (for AI actions) or
        platform list.

        Allowed keys: AI_MESSAGE, AI_TOOL_USE, EVALUATION_CALL,
        INTERRUPTION_HANDLING, CONTACT_TAG, CHAT_SUMMARY (credits per action, each
        greater than 0 and at most 10), and wa_carrier_multiplier (a multiplier
        from 1 up to 10 applied to every non-AI WhatsApp fee the sub-account pays:
        monthly number rent, managed-lane service/inbound fees, and the
        Meta/Twilio template pass-through). An unknown key, or a value outside
        its allowed range, rejects the whole call and names every offending key.

        Requires the team_management area at edit level. Reachable through a
        sub-account grant at editor/admin role.
      tags:
        - SubAccounts
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: subAccountUid
          in: path
          required: true
          description: The sub-account's user id.
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - actionPricing
              properties:
                actionPricing:
                  type: object
                  description: Map of action key to rate. A null value unsets that key.
                  additionalProperties:
                    type: ['number', 'null']
                  properties:
                    AI_MESSAGE:
                      type: ['number', 'null']
                    AI_TOOL_USE:
                      type: ['number', 'null']
                    EVALUATION_CALL:
                      type: ['number', 'null']
                    INTERRUPTION_HANDLING:
                      type: ['number', 'null']
                    CONTACT_TAG:
                      type: ['number', 'null']
                    CHAT_SUMMARY:
                      type: ['number', 'null']
                    wa_carrier_multiplier:
                      type: ['number', 'null']
            examples:
              price_ai_replies:
                summary: Price AI replies at 0.6 credits and cap the WhatsApp fee markup at 1.5x
                value:
                  actionPricing:
                    AI_MESSAGE: 0.6
                    wa_carrier_multiplier: 1.5
      responses:
        '200':
          description: Action pricing updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  data:
                    type: object
                    properties:
                      subAccountUid:
                        type: string
                      actionPricing:
                        type: ['object', 'null']
                        description: The now-effective merged map, or null when the merge left no keys set.
                        additionalProperties:
                          type: number
        '400':
          description: actionPricing was empty or missing, contained an unknown key, or a value outside its allowed range
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Caller is not an agency, or the sub-account does not belong to it
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Sub-account not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /subaccounts/{subAccountUid}/menu-visibility:
    put:
      summary: Set SubAccount Menu Visibility
      security:
        - ApiKeyAuth: []
      description: |
        Hides specific side-nav pages and settings-nav items from the named
        sub-account, even when its plan otherwise grants access. Each bucket is a
        map of pageId/itemId to a boolean; sending a bucket replaces it whole, so
        include every key you want to keep as well as the ones you are changing.

        Send menuVisibility as null to reset to default-all-visible. Send empty
        bucket objects to clear toggles while keeping the field present.

        Requires the team_management area at edit level. Reachable through a
        sub-account grant at editor/admin role.
      tags:
        - SubAccounts
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: subAccountUid
          in: path
          required: true
          description: The sub-account's user id.
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - menuVisibility
              properties:
                menuVisibility:
                  type: ['object', 'null']
                  properties:
                    side_nav:
                      type: object
                      additionalProperties:
                        type: boolean
                      description: Up to 50 entries, keyed by side-nav page id.
                    settings_nav:
                      type: object
                      additionalProperties:
                        type: boolean
                      description: Up to 50 entries, keyed by settings-nav item id.
            examples:
              hide_pages:
                summary: Hide two side-nav pages
                value:
                  menuVisibility:
                    side_nav:
                      broadcasts: false
                      analytics: false
      responses:
        '200':
          description: Menu visibility updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  data:
                    type: object
                    properties:
                      subAccountUid:
                        type: string
                      menuVisibility:
                        type: ['object', 'null']
                        properties:
                          side_nav:
                            type: object
                            additionalProperties:
                              type: boolean
                          settings_nav:
                            type: object
                            additionalProperties:
                              type: boolean
        '400':
          description: menuVisibility (or one of its buckets) was malformed, or the target user is not an agency sub-account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Caller is not an agency, or the sub-account does not belong to it
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Sub-account not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /subaccounts/{subAccountUid}/notifications:
    put:
      summary: Set SubAccount Notifications
      security:
        - ApiKeyAuth: []
      description: |
        Replaces the named sub-account's notifications struct wholesale — the
        client submits a full snapshot on every save, so this is not a merge.
        Send notifications as null to clear all preferences.

        Each entry in settings is keyed by a notification category (for example
        campaign_status, credit_alerts, appointment) and may set enabled, up to 3
        channels from email, in_app, webhook, and recipients
        (send_to_sub_account / send_to_agency booleans). An unknown category, or
        an unknown channel, is rejected.

        Requires the team_management area at edit level. Reachable through a
        sub-account grant at editor/admin role.
      tags:
        - SubAccounts
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: subAccountUid
          in: path
          required: true
          description: The sub-account's user id.
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - notifications
              properties:
                notifications:
                  type: ['object', 'null']
                  properties:
                    settings:
                      type: object
                      additionalProperties:
                        type: object
                        properties:
                          enabled:
                            type: boolean
                          channels:
                            type: array
                            maxItems: 3
                            items:
                              type: string
                              enum: [email, in_app, webhook]
                          recipients:
                            type: object
                            properties:
                              send_to_sub_account:
                                type: boolean
                              send_to_agency:
                                type: boolean
            examples:
              mute_credit_alerts_for_client:
                summary: Turn off credit alerts for the sub-account, keep them for the agency
                value:
                  notifications:
                    settings:
                      credit_alerts:
                        enabled: true
                        channels: [email]
                        recipients:
                          send_to_sub_account: false
                          send_to_agency: true
      responses:
        '200':
          description: Notifications updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  data:
                    type: object
                    properties:
                      subAccountUid:
                        type: string
        '400':
          description: notifications was malformed, contained an unknown category or channel, or the target user is not an agency sub-account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Caller is not an agency, or the sub-account does not belong to it
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Sub-account not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /subaccounts/{subAccountUid}/chats:
    get:
      summary: Get SubAccount Chats
      security:
        - ApiKeyAuth: []
      description: |
        Lists contacts belonging to one sub-account, each with a preview of its
        last message, for an agency's chat-monitoring view. Ordered by last
        activity, most recent first.

        Requires the team_management area at view level. Reachable through a
        sub-account grant at any role (viewer included).
      tags:
        - SubAccounts
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: subAccountUid
          in: path
          required: true
          description: The sub-account's user id.
          schema:
            type: string
        - name: pageSize
          in: query
          required: false
          description: Contacts per page (default 25, max 50).
          schema:
            type: integer
            minimum: 1
            maximum: 50
            default: 25
        - name: lastActivityAt
          in: query
          required: false
          description: Pagination cursor from a previous response's lastActivityAt.
          schema:
            type: string
        - name: searchQuery
          in: query
          required: false
          description: Filter contacts by name or phone number, case-insensitive.
          schema:
            type: string
      responses:
        '200':
          description: Chats retrieved successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  data:
                    $ref: '#/components/schemas/SubAccountChatsData'
        '403':
          description: Caller is not an agency, or the sub-account does not belong to it
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Sub-account not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /subaccounts/{subAccountUid}/chats/{contactId}/messages:
    get:
      summary: Get SubAccount Chat Messages
      security:
        - ApiKeyAuth: []
      description: |
        Paginated conversation history for one contact of a sub-account, newest
        first, for an agency's chat-monitoring view.

        Requires the team_management area at view level. Reachable through a
        sub-account grant at any role (viewer included).
      tags:
        - SubAccounts
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: subAccountUid
          in: path
          required: true
          description: The sub-account's user id.
          schema:
            type: string
        - name: contactId
          in: path
          required: true
          description: The contact whose messages to fetch.
          schema:
            type: string
        - name: pageSize
          in: query
          required: false
          description: Messages per page (default 30, max 100).
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 30
        - name: beforeTimestamp
          in: query
          required: false
          description: Pagination cursor from a previous response's oldestTimestamp — fetches messages older than this.
          schema:
            type: string
      responses:
        '200':
          description: Messages retrieved successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  data:
                    $ref: '#/components/schemas/SubAccountChatMessagesData'
        '403':
          description: Caller is not an agency, or the sub-account does not belong to it
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Sub-account or contact not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /agencies/usage/transactions:
    get:
      summary: Get Agency Credit Transactions
      security:
        - ApiKeyAuth: []
      description: |
        Paginated credit-purchase history for the agency's dashboard: one row per
        sub-account credit purchase or subscription renewal, newest first, plus a
        revenue summary for the returned page.

        Requires Agency or Dev role. Owner-only for now — team members get a 403.
      tags:
        - Agency
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: limitCount
          in: query
          required: false
          description: Max transactions to return (1-100, default 20).
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
        - name: startAfterTimestamp
          in: query
          required: false
          description: Pagination cursor — an ISO 8601 created_at from a previous response's last row.
          schema:
            type: string
        - name: subAccountId
          in: query
          required: false
          description: Only return transactions made by this sub-account.
          schema:
            type: string
      responses:
        '200':
          description: Transactions retrieved successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  data:
                    $ref: '#/components/schemas/AgencyCreditTransactionsData'
        '403':
          description: Caller is not an agency or dev account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /whatsapp-templates/campaign/{campaignId}:
    post:
      summary: Create Campaign Opening Template
      security:
        - ApiKeyAuth: []
      description: |
        Creates the campaign's WhatsApp opening template and submits it (plus
        the campaign's follow-up drafts, when any exist) for approval. This
        is the dedicated campaign-scoped create path — distinct from
        POST /whatsapp-templates, which creates a template from a
        campaign_id in the body and does not submit follow-ups.

        A campaign that doesn't use any WhatsApp channel skips submission
        entirely (campaign_status "not_applicable", template_sid null). A
        campaign whose provider can't be submitted to yet (no usable
        credentials, or the template doesn't require approval) is
        auto-approved locally instead of failing.
      tags:
        - Campaigns
        - WhatsApp Templates
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
        - name: campaignId
          in: path
          required: true
          description: ID of the campaign
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, variables, language, body]
              properties:
                name:
                  type: string
                variables:
                  type: array
                  items:
                    type: string
                language:
                  type: string
                  description: Template language code (for example en, es, de, pt_BR, zh_CN).
                body:
                  type: string
                  description: Template body text, up to 1024 characters. Alternative variable syntaxes ({var}, [var]) are normalized to {{var}}.
            examples:
              create:
                value:
                  name: booking_reminder
                  variables: [first_name, appointment_time]
                  language: en
                  body: "Hi {{first_name}}, just a reminder about your appointment at {{appointment_time}}."
      responses:
        '201':
          description: Template created (or the campaign was auto-approved without submission)
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  campaign_status:
                    type: string
                    examples: ["pending", "approved", "not_applicable"]
                  template_sid:
                    type: ['string', 'null']
                  message:
                    type: string
        '400':
          description: Missing required fields, or the body exceeds 1024 characters
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error_code:
                    type: integer
                    examples: [400]
                  error:
                    type: string
        '403':
          description: Campaign belongs to another account, or the account plan does not include WhatsApp templates
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error_code:
                    type: integer
                    examples: [403]
                  error:
                    type: string
    put:
      summary: Update Campaign Opening Template
      security:
        - ApiKeyAuth: []
      description: |
        Updates a campaign's LIVE WhatsApp opening template and re-submits it
        for approval, managing the previous-version bookkeeping on both the
        live and try-out bot slots. This is NOT equivalent to
        PUT /whatsapp-templates/{templateId}, which edits a draft or
        rejected template doc with no provider submission at all.
      tags:
        - Campaigns
        - WhatsApp Templates
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
        - name: campaignId
          in: path
          required: true
          description: ID of the campaign
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, variables, language, body]
              properties:
                name:
                  type: string
                variables:
                  type: array
                  items:
                    type: string
                language:
                  type: string
                body:
                  type: string
      responses:
        '200':
          description: Template updated and re-submitted
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  campaign_status:
                    type: string
                  template_sid:
                    type: ['string', 'null']
                  message:
                    type: string
        '400':
          description: |
            Missing required fields, the account's Meta lane doesn't have
            templates enabled, or the campaign has no existing template to
            update
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error_code:
                    type: integer
                    examples: [400]
                  error:
                    type: string
        '403':
          description: Campaign belongs to another account, Twilio credentials are missing, or the account plan does not include WhatsApp templates
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error_code:
                    type: integer
                    examples: [403]
                  error:
                    type: string
  /whatsapp-templates/campaign/{campaignId}/generate-async:
    post:
      summary: Start Async Follow-up Template Generation
      security:
        - ApiKeyAuth: []
      description: |
        Kicks off background AI generation of the campaign's follow-up
        templates: pre-pays credits (skipped for a BYOK account), flips the
        campaign into a processing state, and returns immediately while a
        background worker does the generation. Requires the account's
        automatic_follow_ups feature.
      tags:
        - Campaigns
        - WhatsApp Templates
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
        - name: campaignId
          in: path
          required: true
          description: ID of the campaign
          schema:
            type: string
      responses:
        '202':
          description: Generation job started
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  data:
                    type: object
                    properties:
                      result:
                        type: string
                        examples: ["success"]
                      message:
                        type: string
        '400':
          description: Insufficient credits, or the account plan does not include the automatic follow-ups feature
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error_code:
                    type: integer
                    examples: [400]
                  error:
                    type: string
        '403':
          description: Campaign belongs to another account
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error_code:
                    type: integer
                    examples: [403]
                  error:
                    type: string
  /whatsapp-templates/campaign/{campaignId}/generate-followups:
    post:
      summary: Generate All Campaign Follow-up Templates
      security:
        - ApiKeyAuth: []
      description: |
        Synchronously generates all follow-up templates for a campaign that
        was created before this feature existed (or needs them regenerated).
        Unlike the async endpoint above, this call does the work inline and
        returns once every template has been created. Requires the
        account's automatic_follow_ups feature.
      tags:
        - Campaigns
        - WhatsApp Templates
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
        - name: campaignId
          in: path
          required: true
          description: ID of the campaign
          schema:
            type: string
      responses:
        '200':
          description: Follow-up templates generated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  data:
                    type: object
                    properties:
                      result:
                        type: string
                        examples: ["success"]
                      message:
                        type: string
                      templatesGenerated:
                        type: integer
        '400':
          description: Insufficient credits, or the account plan does not include the automatic follow-ups feature
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error_code:
                    type: integer
                    examples: [400]
                  error:
                    type: string
        '403':
          description: Campaign belongs to another account
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error_code:
                    type: integer
                    examples: [403]
                  error:
                    type: string
  /whatsapp-templates/agent/{agentId}/generate-followups:
    post:
      summary: Generate All Agent Follow-up Templates
      security:
        - ApiKeyAuth: []
      description: |
        Agent-scoped twin of Generate All Campaign Follow-up Templates: for
        an agent-native account (whose campaigns are read-only or
        non-existent), resolves the campaign attached to the given agent
        and runs the identical generation. When the agent has no attached
        campaign at all — the normal case for a newly created agent — the
        templates are generated and stored directly on the agent instead,
        and the response reports target "agent" with campaign_id null.
      tags:
        - Campaigns
        - WhatsApp Templates
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
        - name: agentId
          in: path
          required: true
          description: ID of the agent
          schema:
            type: string
      responses:
        '200':
          description: Follow-up templates generated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  agent_id:
                    type: string
                  campaign_id:
                    type: ['string', 'null']
                  target:
                    type: string
                    enum: [agent, campaign]
                  data:
                    type: object
                    properties:
                      result:
                        type: string
                        examples: ["success"]
                      message:
                        type: string
                      templatesGenerated:
                        type: integer
        '400':
          description: Insufficient credits, or the account plan does not include the automatic follow-ups feature
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Agent not found, or belongs to another account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /whatsapp-templates/{templateId}/bulk-send:
    post:
      summary: Bulk Send WhatsApp Template
      security:
        - ApiKeyAuth: []
      description: |
        Sends a WhatsApp template message to a batch of contacts (maximum
        5000 per call), resolving any AI-powered smart variables per contact
        and pre-paying credits before each write. A team member with
        restricted contact visibility is refused outright if ANY of the
        listed contacts falls outside their scope, rather than sending to
        the subset they can see.
      tags:
        - WhatsApp Templates
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
        - name: templateId
          in: path
          required: true
          description: ID of the template to send
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [contactIds]
              properties:
                contactIds:
                  type: array
                  description: Contact IDs to send to. Maximum 5000 per call.
                  items:
                    type: string
      responses:
        '200':
          description: Send completed
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  data:
                    type: object
                    properties:
                      sent:
                        type: integer
                      failed:
                        type: integer
                      total:
                        type: integer
        '400':
          description: Missing/empty contactIds, contactIds exceeds 5000, or missing templateId
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error_code:
                    type: integer
                    examples: [400]
                  error:
                    type: string
        '403':
          description: The account plan does not include WhatsApp templates
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: One of the listed contacts is outside the caller's team visibility scope
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /whatsapp-templates/{templateId}/send-to-contact:
    post:
      summary: Send Standalone WhatsApp Template to Contact
      security:
        - ApiKeyAuth: []
      description: |
        Sends a single WhatsApp template message to one contact, outside a
        campaign flow. Distinct from POST /whatsapp-templates/send (which
        sends by name/body directly): this endpoint sends an EXISTING
        template document by ID, deducts credits, and refuses a template
        that was created on a different provider lane than the account
        currently sends from (for example a Twilio-created template cannot
        be delivered from a Meta number).
      tags:
        - WhatsApp Templates
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
        - name: templateId
          in: path
          required: true
          description: ID of the template to send
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [contactId]
              properties:
                contactId:
                  type: string
      responses:
        '200':
          description: Template sent
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  data:
                    type: string
                    examples: ["WhatsApp template message sent successfully"]
        '400':
          description: Missing contactId or templateId, or insufficient credits
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error_code:
                    type: integer
                    examples: [400]
                  error:
                    type: string
        '403':
          description: |
            Contact belongs to another account, Twilio credentials are
            missing for the account, or the account plan does not include
            WhatsApp templates
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error_code:
                    type: integer
                    examples: [403]
                  error:
                    type: string
        '404':
          description: Contact is outside the caller's team visibility scope
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /whatsapp-templates/{templateId}/estimate-bulk-cost:
    post:
      summary: Estimate Bulk WhatsApp Template Cost
      security:
        - ApiKeyAuth: []
      description: |
        Read-only preview of what sending this template to an explicit
        contact set will cost, in credits, broken down by destination
        country. Nothing is sent and no credits move. Contact IDs that are
        missing, not owned by the caller, or hold no phone number are
        skipped rather than priced, and counted in skippedContacts so the
        UI can show the estimate covers fewer contacts than were selected.
        Maximum 500 contact IDs per call.
      tags:
        - WhatsApp Templates
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
        - name: templateId
          in: path
          required: true
          description: ID of the template
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [contactIds]
              properties:
                contactIds:
                  type: array
                  description: Contact IDs to price. Maximum 500 per call.
                  items:
                    type: string
      responses:
        '200':
          description: Estimate computed
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  data:
                    type: object
                    properties:
                      countries:
                        type: array
                        items:
                          type: object
                          properties:
                            countryCode:
                              type: string
                            name:
                              type: string
                            iso:
                              type: string
                            flag:
                              type: string
                            contactCount:
                              type: integer
                            costPerContact:
                              type: number
                            subtotal:
                              type: number
                      totalContacts:
                        type: integer
                      totalTemplateCost:
                        type: number
                      templateCategory:
                        type: string
                      skippedContacts:
                        type: integer
                        description: How many requested contact IDs were skipped (missing, not owned by the caller, or with no phone number).
        '400':
          description: Missing/empty contactIds, or contactIds exceeds 500
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The account plan does not include WhatsApp templates
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /whatsapp-templates/provider:
    get:
      summary: Get WhatsApp Template Provider
      security:
        - ApiKeyAuth: []
      description: |
        Reports which provider lane the account's WhatsApp templates
        actually submit through: Twilio, or a Meta lane (own Meta app,
        or embedded signup). On the Meta lane, templates_enabled tells you
        whether template submission is actually usable yet; on Twilio it is
        always true (credential problems only surface at submit time).
      tags:
        - WhatsApp Templates
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
      responses:
        '200':
          description: Provider resolved
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  provider:
                    type: string
                    enum: [twilio, meta]
                  lane:
                    type: ['string', 'null']
                    enum: [meta_cloud_api, meta_embedded, null]
                  waba_id:
                    type: ['string', 'null']
                  templates_enabled:
                    type: boolean
        '403':
          description: The account plan does not include WhatsApp templates
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /whatsapp-templates/meta-sync:
    post:
      summary: Sync WhatsApp Templates from Meta
      security:
        - ApiKeyAuth: []
      description: |
        Reconciles the account's template library with the templates on its
        Meta WhatsApp Business Account: imports any template that was
        created outside DM Champ and refreshes approval statuses on the
        ones already in the library. A no-op on the Twilio lane, where
        Twilio templates are polled by a separate schedule instead.
      tags:
        - WhatsApp Templates
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
      responses:
        '200':
          description: Sync completed
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  provider:
                    type: string
                    enum: [twilio, meta]
                  imported:
                    type: integer
                    description: Templates found on the WABA with no library doc, and imported.
                  updated:
                    type: integer
                    description: Library docs whose status/provider fields were refreshed from Meta.
                  total:
                    type: integer
                    description: Templates in the account's library after the sync.
        '400':
          description: |
            The template was rejected by Meta, or an unsupported language
            code was used
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The account plan does not include WhatsApp templates
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /whatsapp-templates/profile:
    put:
      summary: Save WhatsApp Business Profile
      security:
        - ApiKeyAuth: []
      description: |
        Saves the WhatsApp Business profile (about, address, description,
        email, websites, business vertical, and logo) for a connected phone
        number. Routes through Twilio's Sender API for Twilio-managed
        senders (whose token often binds to a different WABA than the
        account's own Meta token knows about), or directly to the Meta Graph
        API for everyone else.
      tags:
        - WhatsApp Templates
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [phoneNumber]
              properties:
                phoneNumber:
                  type: string
                  description: The connected WhatsApp phone number this profile belongs to.
                about:
                  type: string
                address:
                  type: string
                description:
                  type: string
                email:
                  type: string
                profilePictureHandle:
                  type: string
                  description: A Meta resumable-upload file handle, as returned by the upload-picture endpoint. Meta-lane only.
                websites:
                  type: array
                  items:
                    type: string
                vertical:
                  type: string
                  description: Business category. Accepts either Meta's enum codes (for example PROF_SERVICES) or Twilio's human-readable labels.
            examples:
              save:
                value:
                  phoneNumber: "+14155551234"
                  about: "We help you grow your business."
                  vertical: PROF_SERVICES
                  websites: ["https://example.com"]
      responses:
        '200':
          description: Profile saved
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  data:
                    type: string
                    examples: ["WhatsApp Business profile updated successfully"]
        '400':
          description: |
            Missing phoneNumber, an invalid website URL or business vertical,
            or missing Twilio credentials on a Twilio-managed sender
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error_code:
                    type: integer
                    examples: [400]
                  error:
                    type: string
        '403':
          description: The account plan does not include WhatsApp templates
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: The phone number is not connected to this account, or was not found in the WhatsApp Business Account
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error_code:
                    type: integer
                    examples: [404]
                  error:
                    type: string
  /whatsapp-templates/profile/picture:
    post:
      summary: Upload WhatsApp Business Profile Picture
      security:
        - ApiKeyAuth: []
      description: |
        Downloads an image from the given fileUrl and uploads it to Meta's
        resumable-upload endpoint, returning the resulting file handle. Pass
        that handle as profilePictureHandle on Save WhatsApp Business
        Profile to actually apply it (Twilio-managed senders sync their logo
        automatically from the account's stored picture URL instead, so this
        endpoint is mainly for Meta-lane accounts).
      tags:
        - WhatsApp Templates
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [phoneNumber, fileUrl]
              properties:
                phoneNumber:
                  type: string
                fileUrl:
                  type: string
                  description: Publicly reachable URL of the image to upload.
      responses:
        '200':
          description: Picture uploaded
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  data:
                    type: string
                    description: The Meta resumable-upload file handle for the picture.
        '400':
          description: Missing phoneNumber or fileUrl, or the file could not be downloaded/uploaded
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error_code:
                    type: integer
                    examples: [400]
                  error:
                    type: string
        '403':
          description: The account plan does not include WhatsApp templates
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: The phone number is not connected to this account
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error_code:
                    type: integer
                    examples: [404]
                  error:
                    type: string
  /whatsapp-templates/sender-status/{phoneNumber}:
    get:
      summary: Get WhatsApp Sender Status
      security:
        - ApiKeyAuth: []
      description: |
        Polls Twilio for a WhatsApp sender's live status and refreshes it on
        the phone number record. When Twilio no longer recognizes the
        sender, the status is set to DELETED and the stored sender SID is
        cleared.
      tags:
        - WhatsApp Templates
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
        - name: phoneNumber
          in: path
          required: true
          description: The connected phone number to poll, as stored (used as the phone_numbers document ID).
          schema:
            type: string
      responses:
        '200':
          description: Status retrieved
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  data:
                    type: string
                    enum: [ONLINE, PENDING, DELETED]
        '400':
          description: Twilio credentials are not configured for the account
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error_code:
                    type: integer
                    examples: [400]
                  error:
                    type: string
        '404':
          description: No WhatsApp Business sender is on record for this phone number
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error_code:
                    type: integer
                    examples: [404]
                  error:
                    type: string
  /whatsapp-templates/messages/{contactId}/{messageId}/retry-template:
    post:
      summary: Retry Failed WhatsApp Template Message
      security:
        - ApiKeyAuth: []
      description: |
        Retries a failed WhatsApp TEMPLATE message: resets the contact's
        do-not-disturb flag and resends the EXISTING message, re-resolving
        content_sid/content_variables from the campaign's current template
        when the stored message doesn't carry them. No new message document
        is created and no additional credits are deducted.
      tags:
        - WhatsApp Templates
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
        - name: contactId
          in: path
          required: true
          description: ID of the contact the failed message belongs to.
          schema:
            type: string
        - name: messageId
          in: path
          required: true
          description: ID of the failed message to retry.
          schema:
            type: string
      responses:
        '200':
          description: Retry sent
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  data:
                    type: string
        '400':
          description: |
            The message is not a template message, is not in a failed
            state, or is missing the data needed to resubmit it
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error_code:
                    type: integer
                    examples: [400]
                  error:
                    type: string
        '403':
          description: The account plan does not include WhatsApp templates
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: |
            Contact or message not found, outside the caller's team
            visibility scope, or belongs to another account
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error_code:
                    type: integer
                    examples: [404]
                  error:
                    type: string
  /whatsapp-templates/messages/{contactId}/{messageId}/retry:
    post:
      summary: Retry Failed WhatsApp Message
      security:
        - ApiKeyAuth: []
      description: |
        Channel-agnostic retry for a failed, NON-template message: dispatches
        to the correct send helper based on the message's own channel field
        (for example WhatsApp Web). A template message must be retried via
        Retry Failed WhatsApp Template Message instead.
      tags:
        - WhatsApp Templates
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404."
          schema:
            type:
              - string
              - "null"
        - name: contactId
          in: path
          required: true
          description: ID of the contact the failed message belongs to.
          schema:
            type: string
        - name: messageId
          in: path
          required: true
          description: ID of the failed message to retry.
          schema:
            type: string
      responses:
        '200':
          description: Retry sent
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [true]
                  data:
                    type: string
        '400':
          description: |
            The message is not in a failed state, or is a template message
            (retry it via Retry Failed WhatsApp Template Message instead)
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error_code:
                    type: integer
                    examples: [400]
                  error:
                    type: string
        '403':
          description: The account plan does not include WhatsApp templates
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: |
            Contact or message not found, outside the caller's team
            visibility scope, or belongs to another account
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    examples: [false]
                  error_code:
                    type: integer
                    examples: [404]
                  error:
                    type: string
  /white-label-config:
    post:
      summary: Create White-Label Config
      security:
        - ApiKeyAuth: []
      description: |
        Creates a new white-label storefront config on the caller's account, or
        (with replace_config_id) re-keys an existing one onto a new domain in
        a single atomic operation.

        Agency and Dev accounts only. The first config is included with
        white-labeling; a second and third are a Champions Circle benefit
        unless the account carries a partner grant. A white_label_configs
        doc already existing for the domain (owned by anyone) fails with 409
        rather than being overwritten, to prevent domain squatting.
      tags:
        - White Label
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WhiteLabelConfigCreateInput'
            example:
              domain: app.myagency.com
              company_name: My Agency
              app_name: My Agency Portal
      responses:
        '200':
          description: The config was created (or the domain swap completed)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WhiteLabelConfigCreateResponse'
        '400':
          description: The domain is missing or invalid, the config cap is reached, or replace_config_id equals domain
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                success: false
                error: domain is not a valid hostname.
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: |
            The caller is not an Agency/Dev account, an additional config
            requires Champions Circle membership, a LeadFindr config requires
            Find Leads access, replace_config_id does not belong to the
            caller, or the caller may not edit white-label settings on this account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                success: false
                error: Only Agency accounts can create white-label configs.
        '404':
          description: replace_config_id does not match an existing config
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: The domain is already in use by a white-label config
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                success: false
                error: This domain is already in use.
    put:
      summary: Save White-Label Config
      security:
        - ApiKeyAuth: []
      description: |
        Updates one or more brand/content fields on an existing white-label
        config. Send only the fields you want to change.

        Agency and Dev accounts only. not-found and permission-denied are
        both reported as a generic 404 so a config id owned by another
        account cannot be enumerated.
      tags:
        - White Label
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WhiteLabelConfigSaveInput'
            example:
              config_id: app.myagency.com
              primary_color: "#25D366"
              app_name: My Agency Portal
      responses:
        '200':
          description: The config was updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WhiteLabelConfigSaveResponse'
        '400':
          description: Neither config_id nor product was supplied, or a field failed validation
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                success: false
                error: "Missing required field: config_id"
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The caller may not edit white-label settings on this account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: The config was not found, or is not owned by the caller
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                success: false
                error: White label config not found
    delete:
      summary: Delete White-Label Config
      security:
        - ApiKeyAuth: []
      description: |
        Removes one of the caller's white-label configs. Idempotent — calling
        this again after a successful delete returns 200 with an empty
        configId rather than an error.

        The account's PRIMARY config (the one users.white_label_config
        names) can only be removed once it is the LAST remaining config;
        removing any other config first is required.
      tags:
        - White Label
      parameters:
        - $ref: '#/components/parameters/ApiKeyParam'
        - name: sub_account_id
          in: query
          required: false
          description: "Optional. Agency API keys only: act on this managed client account (sub-account) instead of your own. The account must belong to your agency, otherwise the call returns 404. May also be sent as a sub_account_id field in the JSON body."
          schema:
            type:
              - string
              - "null"
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                config_id:
                  type: string
                  description: Which config to delete (its id is its domain). Omitted deletes the caller's PRIMARY config.
                  examples: ["app.myagency.com"]
                product:
                  type: string
                  enum: ["dmchamp", "leadfindr"]
                  description: Legacy fallback selector, ignored when config_id is supplied.
            example:
              config_id: app.myagency.com
      responses:
        '200':
          description: The config was removed, or there was nothing to remove
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WhiteLabelConfigDeleteResponse'
        '400':
          description: The primary config was targeted while other configs remain, or the product selector matches more than one config
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                success: false
                error: The primary white label config has to be removed last. Remove app-other.myagency.com first.
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The caller is not an Agency/Dev account, does not own the config, or may not edit white-label settings on this account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: config_id was supplied but does not match any config
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
webhooks:
  agencySubAccountAutoRecharge:
    post:
      summary: Agency Sub-Account Auto-Recharge Webhook
      description: |
        DM Champ sends this webhook to the agency's configured `autoRechargeWebhookUrl`
        when a sub-account's credit balance drops below the configured auto-recharge threshold.

        **Setup**: Configure the webhook URL via the `updateAgencyStripeSettings` callable function
        by passing the `autoRechargeWebhookUrl` parameter. This is an alternative to the
        Stripe-based auto-recharge flow — use it when you want to handle payment processing
        on your own server instead of through Stripe.

        **How it works**:
        1. A sub-account's credit balance drops below the `when_below` threshold
        2. DM Champ sends an HTTP POST to the agency's webhook URL with the payload below
        3. The agency's server processes payment (e.g., charges the customer via their own billing system)
        4. The agency calls `POST /subaccounts/credits` with a positive `amount` to grant credits

        **Important behavior**:
        - Credits are NOT granted automatically by the webhook — the agency must call back the API
        - A 5-minute cooldown is enforced per sub-account to prevent duplicate webhook calls
        - The webhook fires at most once per cooldown window, even if the balance drops further
        - Use the `idempotency_key` field to deduplicate in case of retries
        - DM Champ expects an HTTP 2xx response; non-2xx responses are logged as errors but do not block the sub-account

        **Callback endpoint**: `POST /subaccounts/credits` (see SubAccounts section)
      tags:
        - Agency
        - Webhooks
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookAutoRechargePayload'
            examples:
              withPricing:
                summary: Webhook payload with per-credit pricing configured
                value: {
                  "event": "agency_sub_account_auto_recharge",
                  "sub_account_id": "abc123def456",
                  "sub_account_email": "sub@example.com",
                  "sub_account_name": "John Doe",
                  "agency_id": "fHjQASIIzfUcrvuAvaYWdDd65Bu2",
                  "credits_requested": 500,
                  "current_balance": 42,
                  "threshold": 100,
                  "price_per_credit_cents": 10,
                  "price_per_credit_currency": "usd",
                  "total_amount_cents": 5000,
                  "timestamp": "2026-04-13T12:00:00.000Z",
                  "idempotency_key": "auto_recharge_abc123def456_1712836800000"
                }
              withoutPricing:
                summary: Webhook payload without per-credit pricing (agency calculates price)
                value: {
                  "event": "agency_sub_account_auto_recharge",
                  "sub_account_id": "abc123def456",
                  "sub_account_email": "sub@example.com",
                  "sub_account_name": "John Doe",
                  "agency_id": "fHjQASIIzfUcrvuAvaYWdDd65Bu2",
                  "credits_requested": 500,
                  "current_balance": 42,
                  "threshold": 100,
                  "timestamp": "2026-04-13T12:00:00.000Z",
                  "idempotency_key": "auto_recharge_abc123def456_1712836800000"
                }
      responses:
        '200':
          description: |
            The agency's server should return a 2xx status to acknowledge receipt.
            DM Champ does not inspect the response body.
