# Live call analysis by a partner

A partner service, for example fraud detection, can follow the calls on one of your lines (SIP trunks) while they happen and tell the AI on the call what to do.

- **Call events.** When a call on the line is answered and when it ends, we POST a short JSON message to the partner.
- **Audio.** For every call we open a WebSocket to the partner and stream both sides of the call.
- **Instructions back to the AI.** The partner sends a message for a live call, for example *"Warn the caller: asking for an OTP while posing as the bank is a known scam."* Only the AI sees it. The AI acts on it right away, in its own words and in the language of the call. The message never shows in the transcript.

## Turn it on for a line

1. Open **Telephony**, then the line (SIP trunk) whose calls you want analysed.
2. In **Live call analysis**, fill in the partner's addresses and token (see the table below).
3. Click **Save live call analysis**. From the next call on, every call on that line goes to the partner.

To stop, empty both addresses and save. You can also set it from your AI assistant with the `set_trunk_call_stream` tool. It is not available on Voice Logica numbers: use your own line.

| Field | Example |
| --- | --- |
| **Audio address** (WebSocket) | `wss://partner.example.com:8443/voicelogica/stream` |
| **Events address** (HTTP) | `https://partner.example.com:8443/voicelogica/events` |
| **Token** | A long random string. We send it as `Authorization: Bearer <token>` on both. |

Each address is a full URL with the port and path, for example `wss://150.140.10.20:8443/stream`. Use `wss://` and `https://`: the audio and the phone numbers then travel encrypted. Plain `ws://` and `http://` are only for tests.

The partner can also accept connections only from our servers' IP addresses. Ask us for the list.

## Call events

For every call on the line, whether the AI answers it or it is connected to a person, we send a short JSON message to the events address as things happen: the call is answered, a transfer starts, works or fails, the call ends.

### Event names

Each event's `event` field is written **`subject.action`**: what it is about, then what happened. Every event today is about the call, so they all start with `call.`. Later we may add other subjects on the same address (for example a recording or an analysis), so route by the full name and ignore names you don't know.

| Event | When it is sent |
| --- | --- |
| `call.started` | The call is answered. |
| `call.transfer_started` | A transfer begins: the AI or a person is passing the call on. |
| `call.transferred` | The transfer worked: the caller is now with the new destination. |
| `call.transfer_failed` | The transfer did not happen: the recipient declined, was busy, did not answer, or the number failed. |
| `call.transfer_cancelled` | The transfer was dropped and the caller stays where they were. |
| `call.ended` | The call is over. |

A call can have several transfers, one after the other. Each attempt sends its own transfer events.

### Fields in every event

| Field | |
| --- | --- |
| `event` | The event name, from the table above. |
| `callId` | Our id for the call. Use it to send instructions to the AI on this call, and to match events and the audio stream of the same call. |
| `sipCallId` | The SIP Call-ID of the call on your line, when there is one. |
| `sipTrunkId` | The line (SIP trunk) the call is on. |
| `direction` | `inbound` (someone called the line) or `outbound` (the line called someone). |
| `from`, `to` | The caller's and the called number. |
| `startedAt` | When the call started (ISO 8601, UTC). |

### Extra fields per event

| Event | Extra fields |
| --- | --- |
| `call.transfer_started`, `call.transferred`, `call.transfer_cancelled` | `transfer.destination` (the number or address), `transfer.destinationType` (e.g. `external_number`, `user`, `user_group`), `transfer.mode` (`blind`, `attended` or `warm`), `transfer.reason` (why the AI transferred, when it said). |
| `call.transfer_failed` | The same, plus `transfer.failedReason`: what happened, as recorded on the call, e.g. "Destination declined", "busy", "No answer after 30 seconds of ringing". |
| `call.ended` | `endedAt`, `durationSeconds`, `status` (e.g. `answered`, `transferred`, `missed`), `reason` (why it ended, e.g. `caller_hungup`). |

A field we have no value for is left out rather than sent empty.

### How delivery works

- **Request:** `POST` to the events address, body `application/json`, header `Authorization: Bearer <token>`. Check the token on every request and refuse anything else.
- **Your reply:** any `2xx` within 5 seconds. The body is ignored.
- **Retries:** if there is no `2xx` (or no answer in 5 seconds) we try once more right away. If that fails too, the event is dropped and the failure is logged on the call. We never hold the call while we wait.
- **Once only:** each event is sent once per call (once per attempt for transfer events), even when several of our servers handle the same call.
- **Order:** events go out as the call changes, from whichever of our servers handled that change, so two events close together (for example `call.transferred` and `call.ended`) can arrive in either order. Go by the event name, not by arrival order.
- **New events and fields:** we may add event names and fields without notice. Ignore what you don't know. Removing or renaming one would be announced first.

### Examples

```json
{
  "event": "call.started",
  "callId": "6adf55e278e0d0de8ce090b2",
  "sipCallId": "a84c3c7f-77c8@10.0.0.5",
  "sipTrunkId": "69dcacb29e637bf355a3b834",
  "direction": "inbound",
  "from": "+306900000000",
  "to": "+302100000000",
  "startedAt": "2026-10-26T13:30:11.970Z"
}
```

```json
{
  "event": "call.ended",
  "callId": "6adf55e278e0d0de8ce090b2",
  "sipTrunkId": "69dcacb29e637bf355a3b834",
  "direction": "inbound",
  "from": "+306900000000",
  "to": "+302100000000",
  "startedAt": "2026-10-26T13:30:11.970Z",
  "endedAt": "2026-10-26T13:31:31.242Z",
  "durationSeconds": 79,
  "status": "answered",
  "reason": "caller_hungup"
}
```

Transfer events carry a `transfer` object:

```json
{
  "event": "call.transfer_failed",
  "callId": "6adf55e278e0d0de8ce090b2",
  "sipTrunkId": "69dcacb29e637bf355a3b834",
  "direction": "inbound",
  "from": "+306900000000",
  "to": "+302100000000",
  "startedAt": "2026-10-26T13:30:11.970Z",
  "transfer": {
    "destination": "+302109999999",
    "destinationType": "external_number",
    "mode": "warm",
    "reason": "Caller asked for the accounts department",
    "failedReason": "Destination declined"
  }
}
```

`callId` is the id to use when you send instructions for this call.

## Audio stream

One WebSocket per call, opened as soon as the call reaches us, with `Authorization: Bearer <token>`. Every call on the line is streamed, whether the AI answers it or it is connected to a person.

**1. First message (text, JSON):** the call and the audio format.

```json
{
  "event": "start",
  "callId": "6adf55e278e0d0de8ce090b2",
  "sipTrunkId": "69dcacb29e637bf355a3b834",
  "direction": "inbound",
  "from": "+306900000000",
  "to": "+302100000000",
  "startedAt": "2026-10-26T13:30:11.970Z",
  "version": 1,
  "bidirectional": false,
  "audio": {
    "encoding": "pcm_s16le",
    "sampleRate": 8000,
    "channels": 2,
    "frameMs": 20,
    "layout": ["caller", "callee"],
    "framing": "channel_prefixed"
  }
}
```

`version` is the protocol version, `1` today. `bidirectional` is `false`: we only send audio, and anything the partner sends back on the socket is ignored. Lines where the partner runs the whole call and its audio is played to the caller will use the same protocol with `bidirectional: true`, so check both fields.

**2. Audio (binary):** each message is one side of the call, 20 ms long.

| Byte | Content |
| --- | --- |
| 0 | Side: `0` = the caller (the person on your line), `1` = the other side (the AI, or the person the call is connected to) |
| 1 onward | 20 ms of PCM 16-bit little-endian, mono. At 8 kHz that is 320 bytes, so each message is 321 bytes. |

The two sides come as separate messages, so you get each one whole, with no mixing and no silence added to line them up.

**3. Last message (text, JSON)** when the call ends. Then we close the connection. The reason the call ended comes in the `call.ended` event.

```json
{ "event": "stop", "callId": "6adf55e278e0d0de8ce090b2" }
```

If the partner reads too slowly and more than about 1 MB is waiting to be sent, we drop audio frames until it catches up, so a slow partner never slows the call down.

## Send an instruction to the AI on a call

```bash
curl -X POST https://api.voicelogica.ai/api/v1/phones/calls/6adf55e278e0d0de8ce090b2/instructions \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"message": "Warn the caller: asking for an OTP while posing as the bank is a known scam.", "type": "possible_scam", "severity": "high", "confidence": 0.87}'
```

| Field | |
| --- | --- |
| `message` | Required, up to 2,000 characters. What the AI should do or say now. |
| `type` | Optional, e.g. `possible_scam`. |
| `severity` | Optional, e.g. `warning`, `high`, `critical`. |
| `confidence` | Optional, 0 to 1. Kept in the call log only. |

The API key needs the **call.send_call_instruction** permission. Create it on **Team and access - API Keys** in the company that owns the line ([API Keys](https://docs.voicelogica.ai/getting-started/api-keys/)). From an AI assistant connected to Voice Logica (MCP), the same is the `send_call_instruction` tool.

**Answer:** `200` with `{ "callId": "...", "delivered": true }` once the AI has the instruction. `400` when the call has ended, is not answered yet, or has no AI on it. `404` when the call is not one of this company's calls.

Every instruction is kept in the call's log with its type, severity and confidence.

**Does the caller hear the instruction?**
No. Only the AI sees it. The caller hears what the AI then says, in its own words.

**Which calls are sent to the partner?**
Every call on that line, audio and events, whether the AI answers it or it is connected to a person. To send only some calls, put them on a separate line.

**Can the partner send several alerts on one call?**
Yes. Each instruction is acted on when it arrives.
