# Campaign API — Reference

Reference for third-party developers and **LLM coding assistants** integrating with the **Voice Campaign API** endpoint (`campaignApi.php`).

This document is the complete LLM-friendly equivalent of [campaignApiDocs.html](campaignApiDocs.html) — every action documented in the HTML reference is here in full. For the email-campaigns counterpart see [campaignEmailApi.md](campaignEmailApi.md).

> **Read this first if you're an AI agent debugging integration errors:** jump to [Recipient-number rules](#recipient-number-rules--read-this-before-using-campaignrun) and [FAQ for AI agents](#faq--common-ai-agent-questions). Most "how do I get past errorCode 6 / how do I approve a number / how do I do a single outbound call" questions are answered there explicitly.

---

## Base URL

```
https://api.tlivr.com/campaignApi.php
```

All actions are served by this single endpoint. The action is selected by the `action` parameter (e.g. `action=campaignRun`). **Exactly one action per request.**

## HTTP method

**All requests must be `POST`.** Body content type can be:

- `application/json`
- `application/x-www-form-urlencoded`
- `multipart/form-data` — required when attaching a file (`audioFile` binary or a comma-separated `phones` text file)

## Authentication

Every request must include an API key, either in the query string or in the POST body:

| Location           | Example                                  |
|--------------------|------------------------------------------|
| Query string       | `?apiKey=YOUR_API_KEY`                   |
| POST body (JSON)   | `{ "apiKey": "YOUR_API_KEY", ... }`      |

The API key identifies the **tenant account**. Treat it like a password — never embed in browser code or public repos.

## Source IP — must be whitelisted

> **Required before ANY request will succeed.** The IP address of the calling server must be approved in advance — in your personal area, under the **"אבטחת API" (API Security)** tab. Requests from a non-whitelisted IP are rejected up front with `errorCode: -99` / `Access denied`, regardless of how correct everything else is.
>
> This is account-level configuration done once per origin server. If you're getting `-99` you do not have a code problem — you have a missing IP allowlist entry — add it in your personal area, under the **"אבטחת API" (API Security)** tab.
>
> <sub>Developers: for troubleshooting, contact customer support.</sub>

---

## Action catalogue

| Action               | Purpose                                                |
|----------------------|--------------------------------------------------------|
| `campaignRun`        | Create + start an outbound voice campaign              |
| `campaignOpen`       | Create an **open** campaign that keeps accepting numbers **until you close it** |
| `campaignPush`       | Add a batch of up to 5,000 numbers to an open campaign |
| `campaignClose`      | Close an open campaign (stop accepting numbers)        |
| `campaignsHistory`   | List campaigns in a date range                         |
| `campaignReport`     | Full per-recipient report for one campaign             |
| `max_ringSet`        | Change parallel-dial cap on a running campaign         |
| `campaignHold`       | Pause a running campaign                               |
| `campaignResumption` | Resume a paused campaign                               |
| `campaignStop`       | Hard-stop a campaign (irreversible)                    |
| `addDid`             | Register a new outbound Caller-ID (DID) on the account |

---

## Recipient-number rules — read this before using `campaignRun`

> **Common AI-agent confusion.** The `phones` list in `campaignRun` is **not** a whitelist. There is **no API endpoint to "approve" recipient numbers** in advance. There is no equivalent of "add to clientes". The system does not require recipient numbers to be pre-registered anywhere.
>
> If you got `{"errorCode":6,"messige":"לא נמצאו מספרים תקינים/מאושרים לקבלת הודעות"}`, it means **every number in your `phones` array was rejected by one of the two rules below** — not that you're missing a registration step.

> These rules apply to recipients however they were supplied — an explicit `phones` array **or** a `mailingList` the system resolved for you. Sourcing recipients from a mailing list does not exempt them from format validation or the opt-out filter.

A number you put in `phones` is **accepted** iff **both** of these are true:

### 1. Israeli phone format

Non-digit characters are stripped. An Israeli international prefix (`+972`, `00972` or a bare `972`) is converted to the local leading `0`, so `+972501234567`, `972-50-123-4567` and `0501234567` are all the same recipient. A leading `0` is enforced. The cleaned string must match **one** of these patterns:

| Type                     | Regex                                | Example       | Total digits |
|--------------------------|--------------------------------------|---------------|--------------|
| Landline (regional)      | `0[2|3|4|8|9]XXXXXXX`                | `0212345678`  | 9            |
| `07` range / shared      | `07[0-9]XXXXXXX`                     | `0723456789`  | 10           |
| Mobile                   | `05[0|1|2|3|4|5|6|8|9]XXXXXXX`       | `0501234567`  | 10           |

**Anything else is rejected.** No non-Israeli country codes (`+1…`, `+44…`), no partial numbers, no `057` (not a real mobile prefix). The API dials Israeli numbers only; `+972` is accepted purely as an alternate spelling of the local format.

### 2. Not on the account's opt-out list

When a recipient presses key `9` during a campaign call, the system adds their number to `distributionPhoneList` with `type='blocked'` for this account. On every subsequent `campaignRun`, anyone on that list is silently dropped from `phones`. This is an opt-out registry — there is no "add to whitelist" counterpart.

### What "מאושרים" means in the error message

The Hebrew word "מאושרים" in the error is a translation accident — it sounds like "approved" but the system has no approval concept for recipients. What it actually means is "**passes the two rules above**". An AI agent reading the error literally will go hunting for an `approveNumber` endpoint that does not exist. **Don't.**

### How to fix `errorCode: 6`

1. **Check format first** — every number must match one of the three patterns above (after `+972` → `0` conversion). Watch for: non-Israeli country codes, 8-digit numbers, prefixes like `057` that aren't real.
2. **Then check opt-out** — if you're recycling a recipient list, some numbers may have hit `9` in a previous campaign. Their being filtered is intentional.
3. The response's `errorPhones` and `blockedPhones` counts (returned even on partial success) tell you which bucket your losses fell into.

### `addDid` is unrelated to recipient numbers

`addDid` registers a **Caller-ID** — the number that **displays on the recipient's screen** as the "from" of the call. It has **nothing** to do with the `phones` list (the "to"). These are orthogonal concepts. AI agents commonly confuse them; don't.

---

## `campaignRun` — start a campaign

Creates and dispatches an outbound voice campaign. The same endpoint handles **everything from a single-number "ring my agent" call to a 100k-number broadcast** — there is no separate "click-to-call" or "single dial" action in this API.

### Request parameters

| Param                  | Required | Default          | Notes |
|------------------------|----------|------------------|-------|
| `action`               | yes      | —                | `campaignRun` |
| `apiKey`               | yes      | —                | Tenant key |
| `phones`               | yes\*    | —                | **(a)** JSON array of Israeli phones, OR **(b)** comma-separated text file via `multipart/form-data`. See [Recipient-number rules](#recipient-number-rules--read-this-before-using-campaignrun). *Not needed when `mailingList` is sent. |
| `mailingList`          | yes\*    | —                | **Send to an existing mailing list instead of an explicit `phones` array.** One list ID (`"1042"`) or several comma-separated (`"1042,1043"`). *Alternative to `phones` — send one of the two. See [Targeting a mailing list](#targeting-a-mailing-list). |
| `audioFile`            | yes\*    | —                | Audio file attached as `multipart/form-data` (**binary upload only** — e.g. `curl -F "audioFile=@message.wav"`). A file *name* string is **not** accepted here: there is no way to reference a previously uploaded file by name, re-attach the file on every `campaignRun`. WAV recommended (MP3 uploads may be challenged by the edge firewall). *Provide exactly one of `audioFile` / `audioText` / `messagesType=extensionActivation`.* |
| `audioText`            | yes\*    | —                | Source text. System runs TTS automatically. Mutually exclusive with `audioFile`. |
| `messagesType`         | no       | `audioFile`      | `audioFile` (default — play a recording), `extensionActivation` (skip audio, route the answered call straight to a PBX extension), `apiUrl` (hand call control to your server — see below) or `RH` (**ring campaign** — ring the handset and hang up, no message — see [Ring campaigns](#ring-campaigns-צינתוק--ring-only-with-or-without-a-message)). |
| `ringOnly`             | no       | `no`             | `yes` = deliver an **audio** campaign as a **ring only**: the phone rings and drops, the recording is **not** played live but is stored and available in the recipient's message history / on callback. Combine with `audioFile` or `audioText`. See [Ring campaigns](#ring-campaigns-צינתוק--ring-only-with-or-without-a-message). |
| `extensionActivation`  | yes\*\*  | —                | PBX extension ID. **Required when `messagesType=extensionActivation`.** |
| `apiUrl`               | yes\*\*  | —                | `http(s)` URL (≤ 500 chars). **Required when `messagesType=apiUrl`.** When the call is answered the PBX calls this URL and executes the JSON actions it returns — the full [Module API](מדריך-API-מרכזיה.md) protocol (play, menu, record, route, charge…). The request carries the campaign context: `event=answered`, `campaignId`, `campaignCallId`, `campaignPhone`, `client`, `did`. |
| `callId`               | no       | account default  | **Outbound Caller-ID.** Must be a phone previously registered via [`addDid`](#adddid--register-an-outbound-caller-id). Omit to use the account's default DID. |
| `anonymousCampaign`    | no       | `no`             | `yes` = present the call as **withheld / unidentified** — the recipient sees "Private"/blocked instead of your Caller-ID. **Requires a per-account permission** (contact support to enable) — otherwise the call is rejected with `errorCode 29`. A registered `callId` is still required for routing; only its *presentation* is hidden. Whether the number is ultimately withheld also depends on the carrier honouring CLIR. |
| `callLength`           | no       | `25`             | Per-call dial-length cap (seconds). Valid range **6–30**. |
| `dialRetries`          | no       | `1`              | Max retries on unanswered. Cap **5**. |
| `betweenRetries`       | yes      | `20`             | **Minutes** between retries. Range **5–120**. |
| `reasonableHours`      | no       | `no`             | `yes` = auto-pause between 23:00 and 08:00 (Israel time). |
| `sendTime`             | no       | now              | Scheduled start, format `yy-mm-dd hh:ii:ss`. Max **21 days** ahead. |
| `title`                | no       | —                | Free-text label visible in dashboards. |
| `maxRing`              | no       | unlimited        | Concurrent-dial cap (integer). |
| `runType`              | no       | —                | `hold` = create the campaign in paused state; start it later with `campaignResumption`. |
| `externalID`           | no       | —                | Your own correlation id (≤ 50 chars). |
| `externalDescription`  | no       | —                | Free description (≤ 100 chars). |
| `keysAction-N`         | no       | —                | DTMF routing for digit `N` (1–8). See [keysAction values](#keysaction-values). |
| `demo`                 | no       | `no`             | `yes` = validate + price the call but do **not** dispatch. Use to dry-run a payload. |
| `DNCTest`              | no       | `no`             | Currently inert — Israeli DNC integration is disabled in code. |

### `keysAction-N` values

Configure what happens when the recipient presses digit `N` during playback. `N` is `1`–`8`. **Digit `9` is hardcoded to "remove me from this account's list" and cannot be reassigned.**

> Per-key actions are only meaningful when the campaign **plays audio** (`audioFile` / `audioText`). With `messagesType=extensionActivation` or `messagesType=apiUrl` the keys are controlled by the extension / your API server, not by the campaign.

| Value                                | Behaviour                                                                    | Example                                            |
|--------------------------------------|------------------------------------------------------------------------------|----------------------------------------------------|
| `replay`                             | Replay the audio from the start                                              | `"keysAction-3": "replay"`                         |
| `digits`                             | Capture additional digits the user enters (saved to call record)             | `"keysAction-2": "digits"`                         |
| `routing-0XXXXXXXXX`                 | Bridge the live call to an Israeli phone number                              | `"keysAction-1": "routing-0521234567"`             |
| `playFile-FILE`                      | Play another audio file, then hang up. `FILE` = system name (`ivrName`) of a file that already exists on the account (an IVR-extension file or a previously uploaded campaign file) | `"keysAction-2": "playFile-a1b2c3d4e5"`            |
| `extension-EXT`                      | Run a full IVR extension for this caller — same as `messagesType=extensionActivation`, but per key. `EXT` = the extension number on your account | `"keysAction-4": "extension-105"`                  |
| `apiUrl-URL`                         | Hand call control to your server: the PBX calls `URL` and executes the JSON actions it returns — the full [Module API](מדריך-API-מרכזיה.md) protocol. The request carries the campaign context plus `event=digitPressed` and `digit=N` | `"keysAction-5": "apiUrl-https://your-server.com/ivr"` |
| `pbxRouting-EXT`                     | Bridge to a PBX queue/extension on your account                              | `"keysAction-1": "pbxRouting-200"`                 |
| `sipRouting-sip@host`                | Bridge to a SIP URI                                                          | `"keysAction-1": "sipRouting-sip@my-ivr.example"`  |
| `nedarimDonations-X1-X2-X3-X4-X5`    | Open a Nedarim Plus donation flow. `X1`=terminal · `X2`=fixed amount · `X3`=`yes`/`no` (amount locked) · `X4`=`yes`/`no` (require CVV) · `X5`=category name | `"keysAction-1": "nedarimDonations-12345-100-no-no-כללי"` |

For `playFile` / `extension` / `apiUrl` the parameter is **everything after the first hyphen**, so URLs containing hyphens work as-is.

### Choosing what to play — three mutually exclusive options

| Option                                                          | When to use                                            |
|-----------------------------------------------------------------|--------------------------------------------------------|
| `audioFile`                                                     | You have a pre-recorded WAV/MP3 to upload              |
| `audioText`                                                     | You want the system to TTS a string                    |
| `messagesType=extensionActivation` + `extensionActivation=EXT`  | No greeting — just route the answered call to a PBX extension |

**Send only one.** Mixing them is undefined.

#### Example — upload a recording (`audioFile`, multipart)

`audioFile` is a **file upload**, so the request must be `multipart/form-data`; the other fields travel as ordinary form fields. The file is attached on every `campaignRun` — a name of a previously uploaded recording is not accepted.

```bash
curl -X POST https://api.tlivr.com/campaignApi.php \
  -F "action=campaignRun" \
  -F "apiKey=YOUR_API_KEY" \
  -F "audioFile=@message.wav;type=audio/wav" \
  -F "phones=0501234567,0521234567" \
  -F "title=קמפיין ינואר" \
  -F "betweenRetries=30" \
  -F "demo=yes"
```

WAV (mono, 8 kHz or higher) is the safe choice; MP3 uploads may be stopped by the edge firewall before reaching the API. Run with `demo=yes` first — the response returns the audio length and cost without dialling.

### Ring campaigns (צינתוק) — ring only, with or without a message

A **ring campaign** rings the recipient's handset and hangs up — it does **not** play a live message. It is the classic Israeli "צינתוק" (missed-call) flow: the phone rings, the recipient sees the call and can call the Caller-ID back. Two variants:

| Variant | How to request | What the recipient gets |
|---------|----------------|-------------------------|
| **Ring only** | `messagesType=RH` (no audio) | The phone rings and drops. No message, anywhere. |
| **Ring + message in history** | a normal audio campaign (`audioFile` **or** `audioText`) **plus** `ringOnly=yes` | The phone rings and drops — the message is **not** played live. It is stored and available in the recipient's **message history** and on **callback** to the Caller-ID. |

Ring campaigns are **available to every account** — no special permission is required.

A ring campaign still respects the account opt-out list — a blocked recipient (pressed `9` previously) is never rung and never billed. Per-key actions (`keysAction-N`) do not apply to ring campaigns (there is no live menu).

#### Pricing — how a ring campaign is charged

Charged **per recipient number**, and **only** for recipients who have **not** opted in to rings:

- A recipient who **opted in to receive rings** → **free**, and not counted at all.
- A recipient who has **not** opted in → charged:
  - if the account has a **ring package** — counted against the package's monthly quota, and beyond it at the package's per-ring rate;
  - otherwise — **0.05 ₪ per number** (VAT included).

So the billed quantity is the number of **non-opted-in** recipients. The split is returned as `ringPaid` (charged) and `ringFree` (opted-in, free). Run with `demo=yes` to see the split and the estimated cost **before** dispatching.

> **Consent is genuine, phone-side only.** A recipient is marked "opted in to rings" only when they themselves approve it on the phone (through the account's ring opt-in extension). The sender cannot flip that flag per contact — this protects the free tier.

#### Example — ring only (no message)

```bash
curl -X POST https://api.tlivr.com/campaignApi.php \
  -H "Content-Type: application/json" \
  -d '{
    "action":       "campaignRun",
    "apiKey":       "YOUR_API_KEY",
    "messagesType": "RH",
    "mailingList":  "1042",
    "demo":         "yes"
  }'
```

#### Example — ring + message available in history

```bash
curl -X POST https://api.tlivr.com/campaignApi.php \
  -H "Content-Type: application/json" \
  -d '{
    "action":    "campaignRun",
    "apiKey":    "YOUR_API_KEY",
    "audioText": "שלום, השארנו לכם הודעה — חייגו אלינו חזרה לשמיעתה",
    "ringOnly":  "yes",
    "mailingList": "1042"
  }'
```

### Example — simple TTS broadcast

```bash
curl -X POST https://api.tlivr.com/campaignApi.php \
  -H "Content-Type: application/json" \
  -d '{
    "action":          "campaignRun",
    "apiKey":          "YOUR_API_KEY",
    "audioText":       "שלום, זוהי הודעה חשובה מחברתנו",
    "phones":          ["0501234567", "0521234567", "0541234567"],
    "title":           "קמפיין ינואר",
    "callLength":      25,
    "dialRetries":     2,
    "betweenRetries":  20,
    "reasonableHours": "yes"
  }'
```

### Targeting a mailing list

Instead of sending an explicit `phones` array, point `campaignRun` at a **mailing list** that already exists on the account. The system resolves the recipients itself at dispatch time, so the audience is always current and you never have to fetch-then-resend it.

Mailing lists are created and maintained through the [Mailing Lists API](mailingListsApiDocs.md) — `mailingListEdit` to create one, `mailingLists` to enumerate IDs, `uplodePhones` to fill it. The ID returned there is exactly what goes in `mailingList` here.

| Param         | Value                                                                                     |
|---------------|---------------------------------------------------------------------------------------------|
| `mailingList` | A single list ID (`"1042"`), or several comma-separated (`"1042,1043,1088"`). Alternative to `phones`. |

**What gets pulled from the list**

| Rule                          | Behaviour                                                                                                                        |
|-------------------------------|------------------------------------------------------------------------------------------------------------------------------------|
| **Active contacts only**      | Contacts blocked inside the list (`status=1`, e.g. via `setStatus`) are **excluded** from the run.                                |
| **Several lists at once**     | Comma-separated IDs are merged into one audience. A number present in more than one list is **dialled only once** — duplicates collapse. |
| **Account opt-out still applies** | After resolving the list, the account-level opt-out registry (key `9` from earlier campaigns) is filtered out on top. The count comes back in `blockedPhones`. |
| **Format validation still applies** | The same Israeli-format rules that govern `phones` apply here. Invalid numbers stored in the list are counted in `errorPhones`.  |
| **Another account's list**    | An ID that isn't yours simply resolves to no recipients → `errorCode 6`. It never reads another account's data.                   |

> **Never send `mailingList` and `phones` together.** When both are present, `mailingList` wins and `phones` is discarded silently — no error is raised.

> **Key `9` opt-out does not mark the contact inside the list.** A recipient who presses `9` is written to the **account-level** opt-out registry and filtered from future campaigns, but stays "active" in the mailing list — the list's own `blocked` counter will not grow. That's why the list's contact count can exceed the `phones` figure returned by the run; the difference shows up in `blockedPhones`.

#### Example — one list

```bash
curl -X POST https://api.tlivr.com/campaignApi.php \
  -H "Content-Type: application/json" \
  -d '{
    "action":          "campaignRun",
    "apiKey":          "YOUR_API_KEY",
    "audioText":       "שלום, זוהי הודעה חשובה מחברתנו",
    "mailingList":     "1042",
    "title":           "קמפיין ינואר",
    "betweenRetries":  20
  }'
```

#### Example — several lists merged, with key routing

```json
{
  "action":         "campaignRun",
  "apiKey":         "YOUR_API_KEY",
  "audioText":      "שלום, זו הודעת דוגמה",
  "mailingList":    "1042,1043",
  "betweenRetries": 20,
  "keysAction-1":   "routing-0521111111"
}
```

#### Example — create a list, fill it, dispatch to it

```python
# 1. create the list (Mailing Lists API)
created = requests.post('https://api.tlivr.com/mailingListsApi.php', data={
    'action': 'mailingListEdit', 'apiKey': KEY, 'name': 'קמפיין ינואר',
}).json()
mailing_list = created['mailingList']

# 2. add the recipients
requests.post('https://api.tlivr.com/mailingListsApi.php', json={
    'action': 'uplodePhones', 'apiKey': KEY, 'mailingList': mailing_list,
    'insertType': 'all',
    'phones': [{'phone': '0501234567', 'name': 'אבי כהן'}],
})

# 3. dispatch the campaign to that list
requests.post('https://api.tlivr.com/campaignApi.php', json={
    'action': 'campaignRun', 'apiKey': KEY,
    'mailingList': mailing_list,
    'audioText': 'שלום, זוהי הודעה מחברתנו',
    'betweenRetries': 20,
    'demo': 'yes',        # drop this to actually dispatch
})
```

> **Dry-run first.** With `demo=yes` you get the same fields back (`phones` = how many recipients resolved from the list, `blockedPhones`, `billing`) without dispatching — the way to confirm the list yields the audience size you expect.

### Example — single outbound "ring my agent" with key-routing to my IVR

A single recipient (the agent), short audio that says "press 1 to take the call", key `1` bridges the live audio to the agent's SIP IVR. **This is the canonical pattern for ad-hoc click-to-call** — there is no other endpoint for it.

```bash
curl -X POST https://api.tlivr.com/campaignApi.php \
  -H "Content-Type: application/json" \
  -d '{
    "action":          "campaignRun",
    "apiKey":          "YOUR_API_KEY",
    "audioText":       "בקשת שירות חדשה. הקש 1 לחיבור ללקוח",
    "phones":          ["0501112222"],
    "title":           "Ring agent",
    "callLength":      25,
    "betweenRetries":  5,
    "dialRetries":     2,
    "keysAction-1":    "sipRouting-sip@my-ivr.example.com"
  }'
```

> **No HTTP webhook to an external IVR URL.** The campaign API does **not** support routing an answered call to `https://your-ivr.example.com/main.php`. The supported routings are PSTN (`routing-`), PBX extension (`pbxRouting-`), and SIP URI (`sipRouting-`). To wire your own IVR you must either expose it via SIP, or front it with a phone number that the campaign can `routing-` to. If your IVR is hosted on this platform's `main.php`, use a `pbxRouting-` to its extension.

### Example — keysAction with a phone-routing menu

```json
{
  "action":         "campaignRun",
  "apiKey":         "YOUR_API_KEY",
  "audioText":      "שלום, זו הודעת דוגמה",
  "phones":         ["0501234567"],
  "title":          "קמפיין מכירות",
  "betweenRetries": 20,
  "keysAction-1":   "routing-0521111111",
  "keysAction-2":   "digits",
  "keysAction-3":   "replay",
  "externalID":     "CAMP-2025-001"
}
```

### Success response

```json
{
  "status":        "OK",
  "errorCode":     0,
  "campaignId":    4521,
  "phones":        1500,
  "errorPhones":   3,
  "blockedPhones": 12,
  "billing":       "625.00",
  "accountSum":    "1,234.56",
  "fileLong":      25
}
```

| Key             | Meaning                                                                  |
|-----------------|--------------------------------------------------------------------------|
| `campaignId`    | New campaign id — use it for report/hold/resume/stop                     |
| `phones`        | Recipients accepted into the run                                         |
| `errorPhones`   | Recipients rejected by format validation                                 |
| `blockedPhones` | Recipients filtered by the per-account opt-out list                      |
| `billing`       | Estimated cost in units (for a ring campaign — the cost of the non-opted-in recipients) |
| `accountSum`    | Account balance before deduction                                         |
| `fileLong`      | Audio length in seconds                                                  |
| `ringPaid`      | *(ring campaigns only)* recipients charged — those **not** opted in to rings |
| `ringFree`      | *(ring campaigns only)* recipients free — those who opted in to receive rings |

With `demo=yes`, the same fields are returned but `campaignId` is the literal string `"demo"` and **no dispatch happens**. Use it as a dry-run.

### Error codes

| `errorCode` | Reason                              | What to do |
|-------------|-------------------------------------|------------|
| `-99`       | `Access denied` — IP not whitelisted | Approve your server IP in the personal area ("אבטחת API" tab) — this is not a code fix |
| `-88`       | Missing essential connection info   | Check auth + required fields |
| `1`         | `callId` not authorised on this account | Use a DID previously registered via [`addDid`](#adddid--register-an-outbound-caller-id), or omit to use account default |
| `2`         | `callLength` out of range            | 6–30 seconds |
| `3`         | `dialRetries` over the account cap   | Up to 5 |
| `4`         | `betweenRetries` out of range        | 5–120 minutes |
| `5`         | Audio too short                      | Minimum 5 seconds |
| `6`         | **No valid recipients.** Every number in `phones` failed format validation, or every one is on the account opt-out list. With `mailingList`: the list is empty, all its contacts are blocked, or the ID isn't yours. | See [Recipient-number rules](#recipient-number-rules--read-this-before-using-campaignrun). **There is no whitelist-add endpoint** — fix the phone format. |
| `7`         | Insufficient balance                 | Response includes `accountSum` (current) + `billing` (required) so you can compute the gap |
| `9`         | `sendTime` more than 21 days out     | Pick a closer time |
| `13`        | `keysAction-N` routing number invalid | Check the phone in `routing-XXXX` |
| `18`        | Invalid audio file                   | Check format |
| `19`        | Audio > 300s or upload error         | Trim or re-upload |
| `27`        | `apiUrl` invalid                     | Must be a valid `http(s)` URL, ≤ 500 chars |
| `28`        | `keysAction-N` parameter invalid     | `playFile` — file not found on the account; `extension` — extension does not exist; `apiUrl` — malformed URL. The message names the offending key |
| `29`        | No permission for anonymous (`anonymousCampaign=yes`) campaign | Ask support to enable the "unidentified campaign" permission on the account |
| `30`        | No audio for a playback campaign — no `audioFile` was attached and no `audioText` given, or the referenced recording could not be found / downloaded | Attach the file as `multipart/form-data` (`audioFile`), or send `audioText`. A file *name* in `audioFile` is not supported |
| `31`        | `messagesType=audioUrl` is not supported | Upload the file (`audioFile`) or use `audioText` |

---

## Open campaigns — `campaignOpen` / `campaignPush` / `campaignClose`

A regular `campaignRun` campaign has a **fixed** recipient list and closes itself once it has finished dialing. An **open campaign** stays open: you create it once, keep **pushing batches of numbers** into it for as long as you need, and **close it yourself** when you're done.

> **You close it — the system doesn't.** An open campaign does **not** close by itself: not when it has finished dialing, not when nothing is waiting to be called, and not when you haven't pushed anything for hours. It keeps accepting numbers until **you** call [`campaignClose`](#campaignclose--close-an-open-campaign). If you never close it, it stays open (up to the [7-day safety limit](#safety-limit--autocloseat--autocloseatunix)).

Typical use: a CRM or website that sends each new lead (or each small batch) to the same voice message as it arrives — one campaign, one report, instead of a new campaign per lead.

> **API only.** Open campaigns are not available in the web interface.

### Lifecycle

```
campaignOpen ──► open ──(campaignPush × N)──► campaignClose ──► closed ──► ended
                  │                              ▲
                  │                              └─ you close it. Only if you never do:
                  │                                 the system closes it at autoCloseAt (7 days)
                  └── campaignHold / campaignResumption work as usual
```

| State (`openState`) | Accepts `campaignPush`? | Dials? |
|---------------------|-------------------------|--------|
| `open`              | yes                     | yes    |
| `hold`              | yes — numbers queue up, dialing starts on `campaignResumption` | no |
| `closed`            | **no** (`errorCode 46`) | yes — numbers already accepted keep dialing, including retries, until done |
| `ended`             | **no** (`errorCode 46`) | no — final; report and billing are complete |

### Hard limits

| Limit | Value | When exceeded |
|-------|-------|---------------|
| Maximum lifetime of an open campaign | **7 days** from `campaignOpen` | Only if you haven't closed it by then — the system closes it at `autoCloseAt` (see [Safety limit](#safety-limit--autocloseat--autocloseatunix)) |
| Numbers per request (`campaignOpen` or `campaignPush`) | **5,000** | `errorCode 43` — the whole request is rejected; split it |
| Numbers per open campaign (all pushes together) | **2,000,000** | `errorCode 48` — open a new open campaign |

### Closing — you close it

Closing an open campaign is **your** job: call [`campaignClose`](#campaignclose--close-an-open-campaign) when you have no more numbers to send. That is the normal way an open campaign ends. Build your integration around it — for example, close at the end of the day, at the end of the sales event, or when your own process finishes.

Until you close it, the campaign keeps accepting numbers, even if it has been idle for a long time.

#### Safety limit — `autoCloseAt` / `autoCloseAtUnix`

As a safeguard against campaigns that were forgotten open, an open campaign cannot live longer than **7 days**. If it is still open 7 days after `campaignOpen`, the system closes it for you. This is a fallback, **not** the way to end a campaign.

You don't send this time. The system sets it when the campaign is created and returns it on every open-campaign response, in two formats — it is the **longest** the campaign can stay open:

| Field             | Format                         | Example               |
|-------------------|--------------------------------|-----------------------|
| `autoCloseAt`     | `YYYY-MM-DD HH:MM:SS`, Israel time | `"2026-10-11 14:30:00"` |
| `autoCloseAtUnix` | Unix timestamp (seconds)       | `1791642600`          |

It cannot be extended. If you need to keep sending after it, open a new open campaign. A campaign closed this way reports `closeType: "auto"`.

### Billing

Each request is billed for the numbers it **accepted**, using the same pricing as `campaignRun` and the call length set at `campaignOpen`. A batch is accepted only if the account balance covers it (`errorCode 7` otherwise). The cost shown per request is an **estimate**; the final charge for the whole campaign is calculated from the actual calls once the campaign has ended, exactly as for a regular campaign.

---

### `campaignOpen` — create an open campaign

Takes **every `campaignRun` parameter** (audio / `audioText` / `extensionActivation` / `apiUrl`, `callId`, `callLength`, `dialRetries`, `betweenRetries`, `reasonableHours`, `keysAction-N`, `maxRing`, `title`, `externalID`, `messageLevel` …) with the same rules and the same errors — see [campaignRun → Request parameters](#request-parameters). The differences:

| Param             | Required | Default | Notes |
|-------------------|----------|---------|-------|
| `action`          | yes      | —       | `campaignOpen` |
| `phones`          | **no**   | —       | Optional first batch, same format as `campaignRun`. **Up to 5,000.** An open campaign may be created empty. (`mailingList` also works as a first batch, within the same 5,000 limit.) |
| `duplicatePolicy` | no       | `allow` | What to do when a pushed number was **already called and finished** in this campaign: `allow` = call it again, `block` = reject it. See [Repeated numbers](#repeated-numbers). |

Not supported in `campaignOpen` (`errorCode 41`): `sendTime` (an open campaign starts immediately), `runType=hold`, `type=test`, and ring-only (`messagesType=RH`) campaigns.

Settings such as the message, caller-ID, retries and key actions are fixed at `campaignOpen` and apply to every number pushed later. The opt-out filters (`blocksSending`, `messageLevel`) chosen here also apply to every later push.

```bash
curl -X POST https://api.tlivr.com/campaignApi.php \
  -H "Content-Type: application/json" \
  -d '{
    "action":          "campaignOpen",
    "apiKey":          "YOUR_API_KEY",
    "audioText":       "שלום, תודה שפניתם אלינו. נציג יחזור אליכם בהקדם.",
    "betweenRetries":  10,
    "dialRetries":     2,
    "duplicatePolicy": "allow",
    "title":           "New leads"
  }'
```

#### Response

All of the `campaignRun` success fields (`campaignId`, `phones`, `errorPhones`, `blockedPhones`, `billing`, `accountSum`, `fileLong`), plus:

```json
{
  "status":           "OK",
  "errorCode":        0,
  "campaignId":       4600,
  "phones":           0,
  "errorPhones":      0,
  "blockedPhones":    0,
  "billing":          "0.00",
  "accountSum":       "1,234.56",
  "fileLong":         12,
  "openState":        "open",
  "autoCloseAt":      "2026-10-11 14:30:00",
  "autoCloseAtUnix":  1791642600,
  "closedAt":         null,
  "closedAtUnix":     null,
  "closeType":        null,
  "duplicatePolicy":  "allow",
  "maxPhonesPerPush": 5000
}
```

**Store `campaignId`** — every push and the close use it. With `demo=yes` the request is validated and priced and the would-be `autoCloseAt` is returned, but nothing is created.

---

### `campaignPush` — add numbers to an open campaign

| Param        | Required | Notes |
|--------------|----------|-------|
| `action`     | yes      | `campaignPush` |
| `apiKey`     | yes      | Tenant key |
| `campaignId` | yes      | The `campaignId` returned by `campaignOpen` |
| `phones`     | yes      | **JSON array** of phone numbers, **1 to 5,000**. Same format rules as `campaignRun` (local `05X…` / `0X…`, or international `+972…`). A comma-separated string is also accepted. |

Each number goes through the same checks as in `campaignRun`: format validation, then the account opt-out list. Then it is checked against the numbers already in this campaign:

#### Repeated numbers

| The number in this campaign… | Result | Counted in |
|------------------------------|--------|------------|
| …is not there yet | added | `accepted` |
| …is **still waiting** to be dialed, being dialed, or has retries left | **not added again** — it will be called once, as already planned | `alreadyPending` / `alreadyPendingList` |
| …was **already called and finished**, `duplicatePolicy=allow` (default) | **added as a new, separate call** — its own result, its own billing, its own row in the report; the earlier call stays in the report as it was | `accepted` and `resent` |
| …was **already called and finished**, `duplicatePolicy=block` | rejected | `duplicateBlocked` / `duplicateBlockedList` |

"Finished" means answered, or every dial attempt (`dialRetries`) has been used. Numbers repeated **inside the same request** are counted once.

A request is **all-or-nothing on errors** (e.g. too many numbers, closed campaign, insufficient balance — nothing is added), but **per-number filtering is not an error**: a request in which every number was invalid, blocked or already pending still returns `errorCode 0` with `accepted: 0` and is not billed.

```bash
curl -X POST https://api.tlivr.com/campaignApi.php \
  -H "Content-Type: application/json" \
  -d '{
    "action":     "campaignPush",
    "apiKey":     "YOUR_API_KEY",
    "campaignId": 4600,
    "phones":     ["0501234567", "+972521234567", "031234567"]
  }'
```

#### Response

```json
{
  "status":               "OK",
  "errorCode":            0,
  "campaignId":           4600,
  "pushId":               17,
  "received":             3,
  "accepted":             2,
  "resent":               0,
  "errorPhones":          0,
  "errorPhonesList":      [],
  "blockedPhones":        0,
  "alreadyPending":       1,
  "alreadyPendingList":   ["0501234567"],
  "duplicateBlocked":     0,
  "duplicateBlockedList": [],
  "totalPhones":          1342,
  "billing":              "0.40",
  "accountSum":           "1,234.56",
  "openState":            "open",
  "autoCloseAt":          "2026-10-11 14:30:00",
  "autoCloseAtUnix":      1791642600,
  "closedAt":             null,
  "closedAtUnix":         null,
  "closeType":            null,
  "duplicatePolicy":      "allow",
  "maxPhonesPerPush":     5000
}
```

| Key                    | Meaning |
|------------------------|---------|
| `pushId`               | Sequence number of this batch in the campaign (`0` when nothing was accepted) |
| `received`             | Numbers in the request |
| `accepted`             | Numbers added to the campaign by this request (and billed) |
| `resent`               | Of `accepted` — numbers that had already finished in this campaign and were added again (`duplicatePolicy=allow`) |
| `errorPhones` / `errorPhonesList` | Invalid format — not added |
| `blockedPhones`        | On the account opt-out list — not added (count only) |
| `alreadyPending` / `alreadyPendingList` | Already waiting in this campaign — not duplicated |
| `duplicateBlocked` / `duplicateBlockedList` | Already finished in this campaign and `duplicatePolicy=block` — not added |
| `totalPhones`          | Numbers in the campaign after this request (counts toward the 2,000,000 limit) |
| `billing`              | Estimated cost of this request, in units |
| `accountSum`           | Account balance **before** this request |
| `openState`, `autoCloseAt`, `autoCloseAtUnix`, … | Current state of the open campaign — see [Lifecycle](#lifecycle) and [Safety limit](#safety-limit--autocloseat--autocloseatunix) |

---

### `campaignClose` — close an open campaign

**This is how an open campaign ends** — it does not close by itself (apart from the [7-day safety limit](#safety-limit--autocloseat--autocloseatunix)).

Stops the campaign from accepting new numbers. **Numbers already accepted keep dialing** (including their retries) until done; then the campaign ends and its report and final billing are complete. To also cancel the numbers that haven't been called yet, use [`campaignStop`](#campaignstop--hard-stop-a-campaign) instead. `campaignStop` on an open campaign also closes it.

| Param        | Required | Notes |
|--------------|----------|-------|
| `action`     | yes      | `campaignClose` |
| `apiKey`     | yes      | Tenant key |
| `campaignId` | yes      | The open campaign |

```bash
curl -X POST https://api.tlivr.com/campaignApi.php \
  -H "Content-Type: application/json" \
  -d '{ "action":"campaignClose", "apiKey":"YOUR_API_KEY", "campaignId":4600 }'
```

```json
{
  "status":           "OK",
  "errorCode":        0,
  "campaignId":       4600,
  "pendingCalls":     37,
  "openState":        "closed",
  "autoCloseAt":      "2026-10-11 14:30:00",
  "autoCloseAtUnix":  1791642600,
  "closedAt":         "2026-10-06 09:12:44",
  "closedAtUnix":     1791267164,
  "closeType":        "manual",
  "duplicatePolicy":  "allow",
  "maxPhonesPerPush": 5000
}
```

`pendingCalls` = numbers that still have dialing left at the moment of closing. `closeType` is `manual` (`campaignClose`), `auto` (reached `autoCloseAt`) or `stop` (`campaignStop`).

Closing a campaign that is in `hold` keeps its remaining numbers paused — resume it with `campaignResumption` so they're dialed, or `campaignStop` it to drop them.

---

### Error codes — open campaigns

In addition to every `campaignRun` error (which `campaignOpen` can also return):

| `errorCode` | Returned by | Reason | What to do |
|-------------|-------------|--------|------------|
| `7`  | `campaignOpen`, `campaignPush` | Insufficient balance for this batch | Top up, or push fewer numbers. Same response shape as in `campaignRun` |
| `25` | `campaignPush`, `campaignClose` | Campaign not found, or it belongs to another account | Check `campaignId` |
| `40` | all three | Open campaigns are not enabled on this server yet | Contact support |
| `41` | `campaignOpen` | Unsupported parameter (`sendTime`, `runType=hold`, `type=test`, `messagesType=RH`) or invalid `duplicatePolicy` | Remove the parameter / send `allow` or `block` |
| `43` | `campaignOpen`, `campaignPush` | More than 5,000 numbers in one request. Response includes `maxPhonesPerPush`, `received`. **Nothing was added** | Split into requests of up to 5,000 |
| `45` | `campaignPush`, `campaignClose` | The campaign is a regular campaign, not an open one | Only campaigns created with `campaignOpen` accept pushes |
| `46` | `campaignPush`, `campaignClose` | The open campaign is already closed (manually, automatically at `autoCloseAt`, or by `campaignStop`). Response includes `openState`, `closedAt`, `closedAtUnix`, `closeType` | Open a new open campaign |
| `47` | `campaignPush` | `phones` missing or empty | Send a JSON array with 1–5,000 numbers |
| `48` | `campaignPush` | The campaign would exceed 2,000,000 numbers. Response includes `maxPhonesPerCampaign`, `totalPhones`, `remaining`. **Nothing was added** | Push at most `remaining`, or open a new open campaign |

---

## `campaignsHistory` — list campaigns

Returns an array of campaign rows for a date range.

### Request

| Param      | Required | Default          | Notes |
|------------|----------|------------------|-------|
| `action`   | yes      | —                | `campaignsHistory` |
| `apiKey`   | yes      | —                | Tenant key |
| `fromDate` | no       | today            | `yyyy-mm-dd` |
| `toDate`   | no       | 30 days back     | `yyyy-mm-dd` |

### Example

```bash
curl -X POST https://api.tlivr.com/campaignApi.php \
  -H "Content-Type: application/json" \
  -d '{
    "action":   "campaignsHistory",
    "apiKey":   "YOUR_API_KEY",
    "fromDate": "2025-01-01",
    "toDate":   "2025-01-31"
  }'
```

### Response

```json
[
  {
    "id":              4521,
    "title":           "קמפיין ינואר 2025",
    "active":          "ended",
    "insert_time":     "01-01-2025 10:00",
    "start_time":      "01-01-2025 10:05",
    "end_time":        "01-01-2025 11:30",
    "run_time":        "01:25:00",
    "timed":           false,
    "total_phones":    1500,
    "total_sent":      1480,
    "new_phones":      20,
    "answerd_calls":   943,
    "dialing_calls":   3,
    "retries_list":    57,
    "billing":         "625.00",
    "max_ring":        10,
    "max_ring_view":   "10",
    "between_retries": 20,
    "donations_sum":   0,
    "donations_count": 0,
    "audioUrl":        "audio.php?type=ivr&audio=FILENAME"
  }
]
```

| Key                | Meaning                                                       |
|--------------------|---------------------------------------------------------------|
| `id`               | Campaign id (use for report/hold/resume/stop)                  |
| `active`           | `active` / `hold` / `ended` / `stoped`                         |
| `timed`            | `true` = scheduled for the future                              |
| `total_phones`     | Total recipient count                                          |
| `total_sent`       | How many have been dialled so far                              |
| `new_phones`       | How many are still to dial                                     |
| `answerd_calls`    | Answered calls                                                 |
| `dialing_calls`    | Currently dialling (active campaign only)                      |
| `retries_list`     | Waiting for retry                                              |
| `run_time`         | Actual run length, `hh:mm:ss`                                  |
| `max_ring_view`    | `"ללא הגבלה"` if no cap was set                                 |
| `between_retries`  | Minutes between retries                                        |

> Fields `answerd_calls`, `total_sent`, `dialing_calls`, `retries_list` come back as the string `"ממתין למידע..."` when the campaign was just created and hasn't started dialling yet.

---

## `campaignReport` — full per-recipient report

Returns **all the data shown in the campaign report** (CSV / HTML view) for a single campaign — campaign metadata + one row per recipient. Values are **formatted and enriched** (not raw DB rows): formatted phone, customer name (joined from `clientes`), call duration as `mm:ss`, Q.850 hangup-cause translated to Hebrew, human-readable times.

### Request

| Param        | Required | Description                                                     |
|--------------|----------|-----------------------------------------------------------------|
| `action`     | yes      | `campaignReport`                                                |
| `apiKey`     | yes      | Tenant API key                                                  |
| `campaignId` | yes      | Numeric campaign id (from `campaignsHistory` `id` field)        |

### Example

```bash
curl -X POST https://api.tlivr.com/campaignApi.php \
  -H "Content-Type: application/json" \
  -d '{
    "action":     "campaignReport",
    "apiKey":     "YOUR_API_KEY",
    "campaignId": 4521
  }'
```

### Response

A JSON object with two blocks. **Keys are stable English identifiers**; values follow the report (Hebrew status strings, translated Q.850, formatted phone/duration).

```json
{
  "campaign": {
    "id":                     "4521",
    "title":                  "קמפיין ינואר",
    "callId":                 "abc-xyz",
    "status":                 "הסתיים",
    "recipients":             "1500",
    "dialRetries":            "3",
    "betweenRetriesMinutes":  "20",
    "callLength":             "45",
    "billing":                "625.00",
    "ipRoutingUnits":         "12",
    "sttUnits":               "7",
    "answeredCalls":          "943"
  },
  "calls": [
    {
      "phone":     "050-1234567",
      "name":      "כהן ישראל",
      "status":    "answered",
      "lastCall":  "10:23:45",
      "retries":   "1",
      "duration":  "00:23",
      "digits":    "3",
      "trunk":     "",
      "sipCode":   "200",
      "q850Text":  "ניתוק תקין (BYE)",
      "sipText":   "OK",
      "had180":    "yes",
      "had183":    "no"
    }
  ]
}
```

### Field reference

#### `campaign`

| Key                       | Meaning                                                                  |
|---------------------------|--------------------------------------------------------------------------|
| `id`                      | Campaign id                                                              |
| `title`                   | Campaign description                                                     |
| `callId`                  | Internal call id                                                         |
| `status`                  | `"הסתיים"` (ended) or `"טרם הסתיים"` (not ended)                         |
| `recipients`              | Total recipient count                                                    |
| `dialRetries`             | Configured max dial retries                                              |
| `betweenRetriesMinutes`   | Wait between retries — **in minutes** (not seconds)                      |
| `callLength`              | Configured per-call length cap                                           |
| `billing`                 | Total units charged for the campaign                                     |
| `ipRoutingUnits`          | Units used by IP routing — **present only if used**                      |
| `sttUnits`                | Units used by speech-to-text — **present only if used**                  |
| `answeredCalls`           | Count of answered calls                                                  |

#### `calls[]`

| Key        | Meaning                                                                                                                                |
|------------|----------------------------------------------------------------------------------------------------------------------------------------|
| `phone`    | Formatted phone number                                                                                                                 |
| `name`     | Customer name from `clientes` (empty if not found)                                                                                     |
| `status`   | Call status (`answered`, `noAnswer`, `busy`, …)                                                                                        |
| `lastCall` | Last dial time, `HH:MM:SS` (Asia/Jerusalem)                                                                                            |
| `retries`  | Retry count for this specific recipient                                                                                                |
| `duration` | Call duration as `mm:ss`                                                                                                               |
| `digits`   | Key the listener pressed (if any)                                                                                                      |
| `trunk`    | Trunk used for the call                                                                                                                |
| `sipCode`  | Final SIP response code                                                                                                                |
| `q850Text` | Q.850 hangup cause, **translated to Hebrew** via the internal map (passes through verbatim if unknown)                                 |
| `sipText`  | Final SIP response text                                                                                                                |
| `had180`   | `"yes"`/`"no"` — did the destination ever return **SIP 180 Ringing** (the phone actually rang). Empty `""` if the gateway didn't supply it. |
| `had183`   | `"yes"`/`"no"` — did the destination ever return **SIP 183 Session Progress** (early media — typically a recorded message / ring-back from the carrier, *before* the call was answered). Empty `""` if not supplied. |

### Notes

- **Source table:** `campaigns_calls` for active campaigns, `campaignsEnd_YYMM` (by `end_time`) for ended ones — picked automatically.
- All values are returned as **strings** (the structured output mirrors the CSV/HTML report row-by-row).

### SIP / Q.850 failure reference

`sipCode` is the final SIP response code from the destination. `q850Text` is the ITU Q.850 hangup cause, **translated to Hebrew** by the system before being returned.

#### Successful

| SIP   | `q850Text` (returned)        | Operational meaning                                       |
|-------|------------------------------|-----------------------------------------------------------|
| `200` | `ניתוק תקין (BYE)`            | Call answered and ended normally (one side hung up).      |
| —     | `סיום רגיל (לא צוין)`        | Normal teardown without a specific cause code.            |

#### Number / dialing problem

| SIP   | `q850Text` (returned)                       | Operational meaning                                                  |
|-------|---------------------------------------------|----------------------------------------------------------------------|
| `404` | `מספר לא מוקצה (404)`                       | Number does not exist at destination carrier — **stop retrying**.    |
| `484` | `מספר שגוי / כתובת חלקית (484)`             | Number was sent partial / in invalid format.                         |
| `488` | `יעד לא תואם (488)`                          | Far end doesn't support the codec / call type — compatibility issue. |

#### Recipient unavailable

| SIP   | `q850Text` (returned)               | Operational meaning                                                  |
|-------|-------------------------------------|----------------------------------------------------------------------|
| `486` | `משתמש תפוס (486)`                  | Line is busy on another call — worth a retry.                        |
| `408` | `אין תגובת משתמש (408)`             | Far end didn't respond to the SIP request in time (timeout).         |
| `480` | `אין מענה (480)`                    | Device reachable but no one answered (off / no coverage / DND).      |
| `603` | `השיחה נדחתה (603)`                  | Recipient actively rejected — typically a "Reject" press or a block. |

#### Network / infrastructure

| SIP   | `q850Text` (returned)              | Operational meaning                                                  |
|-------|------------------------------------|----------------------------------------------------------------------|
| `502` | `יעד לא פעיל (502)`                | Destination Gateway not responding or returned an error.             |
| `503` | `אין ערוץ זמין (503)`              | No free channels at the carrier.                                     |
| `503` | `תקלה ברשת (503)`                  | General SIP-network failure at destination.                          |
| `503` | `כשל זמני (503)`                   | Temporary failure — retry is reasonable.                             |
| `503` | `עומס בציוד מיתוג (503)`           | Switching equipment too loaded to accept additional calls.           |
| `503` | `משאב לא זמין (503)`               | A required resource (codec, trunk…) isn't available.                 |

#### Internal timeout

| SIP   | `q850Text` (returned)                  | Operational meaning                                  |
|-------|----------------------------------------|------------------------------------------------------|
| `504` | `התאוששות עקב פג זמן (504)`           | Internal timeout — call was auto-aborted.            |

#### Other

| SIP | `q850Text` (returned)        | Operational meaning                                                                                  |
|-----|------------------------------|------------------------------------------------------------------------------------------------------|
| —   | `שירות מבוקש לא מנוי`        | The requested service (extension, destination, international…) isn't subscribed on the account.      |
| —   | `שגיאת פרוטוקול`             | SIP-level protocol error — malformed message / incompatibility.                                      |
| —   | `בין-רשתי`                   | Interworking issue — usually crossing between SIP and PSTN.                                          |
| —   | `לא ידוע`                    | Unclassified cause — check `sipCode` + `sipText` for more detail.                                    |

> If `q850Text` returns a value **not in this list**, that's the original English string returned by the Gateway (no auto-translation). Check `sipText` for additional context.

### Dialing indicators — `had180` / `had183`

Two boolean fields (`"yes"` / `"no"`; empty string `""` when the gateway didn't supply the signal) that help distinguish failure modes when `sipCode` alone is ambiguous — e.g. did the recipient's phone actually ring, or did the carrier play a recorded announcement before the call was rejected.

| Field    | Meaning                                                                                              | Operational use                                                                                                                                                |
|----------|------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `had180` | **SIP 180 Ringing** was received during dialing — the recipient's phone **actually rang**.            | `"yes"` + no-answer ⇒ the device was reachable but unanswered (off / DND / missed) — a retry is reasonable. `"no"` ⇒ the carrier rejected before any ring (network/number-level block). |
| `had183` | **SIP 183 Session Progress** was received — early media (carrier ring-back tone or pre-answer recorded announcement) was played before the call was answered. | `"yes"` + no-answer often indicates a carrier announcement ("subscriber unavailable", "number disconnected", etc.) — retrying immediately is unlikely to help. |

> **Availability:** these flags rely on signaling indicators (`RINGTIME` / `PROGRESSTIME`) from the upstream provider. Not all providers report them — when missing, the value is the empty string `""`.

---

## `max_ringSet` — change concurrency on a running campaign

Updates the live parallel-dial cap of an active campaign.

### Request

| Param         | Required | Notes |
|---------------|----------|-------|
| `action`      | yes      | `max_ringSet` |
| `apiKey`      | yes      | Tenant key |
| `campaignId`  | yes      | Integer |
| `max_ringSet` | yes      | New concurrency limit (integer) |

### Example

```bash
curl -X POST https://api.tlivr.com/campaignApi.php \
  -H "Content-Type: application/json" \
  -d '{
    "action":      "max_ringSet",
    "apiKey":      "YOUR_API_KEY",
    "campaignId":  4521,
    "max_ringSet": 10
  }'
```

### Response

```json
{ "errorCode": 0 }
```

```json
{ "errorCode": 1, "note": "סכום לא תקין" }
```

The error variant fires when the value exceeds 99999.

---

## `campaignHold` — pause a campaign

Pauses an active campaign. Unsent recipients are preserved for resumption.

> **A paused campaign is closed automatically 21 days after it started.** From that point it can no longer be resumed (`campaignResumption` returns `errorCode 2`); it ends like a stopped campaign — numbers not yet called are dropped, and the final report and billing are calculated from the calls actually made.

| Param        | Required | Notes |
|--------------|----------|-------|
| `action`     | yes      | `campaignHold` |
| `apiKey`     | yes      | Tenant key |
| `campaignId` | yes      | Integer |

```bash
curl -X POST https://api.tlivr.com/campaignApi.php \
  -H "Content-Type: application/json" \
  -d '{ "action":"campaignHold", "apiKey":"YOUR_API_KEY", "campaignId":4521 }'
```

Success: `{ "errorCode": 0 }`. The campaign's `active` field becomes `hold`.

`{ "errorCode": 25 }` — the campaign doesn't exist or belongs to another account (same for `campaignResumption`, `campaignStop` and `max_ringSet`).

---

## `campaignResumption` — resume a paused campaign

Restarts a campaign currently in `hold` state.

| Param        | Required | Notes |
|--------------|----------|-------|
| `action`     | yes      | `campaignResumption` |
| `apiKey`     | yes      | Tenant key |
| `campaignId` | yes      | Integer |

```bash
curl -X POST https://api.tlivr.com/campaignApi.php \
  -H "Content-Type: application/json" \
  -d '{ "action":"campaignResumption", "apiKey":"YOUR_API_KEY", "campaignId":4521 }'
```

| Response                   | Meaning                                |
|----------------------------|----------------------------------------|
| `{ "errorCode": 0 }`       | Resumed                                |
| `{ "errorCode": 1 }`       | Campaign is not in `hold` state        |
| `{ "errorCode": 2 }`       | Campaign too old (> 21 days)           |
| `{ "errorCode": 25 }`      | Campaign not found / belongs to another account |

---

## `campaignStop` — hard-stop a campaign

**Irreversible.** Closes the campaign permanently. Pending recipients are discarded. Use [`campaignHold`](#campaignhold--pause-a-campaign) for reversible pauses.

| Param        | Required | Notes |
|--------------|----------|-------|
| `action`     | yes      | `campaignStop` |
| `apiKey`     | yes      | Tenant key |
| `campaignId` | yes      | Integer |

```bash
curl -X POST https://api.tlivr.com/campaignApi.php \
  -H "Content-Type: application/json" \
  -d '{ "action":"campaignStop", "apiKey":"YOUR_API_KEY", "campaignId":4521 }'
```

Success: `{ "errorCode": 0 }`. `{ "errorCode": 25 }` — campaign not found / belongs to another account.

On an [open campaign](#open-campaigns--campaignopen--campaignpush--campaignclose), `campaignStop` also closes it: later `campaignPush` calls get `errorCode 46` with `closeType: "stop"`.

---

## `addDid` — register an outbound Caller-ID

Registers a new outbound caller-ID (DID) on the account. Ownership of the number is proven via a **silent caller-ID verification**: the system places a call whose own caller-ID display contains a one-time code, and the user reads the **last 6 digits of the calling number off their phone screen** and submits them.

> **The recipient does not need to answer the call.** No audio, no TTS, no voicemail involved — the verification code is the last 6 digits of the displayed number. Just look at the screen and copy them.

> **`addDid` ≠ recipient approval.** This action registers the **"from"** of the call (Caller-ID). It has no effect on the `phones` list (the "to"). Recipients need no registration — see [Recipient-number rules](#recipient-number-rules--read-this-before-using-campaignrun).

### Two-step flow

```
┌─────────────────────────┐         ┌──────────────────────────────────┐         ┌────────────────────────┐
│ 1. Request verification │  ───▶   │ 2. Phone rings — calling number  │  ───▶   │ 3. Submit last 6 digits│
│    (action=addDid +     │         │    on the screen ends with the   │         │    of that number      │
│    PhoneVerificationSend│         │    6-digit code. No need to      │         │    (action=addDid +    │
│    + phone)             │         │    answer.                       │         │    PhoneVerification)  │
└─────────────────────────┘         └──────────────────────────────────┘         └────────────────────────┘
```

Both steps share the same endpoint and the same `action=addDid` — what differs is whether `PhoneVerificationSend` or `PhoneVerification` is present.

### Step 1 — trigger the verification call (`PhoneVerificationSend`)

Places a call to `phone`. The **caller-ID displayed on the recipient's screen ends with a one-time 6-digit code**. The recipient simply reads those digits — there is no need to answer or listen to anything.

#### Request

| Param                     | Required | Description                                                              |
|---------------------------|----------|--------------------------------------------------------------------------|
| `action`                  | yes      | `addDid`                                                                 |
| `apiKey`                  | yes      | Tenant API key                                                           |
| `phone`                   | yes      | Phone number to register (Israeli format, e.g. `0501234567`)             |
| `PhoneVerificationSend`   | yes      | Any truthy value (e.g. `1`) — selects the "send" branch                  |

#### Example

```bash
curl -X POST https://api.tlivr.com/campaignApi.php \
  -H "Content-Type: application/json" \
  -d '{
    "action":                 "addDid",
    "apiKey":                 "YOUR_API_KEY",
    "phone":                  "0501234567",
    "PhoneVerificationSend":  "1"
  }'
```

#### Response — success

```json
{ "Status": "Ok", "ErrorCode": "0" }
```

The system places the call within seconds. The recipient sees an incoming call from a number whose **last 6 digits are the verification code** — they only need to read those digits off the display. The code is valid for **10 minutes**.

#### Response — errors

| Body                                                                                      | When                                                                  |
|-------------------------------------------------------------------------------------------|-----------------------------------------------------------------------|
| `{"Status":"Error","note":"המספר כבר קיים"}`                                              | The phone is already registered as a caller-ID on this account. **Returned before** the `PhoneVerificationSend` check, so no call is placed. |
| `{"Status":"Error","ErrorCode":"1","note":"המספר אינו תקין"}`                             | The phone value is shorter than 5 digits / fails numeric sanity.       |
| `{"Status":"Error","ErrorCode":"1","note":"המערכת זיהתה מספר נסיונות מרובים בשעה האחרונה למספר שנבחר, נסה שנית מאוחר יותר"}` | More than 5 verification calls were placed to this number in the last 24 hours (rate limit). |

### Step 2 — confirm code (`PhoneVerification`)

Submits the **last 6 digits of the calling number** the recipient saw on their screen, and on success registers the phone as a caller-ID for the account.

#### Request

| Param                | Required | Description                                                                              |
|----------------------|----------|------------------------------------------------------------------------------------------|
| `action`             | yes      | `addDid`                                                                                 |
| `apiKey`             | yes      | Tenant API key                                                                           |
| `phone`              | yes      | Same phone sent in step 1                                                                |
| `PhoneVerification`  | yes      | The **last 6 digits of the displayed calling number**. Non-digit characters are stripped. |

#### Example

```bash
curl -X POST https://api.tlivr.com/campaignApi.php \
  -H "Content-Type: application/json" \
  -d '{
    "action":             "addDid",
    "apiKey":             "YOUR_API_KEY",
    "phone":              "0501234567",
    "PhoneVerification":  "482917"
  }'
```

#### Response — success

```json
{ "Status": "Ok", "ErrorCode": "0" }
```

The number is now appended to the account's caller-ID list and may be used as `callId` in `campaignRun`.

#### Response — failure

```json
{ "Status": "Error", "note": "האימות נכשל" }
```

Returned when the (`phone`, `code`) pair doesn't match any record. Re-trigger step 1 to receive a new code.

### Notes

- **Verification is by caller-ID display only** — no answer required, no audio plays. This survives DND, silent mode, and rejected calls equally well.
- **Response shape uses capital-S `Status`** (`"Ok"` / `"Error"`) — note the capitalisation differs from `campaignRun`'s lowercase `status`.
- **Rate limit:** max 5 verification calls per phone per 24h — applies before any code is generated.
- **Code TTL:** 10 minutes from issue. Treat that as the contract.
- **Idempotency:** placing step 1 again before consuming the previous code does **not** invalidate the previous code — the latest code is the one to use.

---

## FAQ — common AI-agent questions

### "I got `errorCode: 6` — how do I 'approve' a number / add it to clientes / whitelist it?"

**You don't, and there is no such endpoint.** errorCode 6 does not mean "missing approval" — it means **every number in your `phones` array was rejected by format validation or the per-account opt-out list**. The word "מאושרים" in the Hebrew error is a misleading translation; the system has no recipient-approval concept. See [Recipient-number rules](#recipient-number-rules--read-this-before-using-campaignrun). To fix: ensure every number matches one of the three Israeli patterns (`02/03/04/08/09` 9-digit, `07X` 10-digit, `05X` 10-digit) and is not on the account opt-out list.

### "Is there a separate API for a single outbound call (click-to-call), not a broadcast?"

**No, and you don't need one.** `campaignRun` with `phones: ["0501234567"]` (one number) is the canonical pattern. Same endpoint, same shape. See [Example — single outbound "ring my agent"](#example--single-outbound-ring-my-agent-with-key-routing-to-my-ivr).

### "How do I connect the answered call to my own IVR / `main.php`?"

Use `keysAction-N`. The supported routings are:
- `routing-0XXXXXXXXX` — bridge to a phone number (PSTN)
- `pbxRouting-EXT` — bridge to a PBX extension on your account
- `sipRouting-sip@host` — bridge to a SIP URI

There is **no `urlRouting-` / HTTP-callback / webhook-to-external-IVR option** in this API. If your IVR is a web URL, you need to front it with one of those three. If your IVR is already hosted on this platform (`main.php` style), use `pbxRouting-` with the relevant extension id.

### "What's the difference between `addDid` and `phones`?"

- `phones` = **the people you're calling** (the "to" of the call). Just put valid Israeli numbers in the array.
- `addDid` registers a **Caller-ID** (the "from" — what shows on the recipient's screen). Used later via `callId` in `campaignRun`. Most accounts have a default DID, so `callId` is optional.

These are orthogonal — fixing one does not affect the other.

### "What parameters does `campaignRun` require, exactly?"

The strict minimum for a working call:

| Required | Notes |
|---|---|
| `action` | `campaignRun` |
| `apiKey` | Tenant key |
| `phones` **or** `mailingList` | At least one valid Israeli number, or the ID of a mailing list that resolves to at least one |
| One of: `audioFile` / `audioText` / `messagesType=extensionActivation`+`extensionActivation` | What to do on answer |
| `betweenRetries` | 5–120 minutes (effectively required — there's no implicit safe default) |

Everything else is optional. Full parameter list above in [campaignRun → Request parameters](#request-parameters).

### "Will my server's IP work?"

Only if it's been **approved in advance** in your personal area, under the **"אבטחת API" (API Security)** tab. Any IP that wasn't pre-approved gets `errorCode: -99 / Access denied` regardless of how correct your auth is. This is account-level config, not something you fix in code.

### "How do I supply a phone list bigger than fits in a JSON body?"

Two options. Either submit `phones` as a **`multipart/form-data` file upload** — a plain text file with comma-separated numbers, same field name `phones` — or, better for recurring sends, load the numbers once into a **mailing list** and pass `mailingList` instead. See [Targeting a mailing list](#targeting-a-mailing-list).

### "My numbers arrive one by one (new leads). Do I need a new campaign for each?"

No — use an [open campaign](#open-campaigns--campaignopen--campaignpush--campaignclose). Create it once with `campaignOpen`, send each lead (or batch of up to 5,000) with `campaignPush`, and **close it yourself** with `campaignClose` when you're done — it does not close by itself. (Safety limit: if it is still open 7 days after `campaignOpen`, the system closes it; that deadline is returned as `autoCloseAt` / `autoCloseAtUnix`.) A number that was already called in the campaign is called again by default (`duplicatePolicy=allow`); a number still waiting to be called is never duplicated.

### "Can I run a campaign against a saved mailing list instead of pasting numbers?"

Yes — send `mailingList` (one ID, or several comma-separated) in place of `phones`. The system resolves the audience at dispatch time, skips contacts blocked inside the list, de-duplicates across lists, and still applies the account opt-out filter. Full details in [Targeting a mailing list](#targeting-a-mailing-list); the lists themselves are managed in the [Mailing Lists API](mailingListsApiDocs.md).

---

## Quick reference — all actions at a glance

| Action               | Required params (beyond `action`+`apiKey`)                                  | Purpose                                            |
|----------------------|------------------------------------------------------------------------------|----------------------------------------------------|
| `campaignRun`        | `phones` **or** `mailingList`, one of `audioFile`/`audioText`/`extensionActivation`, `betweenRetries` | Create + start a campaign                          |
| `campaignOpen`       | Same as `campaignRun`, but `phones` optional (≤ 5,000)                      | Create an open campaign — stays open until **you** close it with `campaignClose` (7-day safety limit) |
| `campaignPush`       | `campaignId`, `phones` (JSON array, 1–5,000)                                 | Add numbers to an open campaign                    |
| `campaignClose`      | `campaignId`                                                                 | Close an open campaign                             |
| `campaignsHistory`   | —                                                                            | List campaigns in a date range                     |
| `campaignReport`     | `campaignId`                                                                 | Full per-recipient report                          |
| `max_ringSet`        | `campaignId`, `max_ringSet`                                                  | Change concurrency on a running campaign           |
| `campaignHold`       | `campaignId`                                                                 | Pause                                              |
| `campaignResumption` | `campaignId`                                                                 | Resume a paused campaign                           |
| `campaignStop`       | `campaignId`                                                                 | Hard-stop (irreversible)                           |
| `addDid`             | `phone` + (`PhoneVerificationSend` for step 1, `PhoneVerification` for step 2) | Register an outbound Caller-ID                     |

> Reminder: all requests are POST to `https://api.tlivr.com/campaignApi.php` with `apiKey` on every call, from a pre-whitelisted server IP.
