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

# Create or replace an assistant

> `id` is optional — without it, the slug is derived from `config.name`.
A save is validated three ways before it lands, so a bad config fails
here rather than mid-call:

1. **Catalog slots** — the engine/model/transcriber/voice combination
   must exist in the model catalog (a mismatch is a call that dies on
   the worker with an error nobody connects back to this save).
2. **Custom tool URLs** — https, no credentials in the URL, no private
   or link-local destinations. The worker re-validates at call time
   because DNS can change in between.
3. The name must be non-empty and the slug must match
   `^[a-z0-9][a-z0-9-]{1,60}$`.

On success the config snapshot is republished to the cells' GCS bucket
and **awaited**, so `configPublished` tells the console whether the save
actually reached them. The config itself is already durable regardless;
cells otherwise converge within the snapshot TTL.




## OpenAPI

````yaml /openapi/api-public-v1.yaml post /console/assistants
openapi: 3.0.3
info:
  title: Vodex API (v1)
  description: >
    The **public Vodex `/v1` REST API** — everything a customer's own systems

    integrate with: assistants, telephony (numbers, trunks, carrier

    integrations), calls and recordings, batch dialing, the SMS channel, and

    outbound webhooks.


    This is deliberately a **subset**. Account and console-only routes

    (`/v1/auth/*`, `/v1/api-keys`, `/v1/usage`), platform provisioning

    (`/v1/platform/*`), Vodex support operations, and endpoints other systems

    call inward (carrier delivery receipts, the agent worker, cron) are not

    part of the published surface and are not documented here.


    The **data plane** (LiveKit SFU + SIP + the agent worker) is not an HTTP

    API and is not described here: a call reaches the worker over LiveKit, with

    its config carried in participant metadata or read from the config snapshot

    this API publishes.


    ## Authentication


    | Credential | Sent as | Scope |

    |---|---|---|

    | **Tenant API key** | `Authorization: Bearer vdx_live_…` | One customer,
    full access — the credential for everything in this document |


    Mint an API key in the console (**Settings → API keys**). The plaintext is

    shown **once**, by the call that creates it, and no endpoint reads one back

    — a lost key is replaced, not recovered.


    Every route here also answers a signed-in console session, because the

    console is built on this same API. That is **not** an integration

    credential and is deliberately not described as one: it is the console's

    own cookie, it carries a role an API key never has, and it is free to

    change. Integrate with `vdx_live_`.


    ## Customers and tenants


    One thing, two words, and they are not interchangeable:


    - **Customer** is the product word — what a human calls the account, and
      what the console shows you.
    - **Tenant** is the same thing's identity on the wire and in storage:
      `tenantId` on every response.

    **A *workspace* is a third thing, and not a synonym for either.** Tenancy

    is mapped at the customer level deliberately: integrations (carrier, SMS,

    provider credentials) live above the workspaces that use them, so mapping

    workspaces to tenants would fragment one customer's telephony across

    tenants that then have to share credentials. A workspace is therefore a

    **label inside the tenant** (`externalWorkspaceRef`) — for filtering and

    attribution, never isolation. Anything that must not be shared between one

    customer's workspaces needs its own tenant.


    ## Tenancy


    There is **no tenant parameter**. Every tenant-scoped route derives its

    tenant from the credential and binds it for the whole request, so a caller

    cannot ask for another tenant's data by changing a query string. An id that

    belongs to somebody else answers `404`, never `403` — "no such call" is the

    only thing worth confirming.


    ## Response shapes


    Resources are returned **flat**, at the top level (`{ "id": "…", … }`) —

    there is no envelope object. Collections vary by age of the endpoint:


    - Newer lists are `{ "<plural>": [ … ], "total": n, "limit": n, "offset": n
    }`
      (`calls`, `batches`, `rows`, `attempts`, `messages`).
    - Older lists return a **bare JSON array** (`/v1/phone-numbers`,
      `/v1/sip-trunks`, `/v1/integrations`, `/v1/carriers`,
      `/v1/console/assistants`, `/v1/sms/configurations`).

    Deletes and side-effecting no-content operations return `{ "ok": true }`.


    ## Errors


    ```json

    { "error": "human-readable message" }

    ```


    A flat string, not a coded object — HTTP status is the machine signal.

    The one exception is quota exhaustion (`402`), which adds a stable

    `code: "quota_exhausted"` plus the counters the console renders.


    ## Pagination


    `?limit=` and `?offset=`, with `total` in the body. Defaults and ceilings

    differ per endpoint (calls 25/100, batch rows 50/500, attempts 100/500) and

    are stated on each operation.


    ## Conventions


    - **Phone numbers** are E.164 strings (`+14155551234`); `+91…` numbers are
      served from Mumbai, `+1…` from us-central1.
    - **Money** is a JSON number of **USD** (`costUsd`), not a decimal string.

    - **Timestamps** are ISO‑8601 UTC strings, except the calls list, which
      reports `mtime` / `startedAt` as epoch **milliseconds** and durations as
      **milliseconds**.
    - **Ids** are unprefixed: batches, rows, attempts and SMS rows are UUIDs;
      a call id is its LiveKit room name (`call-…`, or `console-…` for web
      calls); a phone number's id is `num-<digits>`; a trunk's is `trunk-…`.
    - **Secrets** (`vdx_live_…`, `whsec_…`) are returned exactly once, by the
      call that mints them. No endpoint reads one back — carrier and provider
      credentials are stored in Secret Manager and echoed only as a
      non-reversible fingerprint.
    - **Idempotency**: `POST /v1/sms` and `POST /v1/batches` **require** an
      `Idempotency-Key` header. Only successful (2xx) responses are replayed;
      a failed attempt releases the key so a corrected retry can reuse it.
  version: 1.0.0
  contact:
    name: Vodex Platform
    url: https://vodex.ai
  license:
    name: Proprietary
    url: https://vodex.ai/terms
servers:
  - url: https://apiv2.vodex.ai/v1
    description: Production
security:
  - ApiKeyBearer: []
tags:
  - name: Assistants
    description: Assistant configurations (the agent's engine, prompt, slots, tools).
  - name: Console
    description: Reference data (model catalog, voices) and the web-call token.
  - name: Usage
    description: Minute usage against the plan.
  - name: Calls
    description: Place calls, read the call log, fetch traces and recordings.
  - name: Phone Numbers
    description: Number inventory and assistant mapping.
  - name: SIP Trunks
    description: Customer-owned (BYO) SIP trunks.
  - name: Carriers
    description: Carrier registry — what can be connected, and how.
  - name: Integrations
    description: Connected carrier accounts and their numbers.
  - name: Batches
    description: Batch dialing — lists in, calls out.
  - name: Number Rotation
    description: Per-number spacing, daily caps, and cooldown.
  - name: Callbacks
    description: >-
      "Call me Tuesday afternoon" — appointments booked mid-call, and dialed
      later.
  - name: SMS
    description: SMS configurations, credentials, preview, send, and message log.
  - name: Webhooks
    description: Outbound webhook configuration and delivery records.
  - name: Tenants
    description: Recording retention.
paths:
  /console/assistants:
    post:
      tags:
        - Assistants
      summary: Create or replace an assistant
      description: |
        `id` is optional — without it, the slug is derived from `config.name`.
        A save is validated three ways before it lands, so a bad config fails
        here rather than mid-call:

        1. **Catalog slots** — the engine/model/transcriber/voice combination
           must exist in the model catalog (a mismatch is a call that dies on
           the worker with an error nobody connects back to this save).
        2. **Custom tool URLs** — https, no credentials in the URL, no private
           or link-local destinations. The worker re-validates at call time
           because DNS can change in between.
        3. The name must be non-empty and the slug must match
           `^[a-z0-9][a-z0-9-]{1,60}$`.

        On success the config snapshot is republished to the cells' GCS bucket
        and **awaited**, so `configPublished` tells the console whether the save
        actually reached them. The config itself is already durable regardless;
        cells otherwise converge within the snapshot TTL.
      operationId: upsertAssistant
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - config
              properties:
                id:
                  type: string
                  pattern: ^[a-z0-9][a-z0-9-]{1,60}$
                config:
                  $ref: '#/components/schemas/AssistantConfig'
      responses:
        '200':
          description: Saved.
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                  configPublished:
                    type: boolean
        '400':
          description: |
            Validation failed — a missing name, a bad slug, a catalog slot
            mismatch, or an unsafe tool URL. Every rejection is also logged with
            the offending value, so "which field?" is answerable from the server
            log alone.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
components:
  schemas:
    AssistantConfig:
      type: object
      description: >-
        The AssistantConfig (FR‑3). `packages/schemas/assistant.schema.json` is
        the source of truth and codegens the TS and Pydantic types; this is a
        faithful summary, and unknown keys are preserved on save.
      required:
        - name
        - engine
        - systemPrompt
      properties:
        id:
          type: string
        name:
          type: string
          maxLength: 80
        engine:
          type: string
          enum:
            - cascade
            - s2s
          description: Engine A (cascade STT→LLM→TTS) or Engine B (speech-to-speech).
        description:
          type: string
          maxLength: 300
        systemPrompt:
          type: string
          description: Supports {{variable}} templating (FR‑3.5).
        firstMessage:
          type: string
        firstMessageMode:
          type: string
          enum:
            - assistant-speaks-first
            - user-speaks-first
          default: assistant-speaks-first
        transcriber:
          type: object
          description: Cascade only. Catalog-driven; default preset Deepgram Flux.
          properties:
            provider:
              type: string
              example: deepgram
            model:
              type: string
              example: flux-general-en
            language:
              type: string
              default: en
        model:
          type: object
          description: 'Cascade: the LLM slot. S2S: the realtime model.'
          properties:
            provider:
              type: string
              example: google-vertex
            model:
              type: string
            temperature:
              type: number
              minimum: 0
              maximum: 2
            customEndpointUrl:
              type: string
              format: uri
              description: 'OpenAI-compatible endpoint when `provider: custom`.'
        voice:
          type: object
          description: 'Cascade: the TTS slot. S2S: a prebuilt realtime voice name.'
          properties:
            provider:
              type: string
              example: cartesia
            voiceId:
              type: string
            speed:
              type: number
              minimum: 0.25
              maximum: 2
        startSpeakingPlan:
          type: object
          properties:
            waitSeconds:
              type: number
              default: 0.4
            smartEndpointing:
              type: string
              enum:
                - stt-native
                - 'off'
              default: stt-native
        stopSpeakingPlan:
          type: object
          properties:
            numWords:
              type: integer
              default: 0
            voiceSeconds:
              type: number
              default: 0.2
            backoffSeconds:
              type: number
              default: 1
        preemptiveGeneration:
          type: boolean
          default: true
          description: FR‑4.8 — the LLM speculates while TTS waits; per-assistant opt-out.
        recordingEnabled:
          type: boolean
          default: true
          description: False under HIPAA mode (NFR‑5.3).
        tools:
          type: array
          description: >-
            Custom tools and built-ins. Server URLs are validated at save time
            and again at call time.
          items:
            type: object
            required:
              - type
            properties:
              type:
                type: string
                enum:
                  - function
                  - transferCall
                  - endCall
                  - dtmf
              function:
                type: object
                properties:
                  name:
                    type: string
                  description:
                    type: string
                  parameters:
                    type: object
                  serverUrl:
                    type: string
                    format: uri
                  async:
                    type: boolean
                    default: false
        transferPhoneNumber:
          type: string
          description: 'Cold-transfer destination: E.164 or a SIP URI.'
        voicemail:
          type: object
          description: >-
            FR‑2.6 voicemail detection on outbound calls: acoustic, keyword and
            LLM signals, any-two agreement.
          properties:
            enabled:
              type: boolean
              default: true
            action:
              type: string
              enum:
                - hangup
                - leaveMessage
              default: hangup
            message:
              type: string
        smsTool:
          type: object
          description: >-
            The built-in `send_sms` tool. The send runs on the API (`POST
            /internal/sms/send`), never on the worker.
          properties:
            enabled:
              type: boolean
              default: false
            configurationId:
              type: string
              description: Empty = the tenant's default configuration.
            description:
              type: string
              description: >-
                Overrides the tool description the model sees — how it decides
                *when* to text.
            dispatchedMessage:
              type: string
              description: >-
                What the agent says as the text is handed off. The send is
                dispatched, not awaited, so wording that promises arrival will
                sometimes be wrong.
        callbackTool:
          type: object
          description: >-
            The built-in `schedule_callback` tool. When a caller asks to be
            contacted later the agent collects the day, time and timezone,
            confirms the resolved time out loud, and Vodex dials at that time.
            The booking is reported back in the `interaction.completed`
            webhook's `outcome.callback`.
          properties:
            enabled:
              type: boolean
              default: false
            description:
              type: string
              description: >-
                Overrides the tool description the model sees — how it decides
                *when* to book, and what it asks for first.
            failureMessage:
              type: string
              description: Spoken when the booking could not be made.
            unconfirmedMessage:
              type: string
              description: >-
                Spoken when the booking could not be confirmed in time. Vaguer
                than failureMessage on purpose: it may still land.
            pendingMessage:
              type: string
              description: >-
                Filler said only if the wait runs long; empty = stay silent.
                Every default sentence is English, so a non-English assistant
                should set them.
        maxDurationSeconds:
          type: integer
          default: 300
          maximum: 600
        silenceTimeoutSeconds:
          type: integer
          default: 30
        variables:
          type: object
          additionalProperties:
            type: string
        tenantId:
          type: string
          description: Owning tenant — selects the recordings bucket via the storage map.
        webhookSecretRef:
          type: string
          description: >-
            Injected by the control plane on read, never stored on the
            assistant. Never the secret itself.
    Error:
      type: object
      properties:
        error:
          type: string
          example: not found
  securitySchemes:
    ApiKeyBearer:
      type: http
      scheme: bearer
      description: 'Tenant API key (`vdx_live_…`) as `Authorization: Bearer`.'

````