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

# Create a campaign

> The campaign lands in `draft` and dials nothing until you launch it. You don't need the full config up front: a draft can be created without an `assistant.assistantId` and [updated field by field](/api-reference/campaigns/update-a-campaign) until launch, which is where missing pieces are rejected. What you do send is checked now: a `contacts.listId` that doesn't exist fails this request. All the ways contacts get into a campaign: [Contacts](/voice-agents/campaigns/contacts). The one choice you can't revisit is `mode` — `bounded` or `continuous` is fixed at creation.



## OpenAPI

````yaml /voice-agents/api-reference/openapi.yaml post /campaigns
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:
    post:
      tags:
        - Campaigns
      summary: Create a campaign
      description: >-
        The campaign lands in `draft` and dials nothing until you launch it. You
        don't need the full config up front: a draft can be created without an
        `assistant.assistantId` and [updated field by
        field](/api-reference/campaigns/update-a-campaign) until launch, which
        is where missing pieces are rejected. What you do send is checked now: a
        `contacts.listId` that doesn't exist fails this request. All the ways
        contacts get into a campaign:
        [Contacts](/voice-agents/campaigns/contacts). The one choice you can't
        revisit is `mode` — `bounded` or `continuous` is fixed at creation.
      operationId: createCampaign
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateCampaignRequest'
            example:
              name: Edversity cybersecurity intake — August
              mode: continuous
              config:
                goal: lead
                assistant:
                  assistantId: 452dda41-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                dialing:
                  lines: 1
                  fromNumber: '+924238900100'
                  retryPolicy:
                    maxAttempts: 3
                    retryOn:
                      - no_answer
                      - busy
                      - voicemail
                    backoffMinutes:
                      - 15
                      - 120
                schedule:
                  callingWindow:
                    from: '10:00'
                    to: '19:00'
                    tz: Asia/Karachi
      responses:
        '201':
          description: The campaign as stored, in `draft`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Campaign'
              example:
                campaignId: 5170facc-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                organizationId: a12b7e74-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                projectId: c782a4ec-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                name: Edversity cybersecurity intake — August
                state: draft
                mode: continuous
                config:
                  goal: lead
                  assistant:
                    assistantId: 452dda41-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                  dialing:
                    lines: 1
                    fromNumber: '+924238900100'
                    retryPolicy:
                      maxAttempts: 3
                      retryOn:
                        - no_answer
                        - busy
                        - voicemail
                      backoffMinutes:
                        - 15
                        - 120
                  schedule:
                    callingWindow:
                      from: '10:00'
                      to: '19:00'
                      tz: Asia/Karachi
                trigger:
                  kind: list
                steps:
                  - call
                createdAt: '2026-08-18T05:20:28.892Z'
                updatedAt: '2026-08-18T05:20:28.892Z'
                version: 1
        '400':
          description: >-
            Validation failed — a malformed `name` or `mode`, an appointment
            goal with no calendar connection, or an invalid scorecard.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: 'Unknown campaign mode: hybrid'
                error: Bad Request
                statusCode: 400
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: >-
            The contact list or calendar connection named in `config` does not
            exist in this project.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: 'Contact list not found: 00000000-0000-4000-8000-000000000000'
                error: Not Found
                statusCode: 404
components:
  schemas:
    CreateCampaignRequest:
      type: object
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 120
        mode:
          type: string
          enum:
            - bounded
            - continuous
          default: bounded
          description: >-
            Fixed at creation. `bounded` snapshots the contact list at launch
            and completes once the queue empties; `continuous` keeps accepting
            appended contacts and ends only when you close it.
        config:
          $ref: '#/components/schemas/CampaignConfig'
      required:
        - name
    Campaign:
      type: object
      properties:
        campaignId:
          type: string
          format: uuid
        organizationId:
          type: string
          format: uuid
        projectId:
          type: string
          format: uuid
        name:
          type: string
          maxLength: 120
        state:
          type: string
          enum:
            - draft
            - scheduled
            - launching
            - launch_failed
            - running
            - paused
            - draining
            - completed
          description: >-
            Where the campaign is in its lifecycle:


            - `draft` — created, nothing dials

            - `scheduled` — the platform launches it at
            `config.schedule.scheduledFor`

            - `launching` — the contact list is being materialized into a run;
            transient

            - `launch_failed` — materialization failed; `lastError` says which
            phase, and launching again retries it

            - `running` — the dialer is placing calls

            - `paused` — no new dials; calls already in flight run to their end

            - `draining` — closed to new work, waiting only on in-flight calls

            - `completed` — terminal
        mode:
          type: string
          enum:
            - bounded
            - continuous
          description: >-
            Fixed at create. `bounded` snapshots the contact list at launch and
            completes itself once the queue empties; `continuous` keeps
            accepting appended contacts and completes only after a close. Absent
            on older campaigns; read that as `bounded`.
        config:
          $ref: '#/components/schemas/CampaignConfig'
        closedAt:
          type: string
          format: date-time
          description: >-
            When the campaign was closed to new work. Continuous campaigns only;
            closing does not change `state`, so expect `running` until the drain
            begins.
        currentRunId:
          type: string
          format: uuid
          description: The single run created at first launch; a retried launch reuses it.
        runStartedAt:
          type: string
          format: date-time
          description: First launch. Unchanged by pauses, resumes and launch retries.
        lastError:
          type: object
          description: Why the last launch failed. Cleared by a launch that gets further.
          properties:
            phase:
              type: string
              description: The launch step that threw, e.g. `materialize`.
            message:
              type: string
            at:
              type: string
              format: date-time
        statusMessage:
          type: string
          description: >-
            Set when the platform acts on its own, and says why. Expect `Paused:
            out of credits`, or `Paused: assistant version not found` when the
            campaign's assistant was deleted. The assistant can't be changed
            after launch, so a resume won't fix that one. Cleared on resume.
          example: 'Paused: out of credits'
        hasGradedCall:
          type: boolean
          description: >-
            True once any call has been graded, after which `config.scorecard`
            is frozen. Absent until then.
        trigger:
          type: object
          description: 'How the campaign takes work. Always `{"kind": "list"}` today.'
          properties:
            kind:
              type: string
              enum:
                - list
        steps:
          type: array
          description: What each contact goes through. Always `["call"]` today.
          items:
            type: string
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
        version:
          type: integer
          description: >-
            Increments on every accepted write — compare it across reads to tell
            a real change from an unchanged poll.
    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
    CampaignConfig:
      type: object
      additionalProperties: true
      description: >-
        Returned exactly as stored — a section never set is absent rather than
        filled in with defaults.
      properties:
        goal:
          type: string
          enum:
            - appointment
            - event
            - lead
            - order_confirmation
            - payment_reminder
            - outreach
            - reservation_confirmation
          description: >-
            What the calls are for. Picks the outcome-recording tool handed to
            the assistant.
        assistant:
          type: object
          description: The assistant that dials. Frozen once the campaign leaves `draft`.
          properties:
            assistantId:
              type: string
              format: uuid
        assistantVersion:
          oneOf:
            - type: string
              enum:
                - prod
                - draft
            - type: integer
              minimum: 1
          description: >-
            Which version of the assistant the calls run. Absent means `prod`,
            so a publish reaches the next dial. A number pins a published
            version, needs `assistant` set, and is a 400 if that version was
            never published. Editable while the campaign runs.
        contacts:
          type: object
          description: >-
            The contact list this campaign dials, created via
            [`/contact-lists`](/api-reference/contacts/create-a-contact-list).
            All the ways contacts get into a campaign:
            [Contacts](/voice-agents/campaigns/contacts).
          properties:
            listId:
              type: string
              format: uuid
        schedule:
          type: object
          properties:
            scheduledFor:
              type: string
              format: date-time
              description: When a scheduled launch fires. Set by the schedule endpoint.
            callingWindow:
              $ref: '#/components/schemas/CallingWindow'
        dialing:
          type: object
          properties:
            lines:
              type: integer
              description: >-
                Requested concurrent calls. Unset defaults to 1, and an explicit
                value below 1 fails the launch. Your account's own limit caps
                it: 1 call at a time by default, shared with direct calls and
                inbound, so a larger number only dials faster once that limit is
                raised. See [Phone numbers &
                concurrency](/voice-agents/calling/concurrency).
            fromNumber:
              type: string
              description: Caller id the calls are placed from.
              example: '+924232567890'
            retryPolicy:
              $ref: '#/components/schemas/RetryPolicy'
        calendar:
          type: object
          description: >-
            Booking calendar for `appointment` campaigns. Supplying
            `connectionId`, a Google Calendar connection from the
            [portal](https://upliftai.org/app/calling/webhooks), adds the
            booking tools to the call.
          properties:
            connectionId:
              type: string
            calendarId:
              type: string
        scorecard:
          $ref: '#/components/schemas/Scorecard'
        tools:
          type: object
          description: Extra tools stamped onto the assistant for this campaign's calls.
        delivery:
          type: object
          description: Where call outcomes are delivered.
        economics:
          type: object
          description: Per-conversion values that turn campaign stats counts into money.
          additionalProperties:
            type: number
    CallingWindow:
      type: object
      description: >-
        Local hours during which this campaign may dial; omit it and the
        campaign dials around the clock. Retries falling outside the window wait
        until it next opens. There is no per-weekday control. The shape is not
        validated — a malformed window misbehaves at dial time instead of
        erroring here.
      properties:
        from:
          type: string
          description: 24-hour local time, `HH:MM`.
          example: '10:00'
        to:
          type: string
          description: >-
            24-hour local time, `HH:MM`. A `to` earlier than `from` wraps past
            midnight.
          example: '19:00'
        tz:
          type: string
          description: IANA timezone.
          example: Asia/Karachi
      required:
        - from
        - to
        - tz
    RetryPolicy:
      type: object
      description: >-
        Applied all or nothing. Unless `maxAttempts`, `retryOn` and
        `backoffMinutes` are all present and usable, every field falls back to
        its default below — a partial policy is ignored, not merged.
      properties:
        maxAttempts:
          type: integer
          minimum: 1
          default: 3
          description: Dials per contact, the first attempt included.
        retryOn:
          type: array
          default:
            - no_answer
            - busy
            - voicemail
            - failed
            - silent_pickup
            - unreachable
          description: >-
            Outcomes worth dialing again. `booked`, `callback`, `not_interested`
            and `wrong_number` always end a contact, even if listed here.
            `unreachable` (phone off or out of coverage) is usually brief here,
            so the default retries it.
          items:
            type: string
            enum:
              - no_answer
              - busy
              - voicemail
              - failed
              - silent_pickup
              - unreachable
              - wrong_person
              - booked
              - callback
              - not_interested
              - wrong_number
        backoffMinutes:
          type: array
          default:
            - 10
            - 60
          description: >-
            Minutes to wait before each following attempt; the last value
            repeats for any attempts beyond the list.
          items:
            type: integer
            minimum: 0
      required:
        - maxAttempts
        - retryOn
        - backoffMinutes
    Scorecard:
      type: object
      description: >-
        The rubric every call on this campaign is graded against. It freezes
        once any call has been graded, so revise it before launching.
      properties:
        rubricVersionId:
          type: string
          minLength: 1
        rules:
          type: array
          minItems: 1
          description: Rule ids must be unique and their `points` must total exactly 100.
          items:
            $ref: '#/components/schemas/ScorecardRule'
      required:
        - rubricVersionId
        - rules
    ScorecardRule:
      type: object
      properties:
        ruleId:
          type: string
          minLength: 1
        title:
          type: string
          minLength: 1
        instruction:
          type: string
          minLength: 1
          description: What the grader looks for in the transcript to award this rule.
        scoringType:
          type: string
          enum:
            - binary
          description: Each rule is earned in full or not at all.
        points:
          type: integer
          minimum: 1
        evidenceRequired:
          type: boolean
          description: >-
            When true, the grader must cite a transcript quote to award the
            rule.
      required:
        - ruleId
        - title
        - instruction
        - scoringType
        - points
  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_…`).

````