openapi: 3.1.0
info:
  title: callagent.pro Agent API
  version: "1.0.0"
  description: >
    Lets an external AI agent (a ChatGPT custom GPT Action, or any script)
    create and manage phone agents, SIP trunks/accounts, and read call
    history, transcripts, recordings and billing — on behalf of the user
    who issued the API key. Every response is scoped to that user's account.

    Auth: mint a key from the dashboard (Settings -> Agent API), then send
    it as `Authorization: Bearer cak_live_...` on every request below.
servers:
  # MUST be the www host. The apex 301-redirects, and a redirect on a POST
  # loses the body in most clients - writes would fail while reads looked fine.
  - url: https://www.callagent.pro
security:
  - bearerAuth: []
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: cak_live_...
  schemas:
    Agent:
      type: object
      properties:
        id: { type: integer }
        name: { type: string }
        agent_type: { type: string, enum: [prompt, flow] }
        prompt: { type: string }
        greeting_message: { type: string }
        company_information: { type: string }
        primary_language_id: { type: integer }
        default_voice_id: { type: integer }
        llm_server_id: { type: integer }
        asr_model_id: { type: integer }
        knowledge_base_id: { type: integer, nullable: true }
        status: { type: string, enum: [active, paused] }
        max_call_duration: { type: integer, minimum: 60, maximum: 7200, description: "SECONDS, not minutes; 60-7200" }
        forwarding_number: { type: string, nullable: true }
        flow: { type: object, description: "Visual flow graph {nodes, edges}; only for agent_type=flow" }
        temperature: { type: number, minimum: 0, maximum: 1 }
        speech_speed: { type: number, minimum: 0.5, maximum: 2 }
        vad_activation_threshold: { type: number, minimum: 0.1, maximum: 0.9 }
        vad_min_silence_duration: { type: number, minimum: 0.1, maximum: 1.5 }
        phrase_pause_ms: { type: integer, minimum: 0, maximum: 500 }
        max_waiting_time: { type: integer, minimum: 5, maximum: 120, description: seconds }
        answer_delay: { type: integer, minimum: 0, maximum: 10, description: seconds }
        enable_recording: { type: boolean }
        enable_call_forwarding: { type: boolean }
        enable_retry: { type: boolean }
        tts_tags: { type: boolean, description: "Let the model write [emotion] tags; engines that do not read them strip them" }
        email_notifications: { type: boolean }
        notification_email: { type: string, format: email, nullable: true }
        webhook_url: { type: string, format: uri, nullable: true, description: "Post-call webhook THIS account calls; not an endpoint served here" }
        tools: { type: string, nullable: true }
    Trunk:
      required: [provider, trunk_name, username, secret]
      type: object
      properties:
        provider: { type: string, description: "sipgate | sipload | easybell | dusnet | telnyx | voipms | sipstation | custom (use custom for a Fritz!Box / DSL line)" }
        trunk_name: { type: string, description: "REQUIRED, and the field is trunk_name - not name" }
        host: { type: string }
        port: { type: integer }
        username: { type: string }
        secret: { type: string }
        direction: { type: string, enum: [inbound, outbound, both] }
        inbound_assistant_id: { type: integer, nullable: true }
        is_default: { type: boolean }
    SipAccount:
      type: object
      properties:
        username: { type: string }
        password: { type: string }
        display_name: { type: string }
        direction: { type: string, enum: [inbound, outbound, both] }
        outgoing_trunk_id: { type: integer, nullable: true }
paths:
  /api/v1/meta:
    get:
      summary: Reference data to build an agent — languages, voices, ASR models, LLM servers, knowledge bases
      operationId: getMeta
      responses: { "200": { description: OK } }
  /api/v1/overview:
    get:
      summary: Dashboard stats — KPIs (calls, answer rate, avg duration, spend, each with its change), call-volume series, account counts
      operationId: getOverview
      responses: { "200": { description: OK } }

  /api/v1/agents:
    get:
      summary: List the account's phone agents
      operationId: listAgents
      responses: { "200": { description: OK } }
    post:
      summary: Create a phone agent
      description: >
        Every field is optional. Anything omitted is given a working default, so a
        near-empty body still returns 200 with a usable agent - the response then
        carries `defaults_applied`, listing which fields were filled in for you.
        Check it before assuming the agent is configured as asked.
      operationId: createAgent
      requestBody:
        content: { application/json: { schema: { $ref: '#/components/schemas/Agent' } } }
      responses: { "200": { description: Created } }
  /api/v1/agents/{id}:
    get:
      summary: Get one agent's full configuration
      operationId: getAgent
      parameters: [ { name: id, in: path, required: true, schema: { type: integer } } ]
      responses: { "200": { description: OK } }
    put:
      summary: Update an agent
      operationId: updateAgent
      parameters: [ { name: id, in: path, required: true, schema: { type: integer } } ]
      requestBody:
        content: { application/json: { schema: { $ref: '#/components/schemas/Agent' } } }
      responses: { "200": { description: OK } }
    delete:
      summary: Delete an agent
      operationId: deleteAgent
      parameters: [ { name: id, in: path, required: true, schema: { type: integer } } ]
      responses: { "200": { description: OK } }
  /api/v1/agents/{id}/duplicate:
    post:
      summary: Duplicate an agent (created paused)
      operationId: duplicateAgent
      parameters: [ { name: id, in: path, required: true, schema: { type: integer } } ]
      responses: { "200": { description: OK } }
  /api/v1/agents/{id}/test-call:
    post:
      summary: Place a real test call from this agent to a phone number
      operationId: testCallAgent
      parameters: [ { name: id, in: path, required: true, schema: { type: integer } } ]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [phone_number]
              properties:
                phone_number: { type: string, description: "Dialable number; digits with an optional leading +" }
                from_number: { type: string, nullable: true, description: "Caller ID to present; the provider decides whether to honour it" }
                context: { type: string, nullable: true, maxLength: 2000, description: "Briefing for THIS call only, appended to the agent prompt" }
      responses: { "200": { description: OK } }
  /api/v1/agents/{id}/test-chat:
    post:
      summary: >
        Talk to the agent as text (no phone call, no STT/TTS) — either one turn at a time
        with `session_id`/`message`, or a whole scripted conversation in one call via
        `messages`, which returns per-turn `first_word_s`/`total_s` latency plus a
        min/max/avg summary, for benchmarking an agent right after creating it.
      operationId: testChatAgent
      description: >
        The agent must be status=active. A paused agent answers 404 agent_not_found
        - including one just made by /duplicate, which creates agents paused.
      parameters: [ { name: id, in: path, required: true, schema: { type: integer } } ]
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                message: { type: string, description: "single-turn mode" }
                session_id: { type: string, description: "continue a session started by an earlier call" }
                messages:
                  type: array
                  items: { type: string }
                  maxItems: 20
                  description: benchmark mode — a whole scripted conversation, scored turn by turn
      responses: { "200": { description: OK } }
  /api/v1/numbers:
    get:
      summary: List phone numbers on the account
      operationId: listNumbers
      responses: { "200": { description: OK } }
  /api/v1/numbers/{id}:
    put:
      summary: Route a number to an agent or SIP extension
      description: "route_mode is REQUIRED. agent -> also send assistant_id; extension -> also send route_extension_id; none -> unroute."
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [route_mode]
              properties:
                route_mode: { type: string, enum: [agent, extension, none] }
                assistant_id: { type: integer, nullable: true }
                route_extension_id: { type: integer, nullable: true }
      operationId: updateNumber
      parameters: [ { name: id, in: path, required: true, schema: { type: integer } } ]
      responses: { "200": { description: OK } }
    delete:
      summary: Remove a number from the account
      operationId: deleteNumber
      parameters: [ { name: id, in: path, required: true, schema: { type: integer } } ]
      responses: { "200": { description: OK } }
  /api/v1/outbound-numbers:
    get:
      summary: Numbers available to place outbound calls from
      operationId: listOutboundNumbers
      responses: { "200": { description: OK } }
  /api/v1/trunks:
    get:
      summary: List SIP trunks with live registration state, status and call counts
      operationId: listTrunks
      responses: { "200": { description: OK } }
    post:
      summary: Create a SIP trunk — a provider account, or a local box such as a Fritz!Box/DSL line (provider "custom")
      operationId: createTrunk
      requestBody:
        content: { application/json: { schema: { $ref: '#/components/schemas/Trunk' } } }
      responses: { "200": { description: Created } }
  /api/v1/trunks/{id}:
    put:
      summary: Update a SIP trunk
      operationId: updateTrunk
      parameters: [ { name: id, in: path, required: true, schema: { type: integer } } ]
      requestBody:
        content: { application/json: { schema: { $ref: '#/components/schemas/Trunk' } } }
      responses: { "200": { description: OK } }
    delete:
      summary: Delete a SIP trunk
      operationId: deleteTrunk
      parameters: [ { name: id, in: path, required: true, schema: { type: integer } } ]
      responses: { "200": { description: OK } }
  /api/v1/trunks/{id}/retry:
    post:
      summary: Force the trunk to re-attempt SIP registration now
      operationId: retryTrunk
      parameters: [ { name: id, in: path, required: true, schema: { type: integer } } ]
      responses: { "200": { description: OK } }
  /api/v1/sip-accounts:
    get:
      summary: List SIP accounts/extensions with registration, device, ping and call counters
      operationId: listSipAccounts
      responses: { "200": { description: OK } }
    post:
      summary: Create a SIP account/extension (e.g. for a Fritz!Box or softphone to register against)
      operationId: createSipAccount
      requestBody:
        content: { application/json: { schema: { $ref: '#/components/schemas/SipAccount' } } }
      responses: { "200": { description: Created } }
  /api/v1/sip-accounts/{id}:
    put:
      summary: Update a SIP account
      operationId: updateSipAccount
      parameters: [ { name: id, in: path, required: true, schema: { type: integer } } ]
      responses: { "200": { description: OK } }
    delete:
      summary: Delete a SIP account
      operationId: deleteSipAccount
      parameters: [ { name: id, in: path, required: true, schema: { type: integer } } ]
      responses: { "200": { description: OK } }
  /api/v1/sip-accounts/{id}/password:
    get:
      summary: Reveal a SIP account's password (rate-limited)
      operationId: revealSipAccountPassword
      parameters: [ { name: id, in: path, required: true, schema: { type: integer } } ]
      responses: { "200": { description: OK } }
  /api/v1/calls:
    get:
      summary: Recent call history
      operationId: listCalls
      responses: { "200": { description: OK } }
  /api/v1/calls/{id}:
    get:
      summary: Call detail — transcript, captured variables, recording URL, status
      operationId: getCall
      parameters: [ { name: id, in: path, required: true, schema: { type: integer } } ]
      responses: { "200": { description: OK } }
  /api/v1/live:
    get:
      summary: Calls in progress right now
      operationId: listLiveCalls
      responses: { "200": { description: OK } }
  /api/v1/knowledge:
    get:
      summary: List knowledge bases on the account, including ones this platform's agents created
      operationId: listKnowledge
      responses: { "200": { description: OK } }
    post:
      summary: Create a knowledge base an agent can be pointed at
      operationId: createKnowledgeBase
      responses: { "200": { description: Created } }
  /api/v1/knowledge/{id}:
    get:
      summary: Get a knowledge base
      operationId: getKnowledgeBase
      parameters: [ { name: id, in: path, required: true, schema: { type: integer } } ]
      responses: { "200": { description: OK } }
    post:
      summary: Update a knowledge base
      operationId: updateKnowledgeBase
      parameters: [ { name: id, in: path, required: true, schema: { type: integer } } ]
      responses: { "200": { description: OK } }
    delete:
      summary: Delete a knowledge base
      operationId: deleteKnowledgeBase
      parameters: [ { name: id, in: path, required: true, schema: { type: integer } } ]
      responses: { "200": { description: OK } }
  /api/v1/billing:
    get:
      summary: Account balance, plan, this month's usage, recent invoices
      operationId: getBilling
      responses: { "200": { description: OK } }
  /api/v1/billing/topup:
    post:
      summary: Start a prepay top-up (returns a PayPal approval_url for the human to complete)
      operationId: topupBilling
      requestBody:
        content: { application/json: { schema: { type: object, properties: { amount: { type: integer, description: "EUR, 5-500, step of 5" } }, required: [amount] } } }
      responses: { "200": { description: OK } }
  /api/v1/billing/plan:
    post:
      summary: Switch subscription plan (free plans switch immediately; paid plans return an approval_url)
      operationId: setBillingPlan
      requestBody:
        content: { application/json: { schema: { type: object, properties: { plan_id: { type: integer } }, required: [plan_id] } } }
      responses: { "200": { description: OK } }

  /api/v1/do-not-call:
    get:
      summary: List do-not-call numbers — check before dialling
      operationId: listDoNotCall
      responses: { "200": { description: OK } }
    post:
      summary: Add a number to the do-not-call list
      operationId: addDoNotCall
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [phone_number]
              properties:
                phone_number: { type: string }
                reason: { type: string, nullable: true }
      responses: { "200": { description: OK } }
  /api/v1/do-not-call/{id}:
    put:
      summary: Update a do-not-call entry
      operationId: updateDoNotCall
      parameters: [ { name: id, in: path, required: true, schema: { type: integer } } ]
      responses: { "200": { description: OK } }
    delete:
      summary: Remove a do-not-call entry
      operationId: deleteDoNotCall
      parameters: [ { name: id, in: path, required: true, schema: { type: integer } } ]
      responses: { "200": { description: OK } }
  /api/v1/contacts:
    get:
      summary: List contacts with their lists and last-called times
      operationId: listContacts
      responses: { "200": { description: OK } }
    post:
      summary: Create a contact
      operationId: createContact
      responses: { "200": { description: OK } }
  /api/v1/contacts/{id}:
    put:
      summary: Update a contact
      operationId: updateContact
      parameters: [ { name: id, in: path, required: true, schema: { type: integer } } ]
      responses: { "200": { description: OK } }
  /api/v1/contact-lists:
    post:
      summary: Create a contact list (campaigns draw from these)
      operationId: createContactList
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name: { type: string }
                notes: { type: string, nullable: true }
      responses: { "200": { description: OK } }
  /api/v1/contact-lists/{id}:
    get:
      summary: Contacts in one list
      operationId: getContactList
      parameters: [ { name: id, in: path, required: true, schema: { type: integer } } ]
      responses: { "200": { description: OK } }
    put:
      summary: Rename/update a contact list
      operationId: updateContactList
      parameters: [ { name: id, in: path, required: true, schema: { type: integer } } ]
      responses: { "200": { description: OK } }
    delete:
      summary: Delete a contact list
      operationId: deleteContactList
      parameters: [ { name: id, in: path, required: true, schema: { type: integer } } ]
      responses: { "200": { description: OK } }
  /api/v1/team:
    get:
      summary: Account members and their roles
      operationId: listTeam
      responses: { "200": { description: OK } }
    post:
      summary: Invite a member (an API key can therefore add a user — treat it as account-admin)
      operationId: inviteTeamMember
      responses: { "200": { description: OK } }
  /api/v1/team/{id}:
    put:
      summary: Change a member's role
      operationId: updateTeamMember
      parameters: [ { name: id, in: path, required: true, schema: { type: integer } } ]
      responses: { "200": { description: OK } }
    delete:
      summary: Remove a member
      operationId: removeTeamMember
      parameters: [ { name: id, in: path, required: true, schema: { type: integer } } ]
      responses: { "200": { description: OK } }
  /api/v1/team/{id}/resend:
    post:
      summary: Resend an invitation
      operationId: resendTeamInvite
      parameters: [ { name: id, in: path, required: true, schema: { type: integer } } ]
      responses: { "200": { description: OK } }
  /api/v1/profile:
    get:
      summary: The key owner's profile and preferences
      operationId: getProfile
      responses: { "200": { description: OK } }
    put:
      summary: Update profile (name, language, timezone, notification preferences)
      operationId: updateProfile
      responses: { "200": { description: OK } }