> ## 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.

# Append rows from a CSV

> A raw `text/csv` body, not a JSON envelope. Every column that is not a
known field (`rowRef`, `to`/`toE164`, `assistantId`, `from`) becomes a
**variable** — that is the whole ergonomic point: export
`to,name,balance,tz` from your CRM and it works, with no mapping UI to
build and none to learn.

Admission is the same function the JSON path uses. Parse errors are
rejections too, and they carry the **line number** the customer's editor
shows them. Up to 100,000 rows (higher than the JSON ceiling on purpose)
and a 32 MB body.




## OpenAPI

````yaml /openapi/api-public-v1.yaml post /batches/{id}/rows.csv
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/{id}/rows.csv:
    post:
      tags:
        - Batches
      summary: Append rows from a CSV
      description: |
        A raw `text/csv` body, not a JSON envelope. Every column that is not a
        known field (`rowRef`, `to`/`toE164`, `assistantId`, `from`) becomes a
        **variable** — that is the whole ergonomic point: export
        `to,name,balance,tz` from your CRM and it works, with no mapping UI to
        build and none to learn.

        Admission is the same function the JSON path uses. Parse errors are
        rejections too, and they carry the **line number** the customer's editor
        shows them. Up to 100,000 rows (higher than the JSON ceiling on purpose)
        and a 32 MB body.
      operationId: appendBatchRowsCsv
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          text/csv:
            schema:
              type: string
            example: |
              to,rowRef,name,balance
              +14155551234,acct_991,Dana,120.50
      responses:
        '201':
          description: Appended.
          content:
            application/json:
              schema:
                type: object
                properties:
                  added:
                    type: integer
                  rejected:
                    type: array
                    items:
                      $ref: '#/components/schemas/RowRejection'
                  counts:
                    type: object
                    additionalProperties:
                      type: integer
        '400':
          description: Not a CSV body, unparseable, or over the row cap.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: The batch is cancelled.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '422':
          description: Every row was rejected.
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                  rejected:
                    type: array
                    items:
                      $ref: '#/components/schemas/RowRejection'
components:
  schemas:
    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:
    Unauthorized:
      description: No usable credential.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    NotFound:
      description: >-
        No such resource — or it belongs to another tenant, which looks the
        same.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
  securitySchemes:
    ApiKeyBearer:
      type: http
      scheme: bearer
      description: 'Tenant API key (`vdx_live_…`) as `Authorization: Bearer`.'

````