> ## 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 a batch

> `Idempotency-Key` is **mandatory**: async producers retry, and a replay
must return the same batch rather than dial the list twice. A replay
answers `202` with `Idempotent-Replay: true`; the same key with a
different body is `409`.

Rows are **admitted or reported, never silently dropped**. A row whose
prompt says "your balance is {{balance}}" with no `balance` is refused
here, where the caller can fix it, rather than dialed and reported as a
normal call. At most 10,000 rows per request — append the rest, or
upload a CSV.

Quota is checked at submission rather than 4,000 dials later.




## OpenAPI

````yaml /openapi/api-public-v1.yaml post /batches
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:
  /batches:
    post:
      tags:
        - Batches
      summary: Create a batch
      description: |
        `Idempotency-Key` is **mandatory**: async producers retry, and a replay
        must return the same batch rather than dial the list twice. A replay
        answers `202` with `Idempotent-Replay: true`; the same key with a
        different body is `409`.

        Rows are **admitted or reported, never silently dropped**. A row whose
        prompt says "your balance is {{balance}}" with no `balance` is refused
        here, where the caller can fix it, rather than dialed and reported as a
        normal call. At most 10,000 rows per request — append the rest, or
        upload a CSV.

        Quota is checked at submission rather than 4,000 dials later.
      operationId: createBatch
      parameters:
        - $ref: '#/components/parameters/IdempotencyKeyRequired'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - name
                - assistantId
              properties:
                name:
                  type: string
                  maxLength: 200
                assistantId:
                  type: string
                  format: uuid
                  description: >-
                    The assistant's uuid, shown on its configure page in the
                    console. Names and slugs are not accepted.
                concurrency:
                  type: integer
                  minimum: 1
                  maximum: 100
                  default: 10
                retry:
                  $ref: '#/components/schemas/RetryPolicy'
                window:
                  $ref: '#/components/schemas/CallingWindow'
                suppress:
                  type: array
                  items:
                    type: string
                  description: E.164 numbers this batch must never dial.
                from:
                  type: object
                  properties:
                    strategy:
                      type: string
                      enum:
                        - fixed
                        - round_robin
                        - weighted
                        - sticky
                      default: sticky
                    numbers:
                      type: array
                      items:
                        type: object
                        required:
                          - e164
                        properties:
                          e164:
                            type: string
                          weight:
                            type: integer
                            minimum: 1
                            default: 1
                rows:
                  type: array
                  maxItems: 10000
                  items:
                    $ref: '#/components/schemas/BatchRowInput'
                startPaused:
                  type: boolean
                  default: false
      responses:
        '202':
          description: 'Accepted. `Idempotent-Replay: true` on a replay.'
          content:
            application/json:
              schema:
                type: object
                properties:
                  batch:
                    $ref: '#/components/schemas/Batch'
                  accepted:
                    type: integer
                  rejected:
                    type: array
                    items:
                      $ref: '#/components/schemas/RowRejection'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          description: Out of minutes — top up before submitting a batch.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: Unknown assistant, or a `from` number that is not yours.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '409':
          description: This `Idempotency-Key` was used with a different body.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '422':
          description: Every row was rejected — a batch that can dial nobody is a mistake.
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                  rejected:
                    type: array
                    items:
                      $ref: '#/components/schemas/RowRejection'
components:
  parameters:
    IdempotencyKeyRequired:
      name: Idempotency-Key
      in: header
      required: true
      schema:
        type: string
        minLength: 8
        maxLength: 255
      description: >-
        Required. A retry with the same key and body replays the stored 2xx
        response; the same key with a different body is `409`. A failed attempt
        releases the key.
  schemas:
    RetryPolicy:
      type: object
      properties:
        'on':
          type: array
          items:
            type: string
          description: End reasons worth retrying (e.g. `no_answer`, `busy`).
        attempts:
          type: integer
          minimum: 0
          maximum: 10
        backoffMinutes:
          type: number
          minimum: 1
          maximum: 10080
    CallingWindow:
      type: object
      nullable: true
      description: When this batch may dial — outside it, rows wait rather than fail.
      properties:
        timezone:
          type: string
          example: America/New_York
        start:
          type: string
          example: '09:00'
        end:
          type: string
          example: '20:00'
        days:
          type: array
          items:
            type: integer
            minimum: 0
            maximum: 6
    BatchRowInput:
      type: object
      required:
        - to
      properties:
        to:
          type: string
          description: E.164. `toE164` is accepted as an alias.
        rowRef:
          type: string
          maxLength: 256
          description: The consumer's own reference, echoed on attempts.
        assistantId:
          type: string
          description: Overrides the batch's assistant for this row.
        from:
          type: string
          description: E.164 caller ID for this row.
        variables:
          type: object
          additionalProperties:
            type: string
          description: >-
            At most 16 kB serialized. A row missing a variable the prompt
            references is rejected — a blank where a number should be is
            indistinguishable from a working call.
    Batch:
      type: object
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        status:
          type: string
          enum:
            - running
            - paused
            - completed
            - cancelled
            - failed
        runNo:
          type: integer
        assistantId:
          type: string
          format: uuid
          description: >-
            The assistant's uuid, shown on its configure page in the console.
            Names and slugs are not accepted.
        concurrency:
          type: integer
        retry:
          $ref: '#/components/schemas/RetryPolicy'
        window:
          $ref: '#/components/schemas/CallingWindow'
        suppress:
          type: array
          items:
            type: string
        createdAt:
          type: string
          format: date-time
          nullable: true
        startedAt:
          type: string
          format: date-time
          nullable: true
        completedAt:
          type: string
          format: date-time
          nullable: true
        counts:
          type: object
          additionalProperties:
            type: integer
          description: Row counts by status, plus `total`.
    RowRejection:
      type: object
      properties:
        index:
          type: integer
          description: Position in the request (or the CSV line number).
        rowRef:
          type: string
        to:
          type: string
          description: >-
            The number as submitted, echoed back. `index` counts PARSED rows, so
            blank lines and column-count failures have already shifted it away
            from the line number the editor shows — this is the value the person
            fixing a four-thousand-row list can actually search for.
        reason:
          type: string
    Error:
      type: object
      properties:
        error:
          type: string
          example: not found
  responses:
    BadRequest:
      description: Validation failed.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    Unauthorized:
      description: No usable credential.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
  securitySchemes:
    ApiKeyBearer:
      type: http
      scheme: bearer
      description: 'Tenant API key (`vdx_live_…`) as `Authorization: Bearer`.'

````