---
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`

## Request

- Base URL: `https://api.outrival.com` (the document declares no server; this is the origin it was published from)
- URL: `https://api.outrival.com/api/v1/campaigns/{campaign_id}/configuration`
- Auth: HTTP bearer

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 blanket field stripping. Only the explicit retired root keys and
    null legacy placeholders are removed by the service; other undeclared
    keys are stored verbatim and logged.

``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[], nullable
        - `step` string, required
        - `content` string, required
      - `outbound` ScriptStep[], nullable
        - `step` string, required
        - `content` string, required
      - `voicemail` ScriptStep[], nullable
        - `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
      - `confidence` number, nullable: Flat VAD shape; migrated or discarded on read
      - `start_secs` number, nullable: Flat VAD shape; migrated or discarded on read
      - `stop_secs` number, nullable: Flat VAD shape; migrated or discarded on read
      - `min_volume` number, nullable: Flat VAD shape; migrated or discarded on read
    - `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
          - `emotion` string, nullable: Cartesia param; ignored for ElevenLabs
          - `volume` number, nullable: Cartesia param; ignored for ElevenLabs
        - 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
          - `stability` number, nullable: ElevenLabs param; ignored for Cartesia
          - `similarity_boost` number, nullable: ElevenLabs param; ignored for Cartesia
          - `style` number, nullable: ElevenLabs param; ignored for Cartesia
          - `use_speaker_boost` boolean, nullable: ElevenLabs param; ignored for Cartesia
          - `auto_mode` boolean, nullable: ElevenLabs param; ignored for Cartesia
          - `enable_ssml_parsing` boolean, nullable: ElevenLabs param; ignored for Cartesia
        - InworldTTSParams
          - `temperature` number, nullable
    - `llm` LLMConfig
      - `model` string
      - `params` LLMParams
        - `temperature` number
        - `top_p` number
        - `frequency_penalty` number
        - `presence_penalty` number
        - `max_tokens` integer
        - `reasoning_effort` string, nullable: Stored by the Cortex demo; not read at runtime
        - `max_completion_tokens` integer, nullable: Stored by the Cortex demo; not read at runtime
    - `hangup` HangupConfig
      - `call_timeout` integer
      - `hangup_delay` integer
    - `insights` InsightsConfig
      - `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
      - `success_response_definition` string, nullable: Retired by OR-3797; the outcome tag option holds the success rubric
    - `bot` BotConfig
      - `allow_interruptions` boolean
      - `answer_delay` number
      - `idle_timeout_secs` number, nullable
    - `reattempt` ReattemptConfig
      - `by_outcome` ReattemptByOutcome: Hours to wait before the next attempt, keyed by the interaction insight.
        - `voicemail_box_full` integer
        - `voicemail_left` integer
        - `line_busy` integer
        - `call_followup_requested` number, nullable: Stored, not read at runtime
        - `sms_followup_requested` number, nullable: Stored, not read at runtime
        - `no_answer` integer, nullable: Not an insight name; never read
        - `busy` integer, nullable: Not an insight name; line_busy is read instead
      - `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
      - `continue_after_transfer` boolean: Keep the Brain's journey alive after a transfer; the campaign guidelines then define the post-transfer follow-up
      - `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' | 'callgrid': 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).
      - `callgrid` CallGridTransferConfig: Credential references only; Grid IDs stay in organization integrations.
        - `integration_id` string, required
        - `revision` string, required
        - `vertical` 'auto' | 'home', required
        - `field_mapping` object
      - `whisper` TransferWhisperConfig: Private briefing before a non-blind transfer. Keypad acceptance is the default. LiveKit consultations and private Vonage destination legs for Daily can opt into automatic human readiness or shadow observations. Timeout and failure behavior follow the transfer policy.
        - `enabled` boolean
        - `human_detection` DestinationDetectionConfig
          - `mode` 'keypad' | 'shadow' | 'automatic'
          - `stt_model` string
          - `stt_language` string
          - `min_transcript_confidence` number
          - `speech_threshold` number
          - `music_threshold` number
          - `confirmation_seconds` number
          - `vad_params` object, nullable
          - `approval_required` boolean
          - `whisper_text` string
          - `message_enabled` boolean
          - `campaign_id` string, nullable
          - `tts_config` object, nullable
          - `llm_config` object, nullable
        - `message_template` string, nullable
        - `acceptance_prompt_template` string, nullable
        - `handoff_message_template` string, nullable
        - `message_enabled` boolean
        - `dynamic_message_enabled` boolean
        - `timeout_seconds` number
        - `auto_bridge_on_timeout` boolean
        - `timeout_callback_message` string
        - `handoff_grace_seconds` number
        - `max_message_plays` integer
        - `message_gap_seconds` number
      - `student_wait_updates` TransferWaitUpdateConfig: Opt-in spoken updates on the caller leg during a transfer wait. Absent means the wait is unchanged. ``play_music`` is independent: updates can run with or without the hold bed between lines.
        - `enabled` boolean
        - `interval_seconds` number
        - `custom_message` string, nullable
        - `max_update_plays` integer
        - `events` TransferWaitUpdateEvent[]
        - `event_messages` TransferWaitEventMessage[]
          - `event` 'transfer_detected' | 'transfer_initiated' | 'transfer_connected' | 'destination_ringing' | 'destination_answered' | 'destination_reached_music' | 'destination_still_in_music' | 'destination_reached_agent' | 'transfer_failed' | 'destination_hung_up', required: Journey events the caller can hear while a transfer is in progress.
          - `message` string, required
        - `play_music` boolean
        - `music_track` 'generated' | 'airport_lounge' | 'lobby_time' | 'ultralounge' | 'smooth_lovin' | 'backbay_lounge' | 'bossa_antigua' | 'jazz_brunch' | 'local_forecast_elevator': Fixed hold beds. ``generated`` is the original synthesized tone. Recorded values map to files shipped with the API. They are not URLs.
        - `music_volume` integer
    - `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
        - `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
    - `languages` LanguagesConfig: Languages the agent speaks (``campaigns.configuration.languages``). ``inherit`` (default) takes the organization's set. With ``inherit=false`` the campaign's own ``supported`` set applies; ``default`` is the opening language and must be in that set. Codes are normalized to canonical registry codes (``app.models.languages``); ``resolve_agent_languages`` resolves the effective set. Reads are lenient: unknown or disabled codes, and a ``default`` outside the own set, are dropped with a warning. Validating with ``context=LANGUAGES_WRITE_CONTEXT`` rejects them instead.
      - `inherit` boolean
      - `default` string, nullable
      - `supported` string[], nullable
    - `organization_id` string, nullable
    - `persona_name` string, nullable
    - `voice_opt_out_message` 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
        - `source_id` 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
        - `source_id` 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
            - `source_id` 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
            - `source_id` 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, nullable
        - `options` TagOption[]
          - `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
    - `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_guidelines` string, nullable: Free-text Brain guidelines (joma-web Advanced tab); declared so the door dump carries a pinned per-arm value
    - `brain_live` boolean, nullable
    - `brain_model` BrainModelConfig: ``configuration.brain_model``: the model joma-brain runs for this campaign.
      - `provider` string, nullable
      - `model` string, nullable
      - `fallback` BrainModelConfig (recursive)
    - `brain_persona` string, nullable
    - `brain_guardrails` string, nullable
    - `brain_tools_disabled` string[], nullable
    - `agent_setup` object, nullable
    - `cortex_control` boolean, nullable
    - `cortex_entry` 'start' | 'connect', nullable
    - `cortex_entry_cutoff` string, nullable
    - `knowledge_release` KnowledgeRelease: The knowledge-base version a workstream publishes. Keys are stored in camelCase.
      - `sourceId` string, nullable
      - `sourceFingerprint` string, nullable
    - `redaction` RedactionConfig: Per-campaign override for joma-pii-redaction. It owns the category names.
      - `disabled_categories` string[], 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

## Changes

- **2026-11-02** `f27d90300b5e`: 8 warning
  - removed the request property `configuration/campaign_reference`
  - removed the request property `configuration/cortex_dev_only`
  - removed the request property `configuration/inbound_script`
  - removed the request property `configuration/operating_hours`
  - …4 more
- **2026-10-28** `f419a3c85b21`: 6 breaking, 1 warning, 4 info
  - removed `#/components/schemas/AfterHoursActionType, subschema #2` from the `configuration/always_forward_action/anyOf[subschema #1: AfterHoursAction]/action` request property `anyOf` list
  - removed `#/components/schemas/AfterHoursActionType, subschema #2` from the `configuration/hours_of_operation/anyOf[subschema #1: CampaignHoursOfOperation]/after_hours_action/anyOf[subschema #1: AfterHoursAction]/action` request property `anyOf` list
  - request property `configuration/always_forward_action/anyOf[subschema #1: AfterHoursAction]/action` was restricted to a list of enum values
  - request property `configuration/hours_of_operation/anyOf[subschema #1: CampaignHoursOfOperation]/after_hours_action/anyOf[subschema #1: AfterHoursAction]/action` was restricted to a list of enum values
  - …7 more
- **2026-10-22** `74d03a46806e`: 1 info
  - added the new optional request property `configuration/languages`
- …earlier changes not shown

[Operation history](https://skmtc.dev/outrival/apis/outrival-api/changes/api/v1/campaigns/:campaign_id/configuration/put.md)

---

[API](https://skmtc.dev/outrival/apis/outrival-api.md) - [All operations](https://skmtc.dev/outrival/apis/outrival-api/llms.txt) - [OpenAPI document](https://skmtc.dev/outrival/apis/outrival-api/revisions/f27d90300b5e?raw)
