# client.campaigns

> The campaigns methods of the morevoice Python SDK: Outbound calling campaigns.

Outbound calling campaigns. These methods are on `client.campaigns`, where `client` is a `MoreVoice` client (see [the Python SDK](https://docs.morevoice.ai/sdk/python/#connect)). On `AsyncMoreVoice` the same methods are awaited. Each one returns the response object and raises an exception when the API answers with an error.

## `cancel()`

**Cancel a campaign.** Stops the campaign for good: unanswered calls are hung up and the contacts not yet dialled are cancelled.

```python
# client.campaigns
def cancel(self, id: str, body: _m.CampaignActionParamsInput | Mapping[str, Any] | Unset = UNSET, *, more_voice_version: str | Unset = UNSET, idempotency_key: str | Unset = UNSET) -> _m.Campaign
```

`client.campaigns.cancel()` · `await async_client.campaigns.cancel()` · `POST /campaigns/{id}/cancel` · [API reference](https://docs.morevoice.ai/api/operations/campaigns_cancel/)

### Parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | `str` | yes | A campaign ID (`cmp_…`). |

### Returns

`Campaign`:

| Field | Type | Description |
| --- | --- | --- |
| `id` | `string` | A campaign ID (prefix `cmp_`). |
| `object` | `"campaign"` | Always `campaign`. |
| `livemode` | `boolean` | `true` in live mode, `false` in test mode. |
| `name` | `string` | — |
| `state` | `"draft" \| "scheduled" \| "running" \| "paused" \| "completed" \| "cancelled"` | draft → running ⇄ paused → completed \| cancelled (scheduled: waits for schedule.start_at). |
| `purpose` | `"marketing" \| "service" \| "survey"` | marketing needs recorded consent with evidence (§30A) and a national-registry check for Israeli numbers. |
| `assistant_id` | `string \| null` | `asst_…` ID. |
| `connection_id` | `string \| null` | The SIP connection it dials through; null: the default one. |
| `caller_id` | `string \| null` | The number presented to contacts; null: the connection's default. |
| `priority` | `integer` | 1–10; higher is served first when campaigns compete for lines. |
| `max_concurrent` | `integer` | Simultaneous calls this campaign may hold. |
| `calls_per_second` | `integer` | New calls per second; 0: only the connection limits it. |
| `schedule` | `CampaignSchedule` | When the campaign dials. |
| `ring_timeout_seconds` | `integer` | Hang up an unanswered call after this long. |
| `retry` | `CampaignRetry` | Retry policy per call result. |
| `amd` | `CampaignAmd` | Answering-machine detection. |
| `disclosure` | `CampaignDisclosure` | The AI and recording disclosure played at the start of each call. |
| `first_message` | `string` | Overrides the assistant's first message ({{variables}} allowed); empty: the assistant's. |
| `script` | `string` | Extra instructions for this campaign ({{variables}} allowed). |
| `dispositions` | `string[]` | Outcomes the assistant can record. |
| `opt_out_dtmf_key` | `string \| null` | The key that opts a contact out; null: the organisation's. |
| `pause_reason` | `string \| null` | Why the campaign paused itself (for example, the national registry can't check it). |
| `stats` | `CampaignStats` | Contact counts per status and the calls in progress. |
| `created_at` | `string` | An ISO-8601 timestamp in UTC. |
| `updated_at` | `string` | An ISO-8601 timestamp in UTC. |
| `started_at` | `string \| null` | — |
| `completed_at` | `string \| null` | — |

### Example

```python
from morevoice import MoreVoice

client = MoreVoice()  # MOREVOICE_API_KEY from the environment

campaign = client.campaigns.cancel("cmp_7Hk2Lm9Qp", {})
print(campaign)
```

## `contacts.create()`

**Add contacts to a campaign.** Adds up to 1,000 contacts and answers what was added and what was left out, like a dashboard import of the same list: numbers are normalised and de-duplicated, do-not-call numbers are kept as dnc (never dialled), and in a marketing campaign a contact without consent evidence is added but skipped by the dialer (§30A).

```python
# client.campaigns.contacts
def create(self, id: str, body: _m.ContactBatchParamsInput | Mapping[str, Any], *, more_voice_version: str | Unset = UNSET, idempotency_key: str | Unset = UNSET) -> _m.ContactBatchResult
```

`client.campaigns.contacts.create()` · `await async_client.campaigns.contacts.create()` · `POST /campaigns/{id}/contacts` · [API reference](https://docs.morevoice.ai/api/operations/campaigns_contacts_create/)

### Parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | `str` | yes | A campaign ID (`cmp_…`). |

### Request body

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `contacts` | `ContactInputInput[]` | yes | Up to 1000 contacts. |
| `default_country` | `string` | no | Country of numbers written without a country code (ISO 3166-1 alpha-2, default IL). |
| `drop_dnc` | `boolean` | no | Leave numbers on the do-not-call list out entirely (default false: they are kept with status dnc, for the record, and never dialled). |

### Returns

`ContactBatchResult`:

| Field | Type | Description |
| --- | --- | --- |
| `object` | `"contact_batch_result"` | Always `contact_batch_result`. |
| `campaign_id` | `string` | A campaign ID (prefix `cmp_`). |
| `livemode` | `boolean` | `true` in live mode, `false` in test mode. |
| `received` | `integer` | Non-empty contacts / rows received. |
| `imported` | `integer` | Contacts added to the campaign for dialling. |
| `rejected` | `integer` | Contacts not added for dialling (invalid, duplicate, do-not-call). |
| `rejected_reasons` | `ContactRejectedReasons` | Rejected contacts per reason. |
| `without_consent` | `integer` | Marketing: contacts added without a consent record. The dialer skips them (§30A) unless consent is recorded first. |
| `consent_recorded` | `integer` | Consent records created from the evidence sent. |
| `errors` | `object[]` | Rejected contacts (the first 100). |
| `warnings` | `object[]` | Contacts added with a problem, e.g. consent claimed without complete evidence (the first 100). |

### Example

```python
from morevoice import MoreVoice

client = MoreVoice()  # MOREVOICE_API_KEY from the environment

contact_batch_result = client.campaigns.contacts.create("cmp_7Hk2Lm9Qp", {
    "contacts": [
        {
            "consent": {
                "channel": "web",
                "granted_at": "2026-09-20T14:02:00+03:00",
                "source": "website_form_2026",
            },
            "name": "Dana Levi",
            "phone": "+972501234567",
            "variables": {
                "plan": "Gold",
            },
        },
        {
            "name": "Yossi Cohen",
            "phone": "052-765-4321",
            "variables": {
                "plan": "Silver",
            },
        },
    ],
    "default_country": "IL",
})
print(contact_batch_result)
```

## `contacts.import_()`

**Import a contact list in the background.** Imports a CSV or XLSX list in the background through the dashboard's import path and answers 202 with the import; poll GET /v1/imports/{id} until it succeeds or fails. Send the file as multipart/form-data (a `file` part plus the options as text fields, `mapping` as a JSON string), or as JSON: CSV text, a base64 file, or an https URL to download. An Idempotency-Key is required, so a retried upload never imports twice.

```python
# client.campaigns.contacts
def import_(self, id: str, body: _m.ContactImportParamsInput | _m.CampaignsContactsImportFilesBody | Mapping[str, Any] | Unset = UNSET, *, more_voice_version: str | Unset = UNSET, idempotency_key: str | Unset = UNSET) -> _m.ContactImport
```

`client.campaigns.contacts.import_()` · `await async_client.campaigns.contacts.import_()` · `POST /campaigns/{id}/contacts/import` · [API reference](https://docs.morevoice.ai/api/operations/campaigns_contacts_import/)

### Parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | `str` | yes | A campaign ID (`cmp_…`). |

### Request body

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `consent_channel` | `"web" \| "phone" \| "sms" \| "email" \| "whatsapp" \| "app" \| "written" \| "in_person" \| …` | no | How every consenting row consented, when the file has no channel column. |
| `consent_source` | `string` | no | Where every consenting row consented, when the file has no source column. |
| `csv` | `string` | no | The list as CSV text (UTF-8; comma, semicolon, tab or pipe separated). |
| `default_country` | `string` | no | Country of numbers written without a country code (ISO 3166-1 alpha-2, default IL). |
| `drop_dnc` | `boolean` | no | Leave do-not-call numbers out entirely (default false: kept with status dnc, never dialled). |
| `file` | `object` | no | A CSV or XLSX file. |
| `has_header` | `boolean` | no | The first row holds column names (default: detected). |
| `mapping` | `ImportMappingInput` | no | Which column holds what: contact field → header name (or 0-based index). Any other key names a {{variable}}. Omit the mapping to detect the columns like the dashboard does. |
| `url` | `string` | no | An https URL to download the CSV / XLSX from (public addresses only, ≤ 10 MB). |

### Returns

`ContactImport`:

| Field | Type | Description |
| --- | --- | --- |
| `id` | `string` | A import ID (prefix `imp_`). |
| `object` | `"import"` | Always `import`. |
| `livemode` | `boolean` | `true` in live mode, `false` in test mode. |
| `campaign_id` | `string` | A campaign ID (prefix `cmp_`). |
| `status` | `"pending" \| "processing" \| "succeeded" \| "failed"` | pending → processing → succeeded \| failed. Poll GET /v1/imports/{id}. |
| `source` | `"csv" \| "xlsx" \| "url"` | — |
| `filename` | `string \| null` | — |
| `received` | `integer` | Non-empty contacts / rows received. |
| `imported` | `integer` | Contacts added to the campaign for dialling. |
| `rejected` | `integer` | Contacts not added for dialling (invalid, duplicate, do-not-call). |
| `rejected_reasons` | `ContactRejectedReasons` | Rejected contacts per reason. |
| `without_consent` | `integer` | Marketing: contacts added without a consent record. The dialer skips them (§30A) unless consent is recorded first. |
| `consent_recorded` | `integer` | Consent records created from the evidence sent. |
| `errors` | `object[]` | Rejected rows (the first 100). |
| `warnings` | `object[]` | Rows added with a problem (the first 100). |
| `error` | `object \| null` | Why the import failed (status failed): a stable code and a message to show. |
| `created_at` | `string` | An ISO-8601 timestamp in UTC. |
| `started_at` | `string \| null` | — |
| `completed_at` | `string \| null` | — |

### Example

```python
from morevoice import MoreVoice

client = MoreVoice()  # MOREVOICE_API_KEY from the environment

contact_import = client.campaigns.contacts.import_("cmp_7Hk2Lm9Qp", {
    "csv": "Mobile,Name,Plan\n0501234567,Dana Levi,Gold\n0527654321,Yossi Cohen,Silver\n",
    "default_country": "IL",
    "mapping": {
        "name": "Name",
        "phone": "Mobile",
        "variables": {
            "plan": "Plan",
        },
    },
})
print(contact_import)
```

## `contacts.list()`

**List a campaign's contacts.** Lists the contacts of a campaign, newest first, with each contact's status, attempts and outcome. Filter by `status` (several, comma-separated), by `outcome` (the recorded disposition) or by `phone`.

```python
# client.campaigns.contacts
def list(self, id: str, *, limit: int | Unset = 20, starting_after: str | Unset = UNSET, ending_before: str | Unset = UNSET, status: str | Unset = UNSET, outcome: str | Unset = UNSET, phone: str | Unset = UNSET, more_voice_version: str | Unset = UNSET) -> AsyncPage[_m.CampaignContact]
```

`client.campaigns.contacts.list()` · `await async_client.campaigns.contacts.list()` · `GET /campaigns/{id}/contacts` · [API reference](https://docs.morevoice.ai/api/operations/campaigns_contacts_list/)

### Parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | `str` | yes | A campaign ID (`cmp_…`). |
| `limit` | `integer` | no | How many objects to return, 1–100 (default 20). |
| `starting_after` | `string` | no | A cursor (`next_cursor`) or object ID: return the objects after it (older). |
| `ending_before` | `string` | no | A cursor or object ID: return the objects before it (newer). |
| `status` | `string` | no | Only these statuses, comma-separated (e.g. pending,scheduled). |
| `outcome` | `string` | no | Only contacts with this recorded disposition. |
| `phone` | `string` | no | Only this number. |

### Returns

A page of results (`CampaignContactList`): `data`, `has_more` and `next_cursor`.

### Example

```python
from morevoice import MoreVoice

client = MoreVoice()  # MOREVOICE_API_KEY from the environment

for contact in client.campaigns.contacts.list("cmp_7Hk2Lm9Qp"):
    print(contact)
```

## `contacts.list_attempts()`

**List a contact's dial attempts.** Lists the dial attempts made to one contact of the campaign, newest first: when each one started, how it ended, and the call it placed.

```python
# client.campaigns.contacts
def list_attempts(self, id: str, contact_id: str, *, limit: int | Unset = 20, starting_after: str | Unset = UNSET, ending_before: str | Unset = UNSET, more_voice_version: str | Unset = UNSET) -> AsyncPage[_m.CampaignAttempt]
```

`client.campaigns.contacts.list_attempts()` · `await async_client.campaigns.contacts.list_attempts()` · `GET /campaigns/{id}/contacts/{contact_id}/attempts` · [API reference](https://docs.morevoice.ai/api/operations/campaigns_contacts_list_attempts/)

### Parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | `str` | yes | A campaign ID (`cmp_…`). |
| `contact_id` | `str` | yes | A contact ID (`ctc_…`). |
| `limit` | `integer` | no | How many objects to return, 1–100 (default 20). |
| `starting_after` | `string` | no | A cursor (`next_cursor`) or object ID: return the objects after it (older). |
| `ending_before` | `string` | no | A cursor or object ID: return the objects before it (newer). |

### Returns

A page of results (`CampaignAttemptList`): `data`, `has_more` and `next_cursor`.

### Example

```python
from morevoice import MoreVoice

client = MoreVoice()  # MOREVOICE_API_KEY from the environment

for contact in client.campaigns.contacts.list_attempts("cmp_7Hk2Lm9Qp", "ctc_7Hk2Lm9Qp"):
    print(contact)
```

## `contacts.retrieve()`

**Retrieve a campaign contact.** Returns one contact of a campaign: the number, its variables, its status, how many times it was dialled and the outcome.

```python
# client.campaigns.contacts
def retrieve(self, id: str, contact_id: str, *, more_voice_version: str | Unset = UNSET) -> _m.CampaignContact
```

`client.campaigns.contacts.retrieve()` · `await async_client.campaigns.contacts.retrieve()` · `GET /campaigns/{id}/contacts/{contact_id}` · [API reference](https://docs.morevoice.ai/api/operations/campaigns_contacts_retrieve/)

### Parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | `str` | yes | A campaign ID (`cmp_…`). |
| `contact_id` | `str` | yes | A contact ID (`ctc_…`). |

### Returns

`CampaignContact`:

| Field | Type | Description |
| --- | --- | --- |
| `id` | `string` | A contact ID (prefix `ctc_`). |
| `object` | `"campaign_contact"` | Always `campaign_contact`. |
| `livemode` | `boolean` | `true` in live mode, `false` in test mode. |
| `campaign_id` | `string` | A campaign ID (prefix `cmp_`). |
| `phone` | `string` | E.164. |
| `name` | `string` | — |
| `variables` | `Record<string, string>` | — |
| `timezone` | `string \| null` | — |
| `consent` | `boolean` | The import said the person consented. Marketing calls also need a consent record with evidence (§30A). |
| `status` | `"pending" \| "scheduled" \| "callback" \| "dialing" \| "done" \| "failed" \| "dnc" \| "skipped" \| …` | pending → dialing → scheduled \| callback → … → done \| failed; skipped, dnc, invalid and cancelled are never dialled. |
| `skip_reason` | `string \| null` | Why it is not dialled: dnc_import, dnc_dial_time, registry, no_consent, no_consent_evidence, no_calling_window, manual, … |
| `attempts` | `integer` | — |
| `next_attempt_at` | `string \| null` | When it is due (dialable statuses only). |
| `last_result` | `string \| null` | The last attempt's result (answered, no_answer, busy, voicemail, …). |
| `outcome` | `string \| null` | The disposition the assistant recorded. |
| `outcome_data` | `object \| null` | — |
| `last_call_id` | `string \| null` | `call_…` ID. |
| `last_attempt_at` | `string \| null` | — |
| `completed_at` | `string \| null` | — |
| `row` | `integer \| null` | The row it came from in its import. |
| `created_at` | `string` | An ISO-8601 timestamp in UTC. |
| `updated_at` | `string` | An ISO-8601 timestamp in UTC. |

### Example

```python
from morevoice import MoreVoice

client = MoreVoice()  # MOREVOICE_API_KEY from the environment

campaign_contact = client.campaigns.contacts.retrieve("cmp_7Hk2Lm9Qp", "ctc_7Hk2Lm9Qp")
print(campaign_contact)
```

## `contacts.update()`

**Requeue or skip a campaign contact.** status scheduled dials the contact again now (refused for a number on the do-not-call list); skipped takes it out of the campaign. A contact being dialled can't change.

```python
# client.campaigns.contacts
def update(self, id: str, contact_id: str, body: _m.ContactUpdateParamsInput | Mapping[str, Any], *, more_voice_version: str | Unset = UNSET) -> _m.CampaignContact
```

`client.campaigns.contacts.update()` · `await async_client.campaigns.contacts.update()` · `PATCH /campaigns/{id}/contacts/{contact_id}` · [API reference](https://docs.morevoice.ai/api/operations/campaigns_contacts_update/)

### Parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | `str` | yes | A campaign ID (`cmp_…`). |
| `contact_id` | `str` | yes | A contact ID (`ctc_…`). |

### Request body

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `status` | `"scheduled" \| "skipped"` | yes | scheduled: dial it again now (refused for a do-not-call number); skipped: take it out of the campaign. |

### Returns

`CampaignContact`:

| Field | Type | Description |
| --- | --- | --- |
| `id` | `string` | A contact ID (prefix `ctc_`). |
| `object` | `"campaign_contact"` | Always `campaign_contact`. |
| `livemode` | `boolean` | `true` in live mode, `false` in test mode. |
| `campaign_id` | `string` | A campaign ID (prefix `cmp_`). |
| `phone` | `string` | E.164. |
| `name` | `string` | — |
| `variables` | `Record<string, string>` | — |
| `timezone` | `string \| null` | — |
| `consent` | `boolean` | The import said the person consented. Marketing calls also need a consent record with evidence (§30A). |
| `status` | `"pending" \| "scheduled" \| "callback" \| "dialing" \| "done" \| "failed" \| "dnc" \| "skipped" \| …` | pending → dialing → scheduled \| callback → … → done \| failed; skipped, dnc, invalid and cancelled are never dialled. |
| `skip_reason` | `string \| null` | Why it is not dialled: dnc_import, dnc_dial_time, registry, no_consent, no_consent_evidence, no_calling_window, manual, … |
| `attempts` | `integer` | — |
| `next_attempt_at` | `string \| null` | When it is due (dialable statuses only). |
| `last_result` | `string \| null` | The last attempt's result (answered, no_answer, busy, voicemail, …). |
| `outcome` | `string \| null` | The disposition the assistant recorded. |
| `outcome_data` | `object \| null` | — |
| `last_call_id` | `string \| null` | `call_…` ID. |
| `last_attempt_at` | `string \| null` | — |
| `completed_at` | `string \| null` | — |
| `row` | `integer \| null` | The row it came from in its import. |
| `created_at` | `string` | An ISO-8601 timestamp in UTC. |
| `updated_at` | `string` | An ISO-8601 timestamp in UTC. |

### Example

```python
from morevoice import MoreVoice

client = MoreVoice()  # MOREVOICE_API_KEY from the environment

campaign_contact = client.campaigns.contacts.update("cmp_7Hk2Lm9Qp", "ctc_7Hk2Lm9Qp", {
    "status": "skipped",
})
print(campaign_contact)
```

## `create()`

**Create a campaign.** Creates a draft campaign. Add contacts, then start it. The purpose decides the compliance checks: marketing needs consent with evidence (§30A) and, for Israeli numbers, the national do-not-call registry.

```python
# client.campaigns
def create(self, body: _m.CampaignCreateParamsInput | Mapping[str, Any], *, more_voice_version: str | Unset = UNSET, idempotency_key: str | Unset = UNSET) -> _m.Campaign
```

`client.campaigns.create()` · `await async_client.campaigns.create()` · `POST /campaigns` · [API reference](https://docs.morevoice.ai/api/operations/campaigns_create/)

### Request body

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | `string` | yes | — |
| `purpose` | `"marketing" \| "service" \| "survey"` | yes | Required on create: marketing, service or survey. It decides the compliance checks. |
| `amd` | `object` | no | — |
| `assistant_id` | `string \| null` | no | `asst_…` ID. |
| `caller_id` | `string \| null` | no | — |
| `calls_per_second` | `integer` | no | — |
| `connection_id` | `string \| null` | no | `conn_…` ID. |
| `disclosure` | `object` | no | — |
| `dispositions` | `string[]` | no | — |
| `first_message` | `string` | no | Overrides the assistant's first message. {{variables}} come from the contacts' variables, name and phone. |
| `max_concurrent` | `integer` | no | — |
| `opt_out_dtmf_key` | `string \| null` | no | — |
| `priority` | `integer` | no | — |
| `retry` | `object` | no | — |
| `ring_timeout_seconds` | `integer` | no | — |
| `schedule` | `object` | no | — |
| `script` | `string` | no | Extra instructions for the assistant. {{variables}} come from the contacts' variables, name and phone. |

### Returns

`Campaign`:

| Field | Type | Description |
| --- | --- | --- |
| `id` | `string` | A campaign ID (prefix `cmp_`). |
| `object` | `"campaign"` | Always `campaign`. |
| `livemode` | `boolean` | `true` in live mode, `false` in test mode. |
| `name` | `string` | — |
| `state` | `"draft" \| "scheduled" \| "running" \| "paused" \| "completed" \| "cancelled"` | draft → running ⇄ paused → completed \| cancelled (scheduled: waits for schedule.start_at). |
| `purpose` | `"marketing" \| "service" \| "survey"` | marketing needs recorded consent with evidence (§30A) and a national-registry check for Israeli numbers. |
| `assistant_id` | `string \| null` | `asst_…` ID. |
| `connection_id` | `string \| null` | The SIP connection it dials through; null: the default one. |
| `caller_id` | `string \| null` | The number presented to contacts; null: the connection's default. |
| `priority` | `integer` | 1–10; higher is served first when campaigns compete for lines. |
| `max_concurrent` | `integer` | Simultaneous calls this campaign may hold. |
| `calls_per_second` | `integer` | New calls per second; 0: only the connection limits it. |
| `schedule` | `CampaignSchedule` | When the campaign dials. |
| `ring_timeout_seconds` | `integer` | Hang up an unanswered call after this long. |
| `retry` | `CampaignRetry` | Retry policy per call result. |
| `amd` | `CampaignAmd` | Answering-machine detection. |
| `disclosure` | `CampaignDisclosure` | The AI and recording disclosure played at the start of each call. |
| `first_message` | `string` | Overrides the assistant's first message ({{variables}} allowed); empty: the assistant's. |
| `script` | `string` | Extra instructions for this campaign ({{variables}} allowed). |
| `dispositions` | `string[]` | Outcomes the assistant can record. |
| `opt_out_dtmf_key` | `string \| null` | The key that opts a contact out; null: the organisation's. |
| `pause_reason` | `string \| null` | Why the campaign paused itself (for example, the national registry can't check it). |
| `stats` | `CampaignStats` | Contact counts per status and the calls in progress. |
| `created_at` | `string` | An ISO-8601 timestamp in UTC. |
| `updated_at` | `string` | An ISO-8601 timestamp in UTC. |
| `started_at` | `string \| null` | — |
| `completed_at` | `string \| null` | — |

### Example

```python
from morevoice import MoreVoice

client = MoreVoice()  # MOREVOICE_API_KEY from the environment

campaign = client.campaigns.create({
    "assistant_id": "asst_3kTzL9Qe2R",
    "name": "November renewals",
    "purpose": "marketing",
    "retry": {
        "busy": {
            "delay_minutes": 30,
        },
        "max_attempts": 3,
        "no_answer": {
            "delay_minutes": 240,
        },
    },
    "schedule": {
        "respect_shabbat": True,
        "timezone": "Asia/Jerusalem",
        "windows": [
            {
                "days": [
                    "sun",
                    "mon",
                    "tue",
                    "wed",
                    "thu",
                ],
                "end": "19:00",
                "start": "09:00",
            },
        ],
    },
})
print(campaign)
```

## `delete()`

**Delete a campaign.** Deletes a campaign that is not running or scheduled, with its contacts and attempts. Calls already made stay in the call log.

```python
# client.campaigns
def delete(self, id: str, *, more_voice_version: str | Unset = UNSET, idempotency_key: str | Unset = UNSET) -> _m.DeletedCampaign
```

`client.campaigns.delete()` · `await async_client.campaigns.delete()` · `DELETE /campaigns/{id}` · [API reference](https://docs.morevoice.ai/api/operations/campaigns_delete/)

### Parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | `str` | yes | A campaign ID (`cmp_…`). |

### Returns

`DeletedCampaign`:

| Field | Type | Description |
| --- | --- | --- |
| `id` | `string` | A campaign ID (prefix `cmp_`). |
| `object` | `"campaign"` | Always `campaign`. |
| `deleted` | `true` | — |

### Example

```python
from morevoice import MoreVoice

client = MoreVoice()  # MOREVOICE_API_KEY from the environment

deleted_campaign = client.campaigns.delete("cmp_7Hk2Lm9Qp")
print(deleted_campaign)
```

## `export()`

**Export a campaign's contacts or attempts as CSV.** The dashboard's export, streamed: UTF-8 with a byte-order mark (opens in Excel with Hebrew intact). kind=contacts (default) has one row per contact with its result and variables; kind=attempts one row per dial attempt.

```python
# client.campaigns
def export(self, id: str, *, kind: _m.CampaignsExportKind | Unset = "contacts", more_voice_version: str | Unset = UNSET) -> str
```

`client.campaigns.export()` · `await async_client.campaigns.export()` · `GET /campaigns/{id}/export` · [API reference](https://docs.morevoice.ai/api/operations/campaigns_export/)

### Parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | `str` | yes | A campaign ID (`cmp_…`). |
| `kind` | `"contacts" \| "attempts"` | no | contacts (default) or attempts. |

### Returns

Nothing, on success.

### Example

```python
from morevoice import MoreVoice

client = MoreVoice()  # MOREVOICE_API_KEY from the environment

data = client.campaigns.export("cmp_7Hk2Lm9Qp")
print(data)
```

## `list()`

**List campaigns.** Lists the campaigns of this mode, newest first, with each campaign's contact counts. Filter by `state` or `purpose`.

```python
# client.campaigns
def list(self, *, limit: int | Unset = 20, starting_after: str | Unset = UNSET, ending_before: str | Unset = UNSET, state: _m.CampaignsListState | Unset = UNSET, purpose: _m.CampaignsListPurpose | Unset = UNSET, more_voice_version: str | Unset = UNSET) -> AsyncPage[_m.Campaign]
```

`client.campaigns.list()` · `await async_client.campaigns.list()` · `GET /campaigns` · [API reference](https://docs.morevoice.ai/api/operations/campaigns_list/)

### Parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `limit` | `integer` | no | How many objects to return, 1–100 (default 20). |
| `starting_after` | `string` | no | A cursor (`next_cursor`) or object ID: return the objects after it (older). |
| `ending_before` | `string` | no | A cursor or object ID: return the objects before it (newer). |
| `state` | `"draft" \| "scheduled" \| "running" \| "paused" \| "completed" \| "cancelled"` | no | Only campaigns in this state. |
| `purpose` | `"marketing" \| "service" \| "survey"` | no | Only campaigns with this purpose. |

### Returns

A page of results (`CampaignList`): `data`, `has_more` and `next_cursor`.

### Example

```python
from morevoice import MoreVoice

client = MoreVoice()  # MOREVOICE_API_KEY from the environment

for campaign in client.campaigns.list():
    print(campaign)
```

## `pause()`

**Pause a campaign.** Stops dialling new calls; calls in progress finish. Resume it to continue.

```python
# client.campaigns
def pause(self, id: str, body: _m.CampaignActionParamsInput | Mapping[str, Any] | Unset = UNSET, *, more_voice_version: str | Unset = UNSET, idempotency_key: str | Unset = UNSET) -> _m.Campaign
```

`client.campaigns.pause()` · `await async_client.campaigns.pause()` · `POST /campaigns/{id}/pause` · [API reference](https://docs.morevoice.ai/api/operations/campaigns_pause/)

### Parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | `str` | yes | A campaign ID (`cmp_…`). |

### Returns

`Campaign`:

| Field | Type | Description |
| --- | --- | --- |
| `id` | `string` | A campaign ID (prefix `cmp_`). |
| `object` | `"campaign"` | Always `campaign`. |
| `livemode` | `boolean` | `true` in live mode, `false` in test mode. |
| `name` | `string` | — |
| `state` | `"draft" \| "scheduled" \| "running" \| "paused" \| "completed" \| "cancelled"` | draft → running ⇄ paused → completed \| cancelled (scheduled: waits for schedule.start_at). |
| `purpose` | `"marketing" \| "service" \| "survey"` | marketing needs recorded consent with evidence (§30A) and a national-registry check for Israeli numbers. |
| `assistant_id` | `string \| null` | `asst_…` ID. |
| `connection_id` | `string \| null` | The SIP connection it dials through; null: the default one. |
| `caller_id` | `string \| null` | The number presented to contacts; null: the connection's default. |
| `priority` | `integer` | 1–10; higher is served first when campaigns compete for lines. |
| `max_concurrent` | `integer` | Simultaneous calls this campaign may hold. |
| `calls_per_second` | `integer` | New calls per second; 0: only the connection limits it. |
| `schedule` | `CampaignSchedule` | When the campaign dials. |
| `ring_timeout_seconds` | `integer` | Hang up an unanswered call after this long. |
| `retry` | `CampaignRetry` | Retry policy per call result. |
| `amd` | `CampaignAmd` | Answering-machine detection. |
| `disclosure` | `CampaignDisclosure` | The AI and recording disclosure played at the start of each call. |
| `first_message` | `string` | Overrides the assistant's first message ({{variables}} allowed); empty: the assistant's. |
| `script` | `string` | Extra instructions for this campaign ({{variables}} allowed). |
| `dispositions` | `string[]` | Outcomes the assistant can record. |
| `opt_out_dtmf_key` | `string \| null` | The key that opts a contact out; null: the organisation's. |
| `pause_reason` | `string \| null` | Why the campaign paused itself (for example, the national registry can't check it). |
| `stats` | `CampaignStats` | Contact counts per status and the calls in progress. |
| `created_at` | `string` | An ISO-8601 timestamp in UTC. |
| `updated_at` | `string` | An ISO-8601 timestamp in UTC. |
| `started_at` | `string \| null` | — |
| `completed_at` | `string \| null` | — |

### Example

```python
from morevoice import MoreVoice

client = MoreVoice()  # MOREVOICE_API_KEY from the environment

campaign = client.campaigns.pause("cmp_7Hk2Lm9Qp", {})
print(campaign)
```

## `preflight()`

**Check whether a campaign can start.** Runs the checks that starting the campaign runs, without starting it: the same checks as the dashboard's Start button. `ok` says whether it can start; `errors` block the start, `warnings` don't. `window_open` and `next_window_at` say whether the campaign's calling window (and Shabbat and holidays) lets it dial now, and when it next can.

```python
# client.campaigns
def preflight(self, id: str, *, more_voice_version: str | Unset = UNSET) -> _m.CampaignPreflight
```

`client.campaigns.preflight()` · `await async_client.campaigns.preflight()` · `GET /campaigns/{id}/preflight` · [API reference](https://docs.morevoice.ai/api/operations/campaigns_preflight/)

### Parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | `str` | yes | A campaign ID (`cmp_…`). |

### Returns

`CampaignPreflight`:

| Field | Type | Description |
| --- | --- | --- |
| `object` | `"campaign_preflight"` | Always `campaign_preflight`. |
| `campaign_id` | `string` | A campaign ID (prefix `cmp_`). |
| `ok` | `boolean` | No errors: the campaign can start. |
| `errors` | `string[]` | What blocks the start (missing assistant or connection, no contacts, no marketing consent, no registry, …). |
| `warnings` | `string[]` | What the dialer will skip or delay (contacts without consent, outside calling hours now, …). |
| `dialable` | `integer` | Contacts the dialer may call. |
| `window_open` | `boolean` | Calling is allowed right now (calling hours, Shabbat, holidays). |
| `next_window_at` | `string \| null` | When calling is next allowed; null if never in the next 3 weeks. |

### Example

```python
from morevoice import MoreVoice

client = MoreVoice()  # MOREVOICE_API_KEY from the environment

campaign_preflight = client.campaigns.preflight("cmp_7Hk2Lm9Qp")
print(campaign_preflight)
```

## `resume()`

**Resume a paused campaign.** Runs the pre-flight checks again and resumes dialling.

```python
# client.campaigns
def resume(self, id: str, body: _m.CampaignActionParamsInput | Mapping[str, Any] | Unset = UNSET, *, more_voice_version: str | Unset = UNSET, idempotency_key: str | Unset = UNSET) -> _m.Campaign
```

`client.campaigns.resume()` · `await async_client.campaigns.resume()` · `POST /campaigns/{id}/resume` · [API reference](https://docs.morevoice.ai/api/operations/campaigns_resume/)

### Parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | `str` | yes | A campaign ID (`cmp_…`). |

### Returns

`Campaign`:

| Field | Type | Description |
| --- | --- | --- |
| `id` | `string` | A campaign ID (prefix `cmp_`). |
| `object` | `"campaign"` | Always `campaign`. |
| `livemode` | `boolean` | `true` in live mode, `false` in test mode. |
| `name` | `string` | — |
| `state` | `"draft" \| "scheduled" \| "running" \| "paused" \| "completed" \| "cancelled"` | draft → running ⇄ paused → completed \| cancelled (scheduled: waits for schedule.start_at). |
| `purpose` | `"marketing" \| "service" \| "survey"` | marketing needs recorded consent with evidence (§30A) and a national-registry check for Israeli numbers. |
| `assistant_id` | `string \| null` | `asst_…` ID. |
| `connection_id` | `string \| null` | The SIP connection it dials through; null: the default one. |
| `caller_id` | `string \| null` | The number presented to contacts; null: the connection's default. |
| `priority` | `integer` | 1–10; higher is served first when campaigns compete for lines. |
| `max_concurrent` | `integer` | Simultaneous calls this campaign may hold. |
| `calls_per_second` | `integer` | New calls per second; 0: only the connection limits it. |
| `schedule` | `CampaignSchedule` | When the campaign dials. |
| `ring_timeout_seconds` | `integer` | Hang up an unanswered call after this long. |
| `retry` | `CampaignRetry` | Retry policy per call result. |
| `amd` | `CampaignAmd` | Answering-machine detection. |
| `disclosure` | `CampaignDisclosure` | The AI and recording disclosure played at the start of each call. |
| `first_message` | `string` | Overrides the assistant's first message ({{variables}} allowed); empty: the assistant's. |
| `script` | `string` | Extra instructions for this campaign ({{variables}} allowed). |
| `dispositions` | `string[]` | Outcomes the assistant can record. |
| `opt_out_dtmf_key` | `string \| null` | The key that opts a contact out; null: the organisation's. |
| `pause_reason` | `string \| null` | Why the campaign paused itself (for example, the national registry can't check it). |
| `stats` | `CampaignStats` | Contact counts per status and the calls in progress. |
| `created_at` | `string` | An ISO-8601 timestamp in UTC. |
| `updated_at` | `string` | An ISO-8601 timestamp in UTC. |
| `started_at` | `string \| null` | — |
| `completed_at` | `string \| null` | — |

### Example

```python
from morevoice import MoreVoice

client = MoreVoice()  # MOREVOICE_API_KEY from the environment

campaign = client.campaigns.resume("cmp_7Hk2Lm9Qp", {})
print(campaign)
```

## `retrieve()`

**Retrieve a campaign.** Returns a campaign: its settings, its state and its contact counts. `GET /v1/campaigns/{id}/stats` has the detailed results.

```python
# client.campaigns
def retrieve(self, id: str, *, more_voice_version: str | Unset = UNSET) -> _m.Campaign
```

`client.campaigns.retrieve()` · `await async_client.campaigns.retrieve()` · `GET /campaigns/{id}` · [API reference](https://docs.morevoice.ai/api/operations/campaigns_retrieve/)

### Parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | `str` | yes | A campaign ID (`cmp_…`). |

### Returns

`Campaign`:

| Field | Type | Description |
| --- | --- | --- |
| `id` | `string` | A campaign ID (prefix `cmp_`). |
| `object` | `"campaign"` | Always `campaign`. |
| `livemode` | `boolean` | `true` in live mode, `false` in test mode. |
| `name` | `string` | — |
| `state` | `"draft" \| "scheduled" \| "running" \| "paused" \| "completed" \| "cancelled"` | draft → running ⇄ paused → completed \| cancelled (scheduled: waits for schedule.start_at). |
| `purpose` | `"marketing" \| "service" \| "survey"` | marketing needs recorded consent with evidence (§30A) and a national-registry check for Israeli numbers. |
| `assistant_id` | `string \| null` | `asst_…` ID. |
| `connection_id` | `string \| null` | The SIP connection it dials through; null: the default one. |
| `caller_id` | `string \| null` | The number presented to contacts; null: the connection's default. |
| `priority` | `integer` | 1–10; higher is served first when campaigns compete for lines. |
| `max_concurrent` | `integer` | Simultaneous calls this campaign may hold. |
| `calls_per_second` | `integer` | New calls per second; 0: only the connection limits it. |
| `schedule` | `CampaignSchedule` | When the campaign dials. |
| `ring_timeout_seconds` | `integer` | Hang up an unanswered call after this long. |
| `retry` | `CampaignRetry` | Retry policy per call result. |
| `amd` | `CampaignAmd` | Answering-machine detection. |
| `disclosure` | `CampaignDisclosure` | The AI and recording disclosure played at the start of each call. |
| `first_message` | `string` | Overrides the assistant's first message ({{variables}} allowed); empty: the assistant's. |
| `script` | `string` | Extra instructions for this campaign ({{variables}} allowed). |
| `dispositions` | `string[]` | Outcomes the assistant can record. |
| `opt_out_dtmf_key` | `string \| null` | The key that opts a contact out; null: the organisation's. |
| `pause_reason` | `string \| null` | Why the campaign paused itself (for example, the national registry can't check it). |
| `stats` | `CampaignStats` | Contact counts per status and the calls in progress. |
| `created_at` | `string` | An ISO-8601 timestamp in UTC. |
| `updated_at` | `string` | An ISO-8601 timestamp in UTC. |
| `started_at` | `string \| null` | — |
| `completed_at` | `string \| null` | — |

### Example

```python
from morevoice import MoreVoice

client = MoreVoice()  # MOREVOICE_API_KEY from the environment

campaign = client.campaigns.retrieve("cmp_7Hk2Lm9Qp")
print(campaign)
```

## `start()`

**Start a campaign.** Runs the dashboard's pre-flight checks and starts dialling (or schedules the start). A failed check answers 409 preflight_failed with the list. Once running, every call is still checked against the do-not-call list, consent, the national registry, the calling hours and Shabbat; a marketing campaign the registry can't check pauses itself (pause_reason).

```python
# client.campaigns
def start(self, id: str, body: _m.CampaignStartParamsInput | Mapping[str, Any] | Unset = UNSET, *, more_voice_version: str | Unset = UNSET, idempotency_key: str | Unset = UNSET) -> _m.Campaign
```

`client.campaigns.start()` · `await async_client.campaigns.start()` · `POST /campaigns/{id}/start` · [API reference](https://docs.morevoice.ai/api/operations/campaigns_start/)

### Parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | `str` | yes | A campaign ID (`cmp_…`). |

### Request body

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `start_at` | `string \| null` | no | Start later: the campaign is scheduled and starts dialling at this time. |

### Returns

`Campaign`:

| Field | Type | Description |
| --- | --- | --- |
| `id` | `string` | A campaign ID (prefix `cmp_`). |
| `object` | `"campaign"` | Always `campaign`. |
| `livemode` | `boolean` | `true` in live mode, `false` in test mode. |
| `name` | `string` | — |
| `state` | `"draft" \| "scheduled" \| "running" \| "paused" \| "completed" \| "cancelled"` | draft → running ⇄ paused → completed \| cancelled (scheduled: waits for schedule.start_at). |
| `purpose` | `"marketing" \| "service" \| "survey"` | marketing needs recorded consent with evidence (§30A) and a national-registry check for Israeli numbers. |
| `assistant_id` | `string \| null` | `asst_…` ID. |
| `connection_id` | `string \| null` | The SIP connection it dials through; null: the default one. |
| `caller_id` | `string \| null` | The number presented to contacts; null: the connection's default. |
| `priority` | `integer` | 1–10; higher is served first when campaigns compete for lines. |
| `max_concurrent` | `integer` | Simultaneous calls this campaign may hold. |
| `calls_per_second` | `integer` | New calls per second; 0: only the connection limits it. |
| `schedule` | `CampaignSchedule` | When the campaign dials. |
| `ring_timeout_seconds` | `integer` | Hang up an unanswered call after this long. |
| `retry` | `CampaignRetry` | Retry policy per call result. |
| `amd` | `CampaignAmd` | Answering-machine detection. |
| `disclosure` | `CampaignDisclosure` | The AI and recording disclosure played at the start of each call. |
| `first_message` | `string` | Overrides the assistant's first message ({{variables}} allowed); empty: the assistant's. |
| `script` | `string` | Extra instructions for this campaign ({{variables}} allowed). |
| `dispositions` | `string[]` | Outcomes the assistant can record. |
| `opt_out_dtmf_key` | `string \| null` | The key that opts a contact out; null: the organisation's. |
| `pause_reason` | `string \| null` | Why the campaign paused itself (for example, the national registry can't check it). |
| `stats` | `CampaignStats` | Contact counts per status and the calls in progress. |
| `created_at` | `string` | An ISO-8601 timestamp in UTC. |
| `updated_at` | `string` | An ISO-8601 timestamp in UTC. |
| `started_at` | `string \| null` | — |
| `completed_at` | `string \| null` | — |

### Example

```python
from morevoice import MoreVoice

client = MoreVoice()  # MOREVOICE_API_KEY from the environment

campaign = client.campaigns.start("cmp_7Hk2Lm9Qp", {})
print(campaign)
```

## `stats()`

**Retrieve a campaign's results.** Returns the campaign's results so far: contacts per status, dial attempts per result with talk and ring time, the dispositions recorded, and the contacts compliance checks skipped, by reason.

```python
# client.campaigns
def stats(self, id: str, *, more_voice_version: str | Unset = UNSET) -> _m.CampaignResults
```

`client.campaigns.stats()` · `await async_client.campaigns.stats()` · `GET /campaigns/{id}/stats` · [API reference](https://docs.morevoice.ai/api/operations/campaigns_stats/)

### Parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | `str` | yes | A campaign ID (`cmp_…`). |

### Returns

`CampaignResults`:

| Field | Type | Description |
| --- | --- | --- |
| `object` | `"campaign_stats"` | Always `campaign_stats`. |
| `campaign_id` | `string` | A campaign ID (prefix `cmp_`). |
| `livemode` | `boolean` | `true` in live mode, `false` in test mode. |
| `contacts` | `CampaignStats` | Contact counts per status and the calls in progress. |
| `attempts` | `object` | — |
| `outcomes` | `Record<string, integer>` | Contacts per recorded disposition (interested, not-interested, …). |
| `skip_reasons` | `Record<string, integer>` | Contacts the compliance checks or a user took out, per reason (dnc_import, registry, no_consent, …). |

### Example

```python
from morevoice import MoreVoice

client = MoreVoice()  # MOREVOICE_API_KEY from the environment

campaign_results = client.campaigns.stats("cmp_7Hk2Lm9Qp")
print(campaign_results)
```

## `update()`

**Update a campaign.** Changes the fields you send and keeps the rest. A completed or cancelled campaign can't change; pause a running one before changing its purpose.

```python
# client.campaigns
def update(self, id: str, body: _m.CampaignUpdateParamsInput | Mapping[str, Any] | Unset = UNSET, *, more_voice_version: str | Unset = UNSET) -> _m.Campaign
```

`client.campaigns.update()` · `await async_client.campaigns.update()` · `PATCH /campaigns/{id}` · [API reference](https://docs.morevoice.ai/api/operations/campaigns_update/)

### Parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | `str` | yes | A campaign ID (`cmp_…`). |

### Request body

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `amd` | `object` | no | — |
| `assistant_id` | `string \| null` | no | `asst_…` ID. |
| `caller_id` | `string \| null` | no | — |
| `calls_per_second` | `integer` | no | — |
| `connection_id` | `string \| null` | no | `conn_…` ID. |
| `disclosure` | `object` | no | — |
| `dispositions` | `string[]` | no | — |
| `first_message` | `string` | no | Overrides the assistant's first message. {{variables}} come from the contacts' variables, name and phone. |
| `max_concurrent` | `integer` | no | — |
| `name` | `string` | no | — |
| `opt_out_dtmf_key` | `string \| null` | no | — |
| `priority` | `integer` | no | — |
| `purpose` | `"marketing" \| "service" \| "survey"` | no | Required on create: marketing, service or survey. It decides the compliance checks. |
| `retry` | `object` | no | — |
| `ring_timeout_seconds` | `integer` | no | — |
| `schedule` | `object` | no | — |
| `script` | `string` | no | Extra instructions for the assistant. {{variables}} come from the contacts' variables, name and phone. |

### Returns

`Campaign`:

| Field | Type | Description |
| --- | --- | --- |
| `id` | `string` | A campaign ID (prefix `cmp_`). |
| `object` | `"campaign"` | Always `campaign`. |
| `livemode` | `boolean` | `true` in live mode, `false` in test mode. |
| `name` | `string` | — |
| `state` | `"draft" \| "scheduled" \| "running" \| "paused" \| "completed" \| "cancelled"` | draft → running ⇄ paused → completed \| cancelled (scheduled: waits for schedule.start_at). |
| `purpose` | `"marketing" \| "service" \| "survey"` | marketing needs recorded consent with evidence (§30A) and a national-registry check for Israeli numbers. |
| `assistant_id` | `string \| null` | `asst_…` ID. |
| `connection_id` | `string \| null` | The SIP connection it dials through; null: the default one. |
| `caller_id` | `string \| null` | The number presented to contacts; null: the connection's default. |
| `priority` | `integer` | 1–10; higher is served first when campaigns compete for lines. |
| `max_concurrent` | `integer` | Simultaneous calls this campaign may hold. |
| `calls_per_second` | `integer` | New calls per second; 0: only the connection limits it. |
| `schedule` | `CampaignSchedule` | When the campaign dials. |
| `ring_timeout_seconds` | `integer` | Hang up an unanswered call after this long. |
| `retry` | `CampaignRetry` | Retry policy per call result. |
| `amd` | `CampaignAmd` | Answering-machine detection. |
| `disclosure` | `CampaignDisclosure` | The AI and recording disclosure played at the start of each call. |
| `first_message` | `string` | Overrides the assistant's first message ({{variables}} allowed); empty: the assistant's. |
| `script` | `string` | Extra instructions for this campaign ({{variables}} allowed). |
| `dispositions` | `string[]` | Outcomes the assistant can record. |
| `opt_out_dtmf_key` | `string \| null` | The key that opts a contact out; null: the organisation's. |
| `pause_reason` | `string \| null` | Why the campaign paused itself (for example, the national registry can't check it). |
| `stats` | `CampaignStats` | Contact counts per status and the calls in progress. |
| `created_at` | `string` | An ISO-8601 timestamp in UTC. |
| `updated_at` | `string` | An ISO-8601 timestamp in UTC. |
| `started_at` | `string \| null` | — |
| `completed_at` | `string \| null` | — |

### Example

```python
from morevoice import MoreVoice

client = MoreVoice()  # MOREVOICE_API_KEY from the environment

campaign = client.campaigns.update("cmp_7Hk2Lm9Qp", {
    "max_concurrent": 20,
    "schedule": {
        "end_at": "2026-11-30T18:00:00+02:00",
    },
})
print(campaign)
```
