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

# Update a campaign

> What you can edit depends on the state:

- `draft`, `launch_failed` — everything
- `scheduled` — everything except `goal` and `assistant`
- `launching`, `running`, `paused`, `draining` — only `assistantVersion`, `schedule.callingWindow`, `dialing`, `tools` and `delivery`; `name` is frozen too
- `completed` — nothing

Config sections replace rather than merge, so send each section back whole. `schedule` is the exception: `scheduledFor` and `callingWindow` are kept independently.



## OpenAPI

````yaml /voice-agents/api-reference/openapi.yaml patch /campaigns/{campaignId}
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}:
    patch:
      tags:
        - Campaigns
      summary: Update a campaign
      description: >-
        What you can edit depends on the state:


        - `draft`, `launch_failed` — everything

        - `scheduled` — everything except `goal` and `assistant`

        - `launching`, `running`, `paused`, `draining` — only
        `assistantVersion`, `schedule.callingWindow`, `dialing`, `tools` and
        `delivery`; `name` is frozen too

        - `completed` — nothing


        Config sections replace rather than merge, so send each section back
        whole. `schedule` is the exception: `scheduledFor` and `callingWindow`
        are kept independently.
      operationId: updateCampaign
      parameters:
        - name: campaignId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          example: 43150de2-xxxx-xxxx-xxxx-xxxxxxxxxxxx
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateCampaignRequest'
            example:
              config:
                dialing:
                  lines: 6
                  fromNumber: '+924238900100'
                schedule:
                  callingWindow:
                    from: '10:00'
                    to: '18:00'
                    tz: Asia/Karachi
      responses:
        '200':
          description: >-
            The whole campaign as stored after the update, not just the sections
            you sent.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Campaign'
              example:
                campaignId: 43150de2-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                organizationId: a12b7e74-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                projectId: c782a4ec-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                name: Al-Falah credit card payment reminder
                state: draft
                mode: bounded
                config:
                  goal: payment_reminder
                  assistant:
                    assistantId: 452dda41-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                  schedule:
                    scheduledFor: '2027-01-15T09:00:00.000Z'
                    callingWindow:
                      from: '10:00'
                      to: '18:00'
                      tz: Asia/Karachi
                  dialing:
                    lines: 6
                    fromNumber: '+924238900100'
                  scorecard:
                    rubricVersionId: alfalah-reminder-v1
                    rules:
                      - ruleId: identified_bank
                        title: بینک کا تعارف
                        instruction: ایجنٹ نے کال کے آغاز میں الفلاح بینک کا نام لیا
                        points: 30
                        scoringType: binary
                        evidenceRequired: true
                      - ruleId: stated_due_date
                        title: آخری تاریخ بتائی
                        instruction: >-
                          ایجنٹ نے واجب الادا رقم اور ادائیگی کی آخری تاریخ
                          بتائی
                        points: 40
                        scoringType: binary
                        evidenceRequired: true
                      - ruleId: logged_promise
                        title: ادائیگی کا وعدہ
                        instruction: اگر کسٹمر ادائیگی کا وعدہ کرے تو ایجنٹ نے تاریخ نوٹ کی
                        points: 30
                        scoringType: binary
                        evidenceRequired: false
                trigger:
                  kind: list
                steps:
                  - call
                createdAt: '2026-08-18T05:15:42.611Z'
                updatedAt: '2026-08-18T05:22:25.445Z'
                version: 12
        '400':
          description: >-
            Editing a section frozen in this state, a blank `name`, an invalid
            `scorecard`, an `assistantVersion` the assistant never published, or
            an `appointment` campaign whose calendar connection is missing.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                frozenSection:
                  summary: Section not editable in this state
                  value:
                    message: Field 'goal' is not editable while campaign is 'running'
                    error: Bad Request
                    statusCode: 400
                blankName:
                  summary: Name was whitespace only
                  value:
                    message: Campaign name cannot be blank
                    error: Bad Request
                    statusCode: 400
                rubricPoints:
                  summary: Scorecard rules do not total 100
                  value:
                    message: 'rubric: points sum to 70, must be 100'
                    error: Bad Request
                    statusCode: 400
                unpublishedVersion:
                  summary: Pinned to a version the assistant never published
                  value:
                    message: >-
                      Assistant 452dda41-xxxx-xxxx-xxxx-xxxxxxxxxxxx has no
                      published version 7
                    error: Bad Request
                    statusCode: 400
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: >-
            No such campaign in the API key's project — or the `contacts.listId`
            you sent names no list in it.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: 'Campaign not found: 00000000-0000-4000-8000-000000000000'
                error: Not Found
                statusCode: 404
components:
  schemas:
    UpdateCampaignRequest:
      type: object
      description: >-
        Both keys are optional and an empty body is a no-op. Unknown keys are
        dropped silently rather than rejected — `mode` in particular is fixed at
        create.
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 120
          description: Trimmed before storage; whitespace-only is rejected.
        config:
          $ref: '#/components/schemas/CampaignConfig'
    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_…`).

````