---
title: "Replace a campaign's configuration"
method: PUT
path: "/api/v1/campaigns/{campaign_id}/configuration"
tags: ["campaign"]
---

# Replace a campaign's configuration

`PUT /api/v1/campaigns/{campaign_id}/configuration`

Replace a campaign's configuration through the one validated write path.

Authorize, validate, then write via the ``apply_campaign_config`` RPC, which
stamps the write so the version trigger records who made it. Returns the id
of the version the write minted, the stored configuration, and the
``base_updated_at`` token to send back on the next conditional write.

Supplying ``base_updated_at`` makes the write conditional: if the
configuration moved since that token was issued, the write is refused with
409 instead of overwriting the other edit. Omit it to write unconditionally.

What this endpoint does NOT do:
  * No partial merge. The body's ``configuration`` REPLACES the stored one
    outright, matching how the product saves today.
  * No experiments consent flow. Consent arguments are always sent empty, so
    a write that would disturb a running experiment is refused by the engine
    and returned as 409 with the engine's message; that caller has to go
    through the consent-aware path instead.
  * No field stripping. Keys CampaignConfig does not declare are stored
    verbatim and logged, never dropped.

``request`` is the published contract and the first validation gate;
``http_request`` supplies what actually gets written. Parsing the body into
CampaignConfig would silently drop every undeclared key (the model is
``extra="ignore"``, and its nested models are too, so no amount of
``extra="allow"`` at the top level would make the parsed object faithful),
so the raw JSON object is what reaches storage.

Decoded from the cached BYTES, not from ``http_request.json()``: Starlette
caches the decoded object and hands the SAME one to FastAPI's parse, and
several config validators rewrite what they are given in place
(TTSConfig._resolve_params_by_provider replaces params with a model
instance and can inject a default model). Reading .json() would therefore
return a body the parse had already corrupted. Re-decoding the bytes costs
one json parse and is the only form that is guaranteed pristine.

## Path parameters

- `campaign_id` string, required

## Request body

- CampaignConfigurationUpdateRequest — Envelope for a whole-configuration replacement. ``extra="forbid"`` applies to the envelope ONLY. ``configuration`` is typed as CampaignConfig so the full config schema is published in the OpenAPI spec and the frontend can generate types from it — but CampaignConfig is ``extra="ignore"``, so parsing it DROPS every undeclared key. This model is therefore the contract and the first validation gate, never the source of what gets stored: the handler reads the raw body and writes that instead.
  - `configuration` CampaignConfig, required
    - `scripts` LLMScripts
      - `inbound` ScriptStep[], required
        - `step` string, required
        - `content` string, required
      - `outbound` ScriptStep[], required
        - `step` string, required
        - `content` string, required
      - `voicemail` ScriptStep[], required
        - `step` string, required
        - `content` string, required
      - `sms` ScriptStep[], nullable
        - `step` string, required
        - `content` string, required
      - `after_hours` ScriptStep[], nullable
        - `step` string, required
        - `content` string, required
    - `vad` VadConfig — Top-level VAD configuration attached to ``CampaignConfig``. Two modes: * ``mode="behavior"`` (default): use the semantic ``behavior`` knobs. * ``mode="advanced"``: engineer-only; overlay raw ``RawVadOverride`` on top of the balanced baseline for the active STT provider. Backwards-compat for the flat pre-framework shape ``{confidence, start_secs, stop_secs, min_volume}``: * If the values match one of the known stale-boilerplate templates (values that prod never actually ran), we discard them and fall back to balanced defaults. Rollout stays a no-op for those campaigns. * Otherwise we migrate the block to ``mode="advanced"`` so the hand-tuned values are honored.
      - `mode` 'behavior' | 'advanced'
      - `behavior` VadBehavior — Customer-facing VAD knobs.
        - `response_speed` union
          - 'eager' | 'fast' | 'balanced' | 'patient' | 'very_patient'
          - number
        - `interruption_sensitivity` union
          - 'very_robust' | 'robust' | 'balanced' | 'sensitive' | 'very_sensitive'
          - number
      - `advanced` RawVadOverride — Internal escape hatch. Not advertised in the customer UI. Any non-``None`` field overrides the corresponding value produced by the resolver for a ``balanced`` baseline. Partial overrides are supported.
        - `confidence` number, nullable
        - `start_secs` number, nullable
        - `stop_secs` number, nullable
        - `min_volume` number, nullable
        - `flux_eager_eot_threshold` number, nullable
        - `flux_eot_threshold` number, nullable
        - `flux_eot_timeout_ms` integer, nullable
    - `tts` TTSConfig
      - `provider` 'elevenlabs' | 'cartesia' | 'inworld'
      - `voice_id` string
      - `model` string
      - `output_format` string
      - `params` union
        - ElevenLabsTTSParams
          - `stability` number
          - `similarity_boost` number
          - `style` number
          - `use_speaker_boost` boolean
          - `language` string, nullable
          - `speed` number, nullable
          - `auto_mode` boolean, nullable
          - `enable_ssml_parsing` boolean, nullable
        - CartesiaTTSParams — Runtime params for Cartesia Sonic TTS. Ranges come from Cartesia's ``GenerationConfig`` (volume [0.5, 2.0], speed [0.6, 1.5]). ``emotion`` is constrained to the supported set and ``language`` is currently limited to English and Spanish until we have demand (and localized scripts) for more. ``pronunciation_dict_id`` is an opaque string returned by Cartesia's ``POST /pronunciation-dicts`` endpoint (no fixed format) — it's stored as-is and forwarded to Cartesia on every TTS request.
          - `language` 'en' | 'es' — Languages exposed to the frontend for Cartesia. Extend as we broaden coverage.
          - `speed` number, nullable
          - `volume` number, nullable
          - `emotion` 'neutral' | 'angry' | 'excited' | 'content' | 'sad' | 'scared' | 'happy' | 'enthusiastic' | 'elated' | 'euphoric' | 'triumphant' | 'amazed' | 'surprised' | 'flirtatious' | 'joking/comedic' | 'curious' | 'peaceful' | 'serene' | 'calm' | 'grateful' | 'affectionate' | 'trust' | 'sympathetic' | 'anticipation' | 'mysterious' | 'mad' | 'outraged' | 'frustrated' | 'agitated' | 'threatened' | 'disgusted' | 'contempt' | 'envious' | 'sarcastic' | 'ironic' | 'dejected' | 'melancholic' | 'disappointed' | 'hurt' | 'guilty' | 'bored' | 'tired' | 'rejected' | 'nostalgic' | 'wistful' | 'apologetic' | 'hesitant' | 'insecure' | 'confused' | 'resigned' | 'anxious' | 'panicked' | 'alarmed' | 'proud' | 'confident' | 'distant' | 'skeptical' | 'contemplative' | 'determined' — Emotions supported by Cartesia Sonic-3. Mirrors ``pipecat.services.cartesia.tts.CartesiaEmotion`` so that the frontend can render a constrained dropdown and we validate inputs at the API boundary.
          - `pronunciation_dict_id` string, nullable
        - InworldTTSParams
          - `temperature` number, nullable
    - `llm` LLMConfig
      - `model` string
      - `params` LLMParams
        - `temperature` number
        - `top_p` number
        - `frequency_penalty` number
        - `presence_penalty` number
        - `max_tokens` integer
    - `hangup` HangupConfig
      - `call_timeout` integer
      - `hangup_delay` integer
    - `insights` InsightsConfig
      - `insights` string[]
      - `success_response_definition` string, nullable
      - `custom_insights` CustomInsight[], nullable
        - `name` string, nullable
        - `definition` string, nullable
        - `maps_to_status` union
          - 'completed' | 'awaiting_followup' | 'awaiting_reply' | 'awaiting_reattempt' — Contact statuses a customer-created insight may map an interaction into. Mirrors ``ALLOWED_CUSTOM_INSIGHT_STATUSES`` (defined further down, after ``CampaignContactStatus``); the two are pinned together by a test. Declared here because ``CustomInsight`` needs it and ``InsightsConfig`` needs ``CustomInsight``.
          - string
    - `bot` BotConfig
      - `allow_interruptions` boolean
      - `answer_delay` number
      - `idle_timeout_secs` number, nullable
    - `reattempt` ReattemptConfig
      - `by_outcome` ReattemptByOutcome
        - `voicemail_box_full` integer
        - `hangup` integer
        - `voicemail_left` integer
        - `line_busy` integer
      - `cadence_prompt` string, nullable — Prompt for cadence configuration
      - `cadence_context` CadenceContextConfig
        - `fields` CadenceContextField[]
          - `source` 'exportable_fields' | 'personalization_fields', required
          - `path` string, required — Exact key to read from the configured contact field map
          - `label` string, nullable — Optional prompt label; defaults to path
      - `use_answer_rate_distribution` boolean — Render the full hourly answer-rate distribution in the reattempt scheduling prompt instead of the single Thompson point recommendation
      - `keep_cadence_on_after_human_connect` boolean — Keep using cadence_prompt scheduling even after a prior human connection
      - `max_voicemail_count` integer, nullable — Maximum number of voicemails to leave per contact. -1 for unlimited, 0 to disable, None uses system default (no voicemail)
      - `voicemail_mode` 'default' | 'specific_attempts' — Voicemail decision mode. 'default' keeps the existing distribution algorithm; 'specific_attempts' leaves voicemail only on the configured attempt numbers.
      - `voicemail_allowed_attempts` integer[], nullable — Allowed dial attempts for voicemail when voicemail_mode is 'specific_attempts'.
    - `transfer` TransferConfig
      - `enabled` boolean
      - `destinations` TransferDestination[]
        - `name` string, nullable
        - `phone_number` string, nullable
        - `description` string, nullable
      - `is_blind_transfer` boolean
      - `background_transfer` boolean
      - `payment_transfer` boolean
      - `payment_number` string, nullable
      - `payment_definition` string, nullable
      - `transfer_definition` string, nullable
      - `handoff_phrase` string, nullable
      - `provider` 'media_alpha' — Vendor that resolves the transfer destination at call time. When set, the configured destinations[] are placeholders — the actual phone number is fetched from the vendor's auction/lookup endpoint using a per-lead token in personalization_fields. When unset, transfer dials destinations[] directly (default behavior).
      - `whisper` TransferWhisperConfig — Opt-in whisper step for non-blind transfers. The human agent is dialed on a separate Vonage leg, listened to via Vonage ASR until a live human is detected, briefed with a personalized whisper message, then bridged into the live Daily room. Ignored for blind transfers. Any whisper failure falls back to the standard transfer path.
        - `enabled` boolean
        - `message_template` string, nullable
        - `timeout_seconds` number
        - `handoff_grace_seconds` number
        - `max_message_plays` integer
        - `message_gap_seconds` number
    - `hours_of_operation` CampaignHoursOfOperation
      - `days` DaysSchedule
        - `Monday` DaySchedule
          - `open` string, nullable
          - `close` string, nullable
        - `Tuesday` DaySchedule
          - `open` string, nullable
          - `close` string, nullable
        - `Wednesday` DaySchedule
          - `open` string, nullable
          - `close` string, nullable
        - `Thursday` DaySchedule
          - `open` string, nullable
          - `close` string, nullable
        - `Friday` DaySchedule
          - `open` string, nullable
          - `close` string, nullable
        - `Saturday` DaySchedule
          - `open` string, nullable
          - `close` string, nullable
        - `Sunday` DaySchedule
          - `open` string, nullable
          - `close` string, nullable
      - `holiday` DaySchedule
        - `open` string, nullable
        - `close` string, nullable
      - `after_hours_action` AfterHoursAction
        - `action` 'forward' | 'sms', required
        - `description` string, nullable
        - `metadata` Metadata
          - `destination` string, nullable
      - `timezone` string
    - `sip_config` SipConfig — SIP routing configuration for inbound calls. Inbound calls default to Daily unless this config sets a provider explicitly. Daily room pool allocation is independent of SIP provider, so any provider can use pooled Daily rooms when available. When ``sip_provider`` is set and ``percentage`` is omitted, all inbound calls use that provider.
      - `sip_provider` 'signalwire' | 'daily'
      - `percentage` integer, nullable
    - `inbound_config` InboundConfig — Configuration specific to inbound campaigns.
    - `outbound_config` OutboundConfig — Configuration specific to outbound campaigns.
      - `lead_type` string, nullable
      - `caller_id_routing` CallerIdRule[], nullable
        - `number` string, required
        - `dial_pattern` 'all' | 'odd' | 'even' | 'specific'
        - `dials` integer[], nullable
    - `always_forward_action` AfterHoursAction
      - `action` 'forward' | 'sms', required
      - `description` string, nullable
      - `metadata` Metadata
        - `destination` string, nullable
    - `mid_call_actions` MidCallAction[], nullable
      - `id` string, nullable
      - `trigger_phrases` string[], nullable
      - `similarity_threshold` number
      - `processor_type` string, required
      - `config` object, nullable
    - `context_injection` ContextInjectionConfig — Granular control over what context is injected into the LLM.
      - `enabled` boolean
      - `include_verification` boolean
      - `include_history` boolean
      - `include_crm` boolean
    - `context_injection_enabled` boolean, nullable
    - `verification_enabled` boolean, nullable
    - `preferred_language` string, nullable
    - `pipecat_flows_enabled` boolean, nullable
    - `flow_graph_id` string, nullable
    - `flow_graph_bindings` DirectionalFlowGraphBindings
      - `inbound` FlowGraphVersionBinding
        - `graph_id` string, required
        - `version_id` string, required
        - `version_hash` string, required
        - `graph_name` string, nullable
        - `version` integer, nullable
        - `published_at` string, nullable
      - `outbound` FlowGraphVersionBinding
        - `graph_id` string, required
        - `version_id` string, required
        - `version_hash` string, required
        - `graph_name` string, nullable
        - `version` integer, nullable
        - `published_at` string, nullable
    - `flow_graphs` FlowGraphsConfig
      - `enabled` boolean, nullable
      - `by_direction` DirectionalFlowGraphSelectors
        - `inbound` union
          - RuntimeFlowGraphSelector
            - `kind` 'runtime'
            - `source_id` string, required
          - PublishedFlowGraphVersionSelector
            - `graph_id` string, required
            - `version_id` string, required
            - `version_hash` string, required
            - `graph_name` string, nullable
            - `version` integer, nullable
            - `published_at` string, nullable
            - `kind` 'published'
          - EmptyFlowGraphSelector
            - `kind` 'none'
        - `outbound` union
          - RuntimeFlowGraphSelector
            - `kind` 'runtime'
            - `source_id` string, required
          - PublishedFlowGraphVersionSelector
            - `graph_id` string, required
            - `version_id` string, required
            - `version_hash` string, required
            - `graph_name` string, nullable
            - `version` integer, nullable
            - `published_at` string, nullable
            - `kind` 'published'
          - EmptyFlowGraphSelector
            - `kind` 'none'
    - `context_fetch_mode` 'query' | 'prefetch' | 'disabled'
    - `post_processing` PostProcessingConfig — Configuration for post-processing LLM (classification, resolution, call quality analysis). When set, overrides the default OpenAI-based post-processing with the specified provider/model. This allows campaigns to use Claude for tagging tasks where it shows better accuracy.
      - `provider` 'openai' | 'anthropic'
      - `model` string, nullable
    - `tag_definitions` TagDefinitions — Definitions for tags that can be applied to interactions (any campaign type).
      - `resolution` string, nullable
      - `resolution_label` string, nullable
      - `classifications` TagClassification[], nullable
        - `name` string, required
        - `definition` string, required
        - `options` TagOption[], required
          - `name` string, required
          - `definition` string, nullable
      - `enable_call_quality` boolean, nullable
    - `success_criteria` SuccessCriteriaConfig
      - `enabled` boolean, nullable
      - `channels` SuccessCriteriaChannels
        - `outbound_call` SuccessCriteriaChannelConfig — Success definition for one channel.
          - `label` string, nullable
          - `insight_keys` string[], nullable
          - `tag_values` string[], nullable
        - `inbound_call` SuccessCriteriaChannelConfig — Success definition for one channel.
          - `label` string, nullable
          - `insight_keys` string[], nullable
          - `tag_values` string[], nullable
        - `sms` SuccessCriteriaChannelConfig — Success definition for one channel.
          - `label` string, nullable
          - `insight_keys` string[], nullable
          - `tag_values` string[], nullable
    - `use_case_template_id` string, nullable
    - `outcome_metrics` OutcomeMetricRule[], nullable
      - `id` string, nullable
      - `label` string, nullable
      - `format` 'number' | 'percent' | 'currency'
      - `operands` OutcomeMetricOperand[], nullable
        - `id` string, nullable
        - `fn` 'count_contacts' | 'sum' | 'avg' | 'min' | 'max'
        - `field` string, nullable
        - `filter` OutcomeMetricContactFilter — Restrict an operand to contacts whose ``source_field`` matches.
          - `source_field` string, nullable
          - `target_values` string[], nullable
        - `include_removed` boolean, nullable
      - `formula` OutcomeMetricFormulaNode — One node of a metric's formula tree. Flattened on purpose: ``kind`` names the variant ("operand", a literal, a binary operation) but is NOT a pydantic discriminator, so the union of variants can never reject a stored node. Only the fields relevant to a node's ``kind`` are populated. ``opId`` is camelCase in the stored JSON and is published as written.
        - `kind` string, nullable
        - `opId` string, nullable
        - `value` number, nullable
        - `label` string, nullable
        - `format` 'number' | 'percent' | 'currency'
        - `op` string, nullable
        - `left` OutcomeMetricFormulaNode — recursive
        - `right` OutcomeMetricFormulaNode — recursive
      - `source_field` string, nullable
      - `target_values` string[], nullable
      - `include_removed` boolean, nullable
    - `outcome_metrics_exclude_zero_dials` boolean, nullable
    - `brain_live` boolean, nullable
    - `reporting_library` object, nullable
    - `action_detection` object, nullable
  - `base_updated_at` string, date-time, nullable — Compare-and-set token from a previous response's base_updated_at. When supplied the write is refused with 409 if the configuration changed since. Omit to write unconditionally.

## Response `200`

Configuration written; a new version was minted

- CampaignConfigurationResponse — What a successful configuration write returns. ``configuration`` is an open mapping ON PURPOSE. Typing it as CampaignConfig would make FastAPI's response filtering strip undeclared keys back out of the response, so a client could not tell what is actually stored. The request side is typed for the contract; this side reports storage verbatim.
  - `campaign_id` string, required — The campaign that was written.
  - `version_id` string, nullable — Id of the campaign version this write minted.
  - `configuration` object, nullable — The configuration as now stored, including undeclared keys.
  - `updated_at` string, date-time, nullable — The campaign row's updated_at after the write.
  - `base_updated_at` string, date-time, nullable — Token to send as base_updated_at on the next conditional write. Advances only when the configuration really changes — do not derive it from updated_at, which moves on any column.

## Other responses

- `403` — User is not authorized to edit this campaign
- `404` — Campaign not found
- `409` — The configuration engine refused the write: a lost version race (stale base_updated_at) or a change that would disturb a running experiment. The detail carries the engine's own message.
- `422` — Invalid campaign configuration

---

[API](https://skmtc.net/outrival/apis/fastapi.md) · [All operations](https://skmtc.net/outrival/apis/fastapi/llms.txt) · [OpenAPI document](https://skmtc-service-staging.skmtc.workers.dev/v1/apis/outrival/fastapi/revisions/7bcf41dff3cb/schema)
