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

# Append contacts to a campaign

> The only way to add contacts after launch — the other ways in: [Contacts](/voice-agents/campaigns/contacts). Only a `continuous` campaign that is `running` or `paused` takes them. Draft, completed, closed and bounded campaigns all 409 — a bounded campaign's contacts are fixed at the snapshot taken at launch. A bad row is rejected on its own — the rest of the batch still lands, and the response is a 200 either way. Read `summary` for what was actually accepted and why the rest were excluded. Column naming rules: [Special column names](/voice-agents/campaigns/contacts#special-column-names).



## OpenAPI

````yaml /voice-agents/api-reference/openapi.yaml post /campaigns/{campaignId}/contacts
openapi: 3.1.0
info:
  title: UpliftAI Voice Agents API
  version: '1.0'
  description: >-
    Build voice assistants, start conversations on the web or the phone, run
    calling campaigns, and receive outcomes by webhook. All endpoints take a
    bearer API key unless noted otherwise.
servers:
  - url: https://api.upliftai.org/v1
security:
  - apiKey: []
tags:
  - name: Assistants
  - name: Authoring an assistant
  - name: Starting a conversation
  - name: Sessions & call records
  - name: Campaigns
  - name: Campaign outcomes
  - name: Contacts
  - name: Google Sheet sources
  - name: Integrations & delivery health
paths:
  /campaigns/{campaignId}/contacts:
    post:
      tags:
        - Campaigns
      summary: Append contacts to a campaign
      description: >-
        The only way to add contacts after launch — the other ways in:
        [Contacts](/voice-agents/campaigns/contacts). Only a `continuous`
        campaign that is `running` or `paused` takes them. Draft, completed,
        closed and bounded campaigns all 409 — a bounded campaign's contacts are
        fixed at the snapshot taken at launch. A bad row is rejected on its own
        — the rest of the batch still lands, and the response is a 200 either
        way. Read `summary` for what was actually accepted and why the rest were
        excluded. Column naming rules: [Special column
        names](/voice-agents/campaigns/contacts#special-column-names).
      operationId: appendCampaignContacts
      parameters:
        - name: campaignId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          example: 270411aa-xxxx-xxxx-xxxx-xxxxxxxxxxxx
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AppendContactsRequest'
            examples:
              inline:
                summary: Two new orders and one row with a broken number
                value:
                  contacts:
                    - externalRef: ord-48812
                      name: عائشہ صدیقی
                      phone: '+923001234567'
                      attributes:
                        order: '48812'
                        amount: Rs 4,500
                        city: لاہور
                    - externalRef: ord-47190
                      name: بلال احمد
                      phone: '03219876543'
                      attributes:
                        order: '47190'
                        amount: Rs 12,000
                    - externalRef: ord-48814
                      name: فرحان قریشی
                      phone: 0300-123
              fromSheet:
                summary: Re-pull a connected sheet, phone column named by hand
                value:
                  sheet:
                    ref: 1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms!Orders
                    connectionId: 7f3c1a90-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                  mapping:
                    phoneColumn: Mobile No.
      responses:
        '200':
          description: >-
            A 200 says the batch was processed, not that every row was accepted
            — read `summary`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AppendContactsResponse'
              example:
                results:
                  - contactId: evt#ord-48812
                    status: enrolled
                  - contactId: evt#ord-47190
                    status: rearmed
                  - status: skipped
                    reason: invalidPhone
                summary:
                  enrolled: 1
                  rearmed: 1
                  alreadyPending: 0
                  inFlight: 0
                  skipped: 1
        '400':
          description: Validation failed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                noSource:
                  summary: No source given (an empty `contacts` reads the same)
                  value:
                    message: contacts must not be empty
                    error: Bad Request
                    statusCode: 400
                twoSources:
                  summary: More than one source given
                  value:
                    message: Provide exactly one of contacts, uploadId, or sheet
                    error: Bad Request
                    statusCode: 400
                tooLarge:
                  summary: Over the per-request cap
                  value:
                    message: 'batch_too_large: at most 1000 contacts per request'
                    error: Bad Request
                    statusCode: 400
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: >-
            No such campaign, or no such upload or connection for the source you
            gave.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                campaign:
                  value:
                    message: 'Campaign not found: 00000000-0000-4000-8000-000000000000'
                    error: Not Found
                    statusCode: 404
                upload:
                  value:
                    message: 'Upload not found: 00000000-0000-4000-8000-000000000000'
                    error: Not Found
                    statusCode: 404
        '409':
          description: >-
            The campaign cannot take contacts. `message` says which precondition
            failed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                notContinuous:
                  summary: A bounded campaign — its contacts are fixed at launch
                  value:
                    message: campaign_not_continuous
                    error: Conflict
                    statusCode: 409
                notActive:
                  summary: Not yet launched, or already completed
                  value:
                    message: campaign_not_active
                    error: Conflict
                    statusCode: 409
                closed:
                  summary: Closed — it is draining and takes no new work
                  value:
                    message: campaign_closed
                    error: Conflict
                    statusCode: 409
components:
  schemas:
    AppendContactsRequest:
      description: Send exactly one variant — none, or fields from two, is a 400.
      oneOf:
        - title: Inline rows
          type: object
          required:
            - contacts
          properties:
            contacts:
              type: array
              minItems: 1
              maxItems: 1000
              description: Up to 1000 rows per request.
              items:
                $ref: '#/components/schemas/AppendContact'
        - title: Uploaded file
          type: object
          required:
            - uploadId
          properties:
            uploadId:
              type: string
              minLength: 1
              description: >-
                The `uploadId` returned by
                [`/contact-lists/uploads`](/api-reference/contacts/upload-a-contact-file).
                Parsed, mapped and validated like a contact-list import, up to
                5000 rows. Rows take their identity from an id column
                (`external_ref`, `order_id` and similar) when the file has one,
                from the phone number otherwise.
            mapping:
              allOf:
                - $ref: '#/components/schemas/ContactColumnMapping'
              description: Names the columns when auto-detection would get them wrong.
        - title: Connected sheet
          type: object
          required:
            - sheet
          properties:
            sheet:
              allOf:
                - $ref: '#/components/schemas/AppendSheetSource'
              description: >-
                Read live at append time, up to 5000 rows, with the same parsing
                and identity rules as an uploaded file.
            mapping:
              allOf:
                - $ref: '#/components/schemas/ContactColumnMapping'
              description: Names the columns when auto-detection would get them wrong.
    AppendContactsResponse:
      type: object
      properties:
        results:
          type: array
          description: >-
            One entry per inline row, in the same order. Upload and sheet rows
            dropped at import appear only in `excluded`.
          items:
            $ref: '#/components/schemas/AppendResult'
        summary:
          type: object
          description: The `results` statuses counted up.
          properties:
            enrolled:
              type: integer
            rearmed:
              type: integer
            alreadyPending:
              type: integer
            inFlight:
              type: integer
            skipped:
              type: integer
        excluded:
          type: object
          description: >-
            Upload and sheet appends only: rows the import pipeline dropped
            before any write.
          properties:
            invalidPhone:
              type: integer
            missingName:
              type: integer
            duplicate:
              type: integer
    Error:
      type: object
      properties:
        message:
          description: >-
            One message per failed field for validation errors, a single string
            otherwise.
          oneOf:
            - type: string
            - type: array
              items:
                type: string
        error:
          type: string
        statusCode:
          type: integer
    AppendContact:
      type: object
      description: >-
        No field is required, but a new contact without a usable phone or a name
        comes back `skipped`.
      properties:
        externalRef:
          type: string
          pattern: ^[A-Za-z0-9_.:-]{1,64}$
          description: >-
            Your own id for this job — an order, ticket or lead number. It
            becomes the contact's identity, so sending it again reaches the same
            contact rather than adding a second one, and it travels verbatim
            into call ids and webhook events. A value outside the pattern gives
            `skipped` / `invalidExternalRef`; omit it and the row always enrolls
            as a new contact.
          example: ord-48812
        name:
          type: string
          description: >-
            Spoken aloud during the call. An Urdu name written in Roman letters
            is read out as English, so send Nastaliq. A new contact needs it:
            blank or missing gives `skipped` / `missingName`. Leave it out when
            your `externalRef` names a contact we already have, and the stored
            name is kept.
        phone:
          type: string
          description: >-
            Pakistani national (`03001234567`) or E.164 (`+923001234567`),
            stored in the format you send. Anything unparseable gives `skipped`
            / `invalidPhone`.
        attributes:
          type: object
          additionalProperties:
            type: string
          description: >-
            Row data the assistant is given for the call. Capped at 1500 bytes
            of JSON — past the cap, values are trimmed and later keys dropped,
            so put what matters first. When the row re-arms a finished contact,
            sending `attributes` replaces the stored set whole, and leaving it
            out keeps it.
    ContactColumnMapping:
      type: object
      description: >-
        Column overrides. Whatever you leave out is auto-detected from the
        headers.
      properties:
        nameColumn:
          type: string
          description: >-
            Header name (case-insensitive) or A1 column letter. An explicit
            value never falls back to auto-detection — if it matches no column,
            names read as empty and the preview reports it in `mappingIssues`.
          example: Name
        phoneColumn:
          type: string
          description: Header name or A1 column letter, resolved like `nameColumn`.
          example: B
        attributeColumns:
          type: array
          items:
            type: string
          description: >-
            Columns to carry as per-contact attributes, usable as
            personalization variables. Exact header names only — no A1 letters.
            Omit to keep every column that is not the name or phone column; send
            `[]` to keep none.
          example:
            - City
            - Appointment
    AppendSheetSource:
      type: object
      properties:
        ref:
          type: string
          minLength: 1
          description: >-
            Spreadsheet id, optionally `id!Tab` to pin one tab. The sheet is
            pulled fresh on every append.
          example: 1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms
        connectionId:
          type: string
          minLength: 1
          description: >-
            A Google connection, needed only for a sheet that is not publicly
            readable.
      required:
        - ref
    AppendResult:
      type: object
      properties:
        contactId:
          type: string
          description: >-
            `evt#{externalRef}` for a row that carried one, otherwise a
            generated uuid. Absent on rows skipped for a bad phone, a missing
            name, a bad `externalRef` or a duplicate phone.
          example: evt#ord-48812
        status:
          type: string
          enum:
            - enrolled
            - rearmed
            - already_pending
            - in_flight
            - skipped
          description: >-
            `enrolled` is a contact that did not exist. `rearmed` is one that
            had finished — done, out of attempts, or skipped — and is now queued
            to be dialed again. `already_pending` and `in_flight` mean it is
            still waiting or on a call right now, so this row changed nothing.
        reason:
          type: string
          enum:
            - invalidPhone
            - missingName
            - duplicate
            - invalidExternalRef
            - campaignClosed
          description: >-
            Only on `skipped`. `duplicate` is a collision inside this request,
            not with an existing contact. `campaignClosed` means the campaign
            was closed part-way through a large batch: earlier rows were written
            and the rest were not.
  responses:
    Unauthorized:
      description: A bearer key was sent but is not recognized.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            message: invalid authorization
            error: Unauthorized
            statusCode: 401
    Forbidden:
      description: No API key sent, or the key lacks permission for this resource.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            message: Forbidden resource
            error: Forbidden
            statusCode: 403
  securitySchemes:
    apiKey:
      type: http
      scheme: bearer
      description: Project API key (`sk_api_…`).

````