---
sidebar_position: 10
title: Outbound Calls & WebRTC
description: Initiating outbound calls via HTTP REST API and establishing per-call native WebSockets in the Firetell Call Center.
---

# Outbound Calls & WebRTC Signaling

Outbound calls are initiated via HTTP REST API (`POST /call-center/calls`). The API validates permissions and returns a short-lived **`call_token`** and `ws_url`.

The client then opens a native WebSocket connection for WebRTC SDP signaling scoped exclusively to that call.

---

## Make Outbound Call

```
POST /call-center/calls
```

### Request Body

| Field  | Type   | Required | Description                                               |
| ------ | ------ | -------- | --------------------------------------------------------- |
| `to`   | string | ✅       | Recipient phone number or extension (e.g. `+84901234567`) |
| `from` | string | ❌       | Outbound Caller ID phone number (e.g. `+842471000000`)    |
| `type` | string | ❌       | Call type: `audio` (default) or `video`                   |

### Request

```bash
curl -X POST "https://{workspace_id}.firetell.app/api/v1/call-center/calls" \
  -H "Authorization: Bearer YOUR_AGENT_JWT" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "+84901234567",
    "from": "+842471000000"
  }'
```

### Response `201 Created`

```json
{
  "call_id": "cl_a1b2c3d4e5f6g7h8i9j0k1",
  "status": "created",
  "call_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "ws_url": "wss://{workspace_id}.firetell.app/ws",
  "expires_in": 900
}
```

---

## Native WebSocket Signaling Protocol

Once `call_token` is received, open a native WebSocket to `ws_url`.

:::important
**3-Second Authentication Requirement**
Upon opening the WebSocket connection, the client **must send the `session.connect` event within 3 seconds**, containing the `call_token`. Unauthenticated sockets are automatically terminated after 3 seconds.
:::

### 1. Handshake (`session.connect`)

**Client Send:**

```json
{
  "event": "session.connect",
  "data": {
    "token": "YOUR_CALL_TOKEN"
  }
}
```

**Server Response:**

```json
{
  "event": "session.connected",
  "data": {
    "session_id": "socket_uuid_123",
    "workspace_id": "ws_123456789",
    "username": "agent.john",
    "call_id": "cl_a1b2c3d4e5f6g7h8i9j0k1",
    "mode": "participant"
  }
}
```

---

### 2. WebRTC SDP Offer (`call.offer`)

**Server Dispatch for Incoming Call:**

```json
{
  "event": "call.offer",
  "data": {
    "call_id": "cl_a1b2c3d4e5f6g7h8i9j0k1",
    "from": "+84901234567",
    "from_name": "Nguyen Van A (VIP)",
    "to": "+842471000000",
    "sdp": {
      "type": "offer",
      "sdp": "v=0\r\no=- 123456789 2 IN IP4 127.0.0.1..."
    },
    "is_transfer": false
  }
}
```

**Client Send for Outbound Call:**

```json
{
  "event": "call.offer",
  "data": {
    "call_id": "cl_a1b2c3d4e5f6g7h8i9j0k1",
    "sdp": {
      "type": "offer",
      "sdp": "v=0\r\no=- 123456789 2 IN IP4 127.0.0.1..."
    }
  }
}
```

**Server Response:**

```json
{
  "event": "call.offered",
  "data": {
    "call_id": "cl_a1b2c3d4e5f6g7h8i9j0k1"
  }
}
```

---

### 3. ICE Candidates (`call.candidate`)

**Client Send:**

```json
{
  "event": "call.candidate",
  "data": {
    "call_id": "cl_a1b2c3d4e5f6g7h8i9j0k1",
    "candidate": {
      "candidate": "candidate:1 1 UDP 2013266431 192.168.1.1 54321 typ host",
      "sdpMid": "0",
      "sdpMLineIndex": 0
    }
  }
}
```

:::info
**ICE Candidate Signaling Note**  
Firetell Voice Platform uses **Full SDP Exchange (Non-Trickle ICE)** where ICE candidates (`a=candidate:...`) are bundled directly within the initial SDP offer (`call.offer`) and SDP answer (`call.answer`). Sending individual `call.candidate` events via WebSocket is supported for compatibility (acknowledged with `call.candidate_ack`), but bundling candidates directly in the SDP is the recommended and primary mechanism for media establishment.
:::

---

### 4. End Call (`call.hangup` / `call.ended`)

**Client Send (`call.hangup`):**

To hang up the call:

```json
{
  "event": "call.hangup",
  "data": {
    "call_id": "cl_a1b2c3d4e5f6g7h8i9j0k1"
  }
}
```

**Server Dispatch (`call.ended`):**

Notifies the client that the call session has been terminated.

```json
{
  "event": "call.ended",
  "data": {
    "call_id": "cl_a1b2c3d4e5f6g7h8i9j0k1",
    "reason": "hangup"
  }
}
```

---

### 5. Answer Call (`call.answer` / `call.answered`)

**Client Send (`call.answer`):**

Sent by the client to accept an incoming call, containing the client's WebRTC SDP answer.

```json
{
  "event": "call.answer",
  "data": {
    "call_id": "cl_a1b2c3d4e5f6g7h8i9j0k1",
    "sdp": "v=0\r\no=- 123456789 2 IN IP4 127.0.0.1..."
  }
}
```

**Server Dispatch (`call.answered`):**

Notifies the client (e.g. for an outbound call) that the call has been answered.

```json
{
  "event": "call.answered",
  "data": {
    "call_id": "cl_a1b2c3d4e5f6g7h8i9j0k1",
    "sdp": "v=0\r\no=- 123456789 2 IN IP4 127.0.0.1..."
  }
}
```

---

### 6. Reject Call (`call.reject` / `call.rejected`)

**Client Send (`call.reject`):**

Sent by the client to reject an incoming call.

```json
{
  "event": "call.reject",
  "data": {
    "call_id": "cl_a1b2c3d4e5f6g7h8i9j0k1"
  }
}
```

**Server Dispatch (`call.rejected`):**

Notifies the client that the call was rejected.

```json
{
  "event": "call.rejected",
  "data": {
    "call_id": "cl_a1b2c3d4e5f6g7h8i9j0k1",
    "reason": "rejected"
  }
}
```

---

### 7. Hold Call (`call.hold` / `call.held`)

**Client Send (`call.hold`):**

Sent by the client to place the active call on hold.

```json
{
  "event": "call.hold",
  "data": {
    "call_id": "cl_a1b2c3d4e5f6g7h8i9j0k1",
    "sdp": {
      "type": "offer",
      "sdp": "v=0\r\no=- 123456789 2 IN IP4 127.0.0.1..."
    }
  }
}
```

**Server Dispatch (`call.held`):**

Notifies the client that the call was successfully held.

```json
{
  "event": "call.held",
  "data": {
    "call_id": "cl_a1b2c3d4e5f6g7h8i9j0k1",
    "sdp": {
      "type": "answer",
      "sdp": "v=0\r\no=- 123456789 2 IN IP4 127.0.0.1..."
    }
  }
}
```

---

### 8. Resume Call (`call.unhold` / `call.unheld`)

**Client Send (`call.unhold`):**

Sent by the client to take the call off hold.

```json
{
  "event": "call.unhold",
  "data": {
    "call_id": "cl_a1b2c3d4e5f6g7h8i9j0k1",
    "sdp": {
      "type": "offer",
      "sdp": "v=0\r\no=- 123456789 2 IN IP4 127.0.0.1..."
    }
  }
}
```

**Server Dispatch (`call.unheld`):**

Notifies the client that the call was successfully resumed.

```json
{
  "event": "call.unheld",
  "data": {
    "call_id": "cl_a1b2c3d4e5f6g7h8i9j0k1",
    "sdp": {
      "type": "answer",
      "sdp": "v=0\r\no=- 123456789 2 IN IP4 127.0.0.1..."
    }
  }
}
```

---

### 9. Mute Call (`call.mute`)

**Client Send:**

Sent by the client to update the local microphone mute state.

```json
{
  "event": "call.mute",
  "data": {
    "call_id": "cl_a1b2c3d4e5f6g7h8i9j0k1",
    "muted": true
  }
}
```

---

### 10. Send DTMF (`call.dtmf`)

**Client Send:**

Sent by the client to play DTMF tones on the call.

```json
{
  "event": "call.dtmf",
  "data": {
    "call_id": "cl_a1b2c3d4e5f6g7h8i9j0k1",
    "digit": "5",
    "duration": 250
  }
}
```

---

### 11. Transfer Call (`call.transfer` / `call.transferred`)

**Client Send (`call.transfer`):**

Sent by the client to transfer the call to another agent.

```json
{
  "event": "call.transfer",
  "data": {
    "call_id": "cl_a1b2c3d4e5f6g7h8i9j0k1",
    "to": "agent.smith",
    "team_id": "te_123456"
  }
}
```

**Server Dispatch (`call.transferred`):**

Notifies the client that the call transfer is complete.

```json
{
  "event": "call.transferred",
  "data": {
    "call_id": "cl_a1b2c3d4e5f6g7h8i9j0k1",
    "reason": "transferred"
  }
}
```

---

## Client-Initiated Call Flows (client-api)

When integrating calling capabilities into end-user client applications (e.g., driver apps, customer support widgets) using **`client-api`** JWT tokens, calls are routed through a Visual Call Flow builder rather than being connected directly as normal operator/agent calls.

### Workflow & Routing
1. **JWT Verification:** The client-api JWT token **must** contain a valid `call_flow_id` claim in its payload.
2. **Call Initiation:** The client makes a `POST /call-center/calls` request. The API validates that the `call_flow_id` exists in the workspace and has `trigger_type` set to `'outbound'`.
3. **Internal VoIP Route:** Direct PSTN dialing is disabled for the `client-api` audience. The call behaves as an internal WebRTC VoIP call.
4. **Flow Execution:** When the client connects via WebSocket and initiates the call, Firetell automatically routes the call directly into the compiled Call Flow actions (e.g., executing an IVR, connecting the caller to a team, or routing to a specific agent).

---

:::tip
**Auto-Hangup on Disconnect**
Closing or dropping the WebSocket connection automatically ends active calls associated with that `call_id`.
:::
