# How to configure UniTalk

Connect UniTalk to ConnectiveOne with the built-in **UniTalk** provider preset. Completed calls appear in the call archive with recording, assignee, and client card fields.

This is a full implementation guide: preset, webhook, two operator-assignment paths (with ready JSON), client fields, smoke test, and troubleshooting.

## Prerequisites

- UniTalk admin access
- ConnectiveOne bot settings (**Settings → Bots → Telephony**)
- Operator emails in ConnectiveOne match emails in UniTalk (for auto-assignment)
- For **path A** (recommended): SIP line (**line** in UniTalk) ↔ operator email table
- For **path B** (optional): UniTalk API key (**Integration → API**)

## When to use it

- UniTalk is your telephony provider
- You need a quick setup without manual webhook field mapping
- Completed calls should appear in Operator Panel with recording and assignee

## Two ways to assign the operator

| | Path A — `operator_mapping` | Path B — `action_telephony_staff_enrich` |
|---|---|---|
| Idea | Static SIP → email table in Advanced Config | UniTalk API `/users/list`, match on **`line`** |
| External API | No | Yes (cache required — UniTalk rate limits) |
| When to choose | Stable line list (typical production setup) | Lines change often; you do not want a manual table |
| Channel setup | `operator_mapping` block in JSON | **API Key** + **Webhook enrich action** = `action_telephony_staff_enrich` |

> When `operator_mapping` matches, ConnectiveOne **skips** enrich — no extra API calls.

`action_telephony_staff_enrich` is a **generic** internal action (any HTTP staff list via json_config). Below is a ready UniTalk configuration.

---

## Step 1: Open channel settings

1. **Settings** → **Bots**
2. Select the bot
3. Open the **Telephony** tab
4. Create a new channel or edit an existing one

Minimum fields:

| Channel field | Value |
|---------------|-------|
| Name | e.g. `UniTalk` |
| Active | On |
| Default country code | e.g. `UA` / `380` (as on your instance) |

After the first save you get **Webhook URL** and token.

## Step 2: Apply the UniTalk preset

1. In **Advanced Config (JSON)**, select **UniTalk** from the provider preset list
2. Click **Apply preset**
3. **Save** and copy the **Webhook URL** (including `?token=...`)

The preset adds:

- `event_detection` — only `CALL_END`
- `field_mapping` / `status_mapping` / `direction_mapping`
- `client_field_mapping` — UTM, source, comment → client card
- recordings from `call.link`

Preset JSON (without `operator_mapping`) also lives in the repo: `apps/engine/modules/extra/telephony/presets/unitalk.json`.

## Step 3: Webhook in UniTalk

1. UniTalk cabinet → **Event handlers**
2. Add a **Send webhook** handler
3. Bind **only** to **`CALL_END`** (do not send `CALL_NEW`, etc.)

| Field in UniTalk | Value |
|------------------|-------|
| **Method** | `POST` |
| **URL** | Full ConnectiveOne **Webhook URL**, e.g. `https://<your-domain>/kw/telephony/webhook/<channel_id>?token=<webhook_token>` |
| **Body** | **Standard JSON** |

> Token only in the URL (`?token=...`). Do not change the `/kw/telephony/webhook/...` path.

### Example `CALL_END` body (inbound)

```json
{
  "event": "CALL_END",
  "call": {
    "id": "sample-call-id",
    "from": "380501234567",
    "to": "9578",
    "direction": "in",
    "state": "ANSWER",
    "secondsFullTime": 120,
    "secondsTalk": 90,
    "date": "2026-09-02T12:00:00.000Z",
    "link": "https://example.test/recording.mp3",
    "utmSource": "google",
    "utmCampaign": "brand",
    "source": "ads",
    "comment": "Call comment"
  }
}
```

| UniTalk field | After UniTalk preset in ConnectiveOne |
|---------------|----------------------------------------|
| `event` | Must be `CALL_END`, otherwise 200 ignored |
| `call.id` | Call ID (idempotency — retry does not create a duplicate) |
| `call.from` / `call.to` | Client number or operator SIP (depends on `direction`) |
| `call.direction` | `in` / `out` |
| `call.state` | `ANSWER` → answered; `NOANSWER`, `BUSY`, … → missed |
| `call.link` | Recording |
| `call.utmSource`, `utmCampaign`, `source`, `comment` | Client card (`client_field_mapping`) |

**Direction and operator SIP:**

| Direction | Client number | Operator SIP (key in `operator_mapping` / enrich lookup) |
|-----------|---------------|----------------------------------------------------------|
| `in` (inbound) | `call.from` | `call.to` |
| `out` (outbound) | `call.to` | `call.from` |

In the ConnectiveOne channel form: webhook format = **JSON**.

---

## Step 4A: Operator assignment via `operator_mapping` (recommended)

After **Apply preset**, add `operator_mapping` to the same JSON (same level as `event_detection`, `field_mapping`, …), then **Validate JSON** / **Save**.

### Fragment example

```json
"operator_mapping": {
  "source": {
    "in": "callee_number",
    "out": "caller_number"
  },
  "map": {
    "9578": "operator@example.com",
    "201": "operator2@example.com"
  }
}
```

### Full Advanced Config example (preset + mapping)

Copy, replace SIP → email with your values, save:

```json
{
  "event_detection": {
    "mode": "event_field",
    "event_field": "event",
    "call_ended_values": ["CALL_END"]
  },
  "field_mapping": {
    "call_id": "call.id",
    "caller_number": "call.from",
    "callee_number": "call.to",
    "status": "call.state",
    "duration": "call.secondsFullTime",
    "talk_duration": "call.secondsTalk",
    "recording_url": "call.link",
    "direction": "call.direction",
    "date": "call.date",
    "utm_source": "call.utmSource",
    "utm_campaign": "call.utmCampaign",
    "call_source": "call.source"
  },
  "status_mapping": {
    "ANSWER": "answered",
    "NOANSWER": "missed",
    "BUSY": "missed",
    "CANCEL": "missed",
    "AUTOCANCEL": "missed",
    "UNREACHABLE": "missed",
    "VOICE_MAIL": "missed",
    "FAIL": "failed",
    "BLOCKED": "failed",
    "AUDIO_ERR": "failed"
  },
  "direction_mapping": {
    "in": "in",
    "out": "out",
    "inbound": "in",
    "outbound": "out"
  },
  "recordings": {
    "enabled": true,
    "download": false
  },
  "transcription": {
    "enabled": false,
    "source": "provider"
  },
  "client_field_mapping": {
    "utm_source": "call.utmSource",
    "utm_campaign": "call.utmCampaign",
    "call_source": "call.source",
    "last_call_comment": "call.comment"
  },
  "operator_mapping": {
    "source": {
      "in": "callee_number",
      "out": "caller_number"
    },
    "map": {
      "9578": "operator@example.com",
      "201": "operator2@example.com"
    }
  }
}
```

**Rules:**

1. Keys in `map` are **raw** SIP strings as in UniTalk (`"9578"`), **before** E.164 (not `+3809578`).
2. Email in ConnectiveOne must match an active Operator Panel user.
3. Each UniTalk operator must have an internal **line** filled in.
4. **Webhook enrich action** can stay empty.

---

## Step 4B: Dynamic assignment via `action_telephony_staff_enrich` (optional)

Use when you do **not** maintain `operator_mapping` (or as a fallback when a line is missing from the table).

### 4B.1 Channel fields

| Field | Value |
|-------|-------|
| **API Key** | UniTalk API key (**Integration → API**) |
| **API Base URL** (if present) | `https://api.unitalk.cloud/api` |
| **Webhook enrich action** | `action_telephony_staff_enrich` |

The key is a secret. Do not publish it in chats or tickets.

### 4B.2 Action parameters (json_config)

The action ships with the product (internal). If you use an Action Jail wrapper on the instance, put the same parameters in the action **JSON config**.

Ready UniTalk example:

```json
{
  "api_url": "https://api.unitalk.cloud/api/users/list",
  "http_method": "POST",
  "auth_header_name": "Authorization",
  "auth_scheme": "raw",
  "request_body": {},
  "response_items_path": "",
  "match_field": "line",
  "lookup_source": {
    "in": "callee_number",
    "out": "caller_number"
  },
  "email_field": "email",
  "name_fields": ["firstName", "lastName"],
  "cache_ttl_seconds": 300,
  "timeout_ms": 8000
}
```

| Parameter | Why |
|-----------|-----|
| `match_field`: **`line`** | Internal PBX SIP in `/users/list` (not `phone` — mobile, often `null`) |
| `lookup_source` | Where to take SIP from the normalized call: in → `callee_number`, out → `caller_number` |
| `cache_ttl_seconds` | Staff-list cache (UniTalk limits ~15 req/s) |
| `api_key` | In json_config **or** only in the channel **API Key** field |

If the API returns a root array, leave `response_items_path` as `""`. If wrapped as `{ "data": [ … ] }`, set `"data"`.

### 4B.3 Sample UniTalk `/users/list` fragment

```json
[
  {
    "id": 1,
    "email": "operator@example.com",
    "firstName": "Ivan",
    "lastName": "Petrenko",
    "line": "9578",
    "phone": null,
    "role": "OPERATOR"
  }
]
```

ConnectiveOne finds `line` = call SIP and writes `manager_email` / `manager_name` → assignment by email.

General enrich how-to: [Configure webhook enrichment](/en/telephony/how-to/configure-webhook-enrich.md).  
Implementor template: `docs/developer/engine/modules/telephony/action_telephony_staff_enrich.template.md`.

---

## Step 5: Client card fields

The preset already includes `client_field_mapping`:

| Key in `extra_data` | Webhook path |
|---------------------|--------------|
| `utm_source` | `call.utmSource` |
| `utm_campaign` | `call.utmCampaign` |
| `call_source` | `call.source` |
| `last_call_comment` | `call.comment` |

For fields to **show** on the client card, add matching custom fields in **Instance Settings** (`extra_data` schema). Telephony merges values without Action Jail.

---

## Step 6: Save and smoke-test

1. **Save** the channel (active).
2. Use **Test** webhook in the form **or** the curl below.
3. Make test **inbound** and **outbound** calls in UniTalk.
4. **Operator Panel → Closed requests**: `telephony` dialogue, recording (if `call.link`), assignee.

### Example curl (local / stage smoke)

Replace URL and token. Body as in step 3; for path A align `"to"` / emails with your `operator_mapping`.

```bash
curl -X POST 'https://<your-domain>/kw/telephony/webhook/<channel_id>?token=<webhook_token>' \
  -H 'Content-Type: application/json' \
  -d '{
    "event": "CALL_END",
    "call": {
      "id": "smoke-unitalk-001",
      "from": "380501234567",
      "to": "9578",
      "direction": "in",
      "state": "ANSWER",
      "secondsFullTime": 60,
      "secondsTalk": 45,
      "date": "2026-09-02T12:00:00.000Z",
      "link": "https://example.test/recording.mp3",
      "utmSource": "google",
      "utmCampaign": "brand",
      "source": "ads",
      "comment": "Smoke test"
    }
  }'
```

Expected: HTTP 200 with `dialogue_id`.  
Same `call.id` again → 200, already processed, no duplicate.

Non-`CALL_END` (e.g. `"event": "CALL_NEW"`) → 200 ignored, no dialogue.

### Acceptance checklist

| # | Check | Expected |
|---|-------|----------|
| 1 | `CALL_END` | Dialogue created, telephony channel, completed |
| 2 | Non-`CALL_END` | 200 ignored, no dialogue |
| 3 | `call.link` present | Recording on the dialogue |
| 4 | Inbound / outbound | Client by correct number |
| 5 | SIP in and out | Operator assigned (mapping or enrich) |
| 6 | UTM / comment | Values on client card (if Instance Settings fields exist) |
| 7 | Same `call.id` twice | No duplicate |
| 8 | Path B | Cache / no rate-limit lock; or path A without enrich |

---

## Troubleshooting

| Symptom | Check |
|---------|-------|
| 200 ignored on every webhook | Body has `"event": "CALL_END"`; UniTalk preset applied (`event_detection`) |
| Dialogue exists, no operator | Email in `map` / UniTalk matches C1 user; SIP as `"9578"`, not E.164; for out — line in `call.from` |
| Enrich not called | Matching `operator_mapping` — expected (enrich skipped) |
| Enrich error / no email | `match_field` = `line`; `/users/list` has `line`; channel **API Key** set |
| Duplicate dialogues | Different `call.id` from UniTalk; idempotency is per id |
| UTM not visible in UI | Merge into `extra_data` works — add custom field display in Instance Settings |
| 403 / unauthorized webhook | URL token matches channel token |

---

## What the UniTalk preset includes

- Event detection for `CALL_END` only
- Field, status, and direction mapping
- Recordings from `call.link`
- `client_field_mapping` (UTM, source, comment)

`operator_mapping` and webhook enrich are **not** in the Apply preset button — add them in steps 4A / 4B.

## Limitations

- Only completed calls (`CALL_END`); no realtime during the call
- Click-to-call from ConnectiveOne is out of scope
- UniTalk transcript / analytics is a separate scope

## Related documentation

- [Configure telephony channel](/en/telephony/how-to/configure-telephony-channel.md)
- [Configure webhook enrichment](/en/telephony/how-to/configure-webhook-enrich.md)
- [Test telephony webhook](/en/telephony/how-to/test-telephony-webhook.md)
- [View calls in Operator Panel](/en/telephony/how-to/view-calls-in-operator-panel.md)
