<!-- https://missblue.dev/docs/calls -->

# Calls

Read FaceTime Audio and regular phone call history, control FaceTime sessions, and retrieve recordings. Calls are separate from message transcripts. A FaceTime dial request alone does not carry audio: connect a browser or custom WebRTC client too. Regular phone dialing is available to signed-in console users, not API keys; use /docs/calling for the product workflow.

Base URL: `https://api.missblue.dev`

Every request carries `Authorization: Bearer $MISS_BLUE_KEY`.

### GET /v1/calls

Every call on the lines you hold, newest first.

Takes an optional project_id to narrow to one project's numbers. Returns your workspace's calls; calls made by another workspace on a shared number remain separate. transport is direct or livekit for FaceTime Audio, telnyx for regular phone calls, or null before the path is known. direction is inbound or outbound. outcome is dialing, answered, no_answer or failed. A FaceTime call that never became active is no_answer; this does not distinguish a decline from a ring-out. History and recording completion are separate.

| Parameter | Type | Required | Notes |
| --- | --- | --- | --- |
| `project_id` | `uuid` | no | Optionally narrow the history to one project's numbers. |

Example:

```bash
curl -X GET https://api.missblue.dev/v1/calls \
  -H "Authorization: Bearer $MISS_BLUE_KEY"
```

Response:

```json
{
  "data": [
    {
      "id": "cf33aa62-1c89-40b0-9887-137108be4b48",
      "number_handle": "+19292727048",
      "handle": "+15555550100",
      "started_by": "Ada Lovelace",
      "transport": "direct",
      "direction": "outbound",
      "outcome": "answered",
      "started_at": "2026-09-06T17:19:37Z",
      "answered_at": "2026-09-06T17:19:38Z",
      "ended_at": "2026-09-06T17:20:18Z",
      "ring_seconds": 1,
      "talk_seconds": 40,
      "recording_status": "ready",
      "recording_bytes": 669427
    }
  ],
  "total": 1
}
```

### GET /v1/numbers/{number_id}/calls

The same history, for one line.

Example:

```bash
curl -X GET https://api.missblue.dev/v1/numbers/5d2c1f1e-6f4a-4d43-9d8e-1a2b3c4d5e6f/calls \
  -H "Authorization: Bearer $MISS_BLUE_KEY"
```

### POST /v1/numbers/{number_id}/rtc/calls

Ask the Mac to dial a FaceTime Audio call.

The handle must be an E.164 number or Apple ID email. This rings and tracks the call but does not connect your microphone or speaker. Establish the audio leg with the RTC signaling routes, then use the recording upload routes if the number has recording enabled. Poll or delete the session to follow or end it.

| Parameter | Type | Required | Notes |
| --- | --- | --- | --- |
| `handle` | `string` | yes | An E.164 phone number or Apple ID email reachable through FaceTime. |

Request body:

```json
{
  "handle": "+15555550100"
}
```

Example:

```bash
curl -X POST https://api.missblue.dev/v1/numbers/5d2c1f1e-6f4a-4d43-9d8e-1a2b3c4d5e6f/rtc/calls \
  -H "Authorization: Bearer $MISS_BLUE_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "handle": "+15555550100"
  }'
```

Response:

```json
{ "session_id": "271b92eb-0a59-4491-bec3-314c7db48400", "state": "dialing" }
```

Errors:

- `404` — The number is not held by this caller.
- `503` — The Mac could not place the call.

### GET /v1/numbers/{id}/rtc/config

Get the ICE configuration for FaceTime browser audio.

Pass ice_servers to your WebRTC client. TURN and LiveKit fallback availability depend on this deployment. Keep returned credentials in memory for the session; do not publish or log them.

Example:

```bash
curl -X GET https://api.missblue.dev/v1/numbers/YOUR_ID/rtc/config \
  -H "Authorization: Bearer $MISS_BLUE_KEY"
```

Response:

```json
{ "ice_servers": [{ "urls": ["stun:stun.example.com:3478"] }], "turn_available": false, "livekit_fallback_available": false }
```

### POST /v1/numbers/{id}/rtc/offer

Negotiate the audio connection to the number's Mac.

Send a real SDP offer produced by your WebRTC client after gathering ICE candidates. The response is the remote answer to set on that connection. The abbreviated SDP below illustrates the shape, not a usable offer. Signaling does not itself dial a recipient.

| Parameter | Type | Required | Notes |
| --- | --- | --- | --- |
| `type` | `string` | yes | Must be offer. |
| `sdp` | `string` | yes | The complete local SDP with gathered ICE candidates. |

Request body:

```json
{
  "type": "offer",
  "sdp": "v=0\r\n..."
}
```

Example:

```bash
curl -X POST https://api.missblue.dev/v1/numbers/YOUR_ID/rtc/offer \
  -H "Authorization: Bearer $MISS_BLUE_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "offer",
    "sdp": "v=0\r\n..."
  }'
```

Response:

```json
{ "type": "answer", "sdp": "v=0\r\n..." }
```

Errors:

- `503` — The Mac or its audio bridge is unavailable.

### POST /v1/numbers/{id}/rtc/livekit

Start the configured LiveKit audio fallback.

Use when the direct connection cannot be established and config reports livekit_fallback_available. The response contains temporary room access for this session, not a reusable account token.

Example:

```bash
curl -X POST https://api.missblue.dev/v1/numbers/YOUR_ID/rtc/livekit \
  -H "Authorization: Bearer $MISS_BLUE_KEY"
```

Response:

```json
{ "url": "wss://relay.example.com", "token": "<temporary-room-token>" }
```

Errors:

- `503` — LiveKit fallback is not configured or the Mac cannot join.

### GET /v1/numbers/{id}/rtc/calls/{session_id}

Read the FaceTime session state.

state is dialing, active, or ended. Use call history for the durable outcome and durations.

Example:

```bash
curl -X GET https://api.missblue.dev/v1/numbers/YOUR_ID/rtc/calls/YOUR_SESSION_ID \
  -H "Authorization: Bearer $MISS_BLUE_KEY"
```

Response:

```json
{ "session_id": "271b92eb-0a59-4491-bec3-314c7db48400", "state": "active" }
```

### DELETE /v1/numbers/{id}/rtc/calls/{session_id}

End a FaceTime Audio call.

Example:

```bash
curl -X DELETE https://api.missblue.dev/v1/numbers/YOUR_ID/rtc/calls/YOUR_SESSION_ID \
  -H "Authorization: Bearer $MISS_BLUE_KEY"
```

Response:

```json
{ "session_id": "271b92eb-0a59-4491-bec3-314c7db48400", "state": "ended" }
```

### POST /v1/numbers/{id}/rtc/calls/{session_id}/transport

Report which audio path carried the FaceTime call.

Accepts direct or livekit. Report the path that actually connected so call history can identify it.

| Parameter | Type | Required | Notes |
| --- | --- | --- | --- |
| `transport` | `string` | yes | direct or livekit, according to the connected media path. |

Request body:

```json
{
  "transport": "direct"
}
```

Example:

```bash
curl -X POST https://api.missblue.dev/v1/numbers/YOUR_ID/rtc/calls/YOUR_SESSION_ID/transport \
  -H "Authorization: Bearer $MISS_BLUE_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "transport": "direct"
  }'
```

Errors:

- `400` — transport is neither direct nor livekit.

### PUT /v1/numbers/{number_id}/call-recording

Turn call recording on or off for one line.

acknowledged is required to enable and refused without it. Recording law is decided by where the people on the call are — some places need every party's consent — and Miss Blue provides the feature without deciding whether a given call may lawfully be recorded. Who switched it on, and when, is stored with the flag. Turning it off asks nothing, and does not delete what was already captured.

| Parameter | Type | Required | Notes |
| --- | --- | --- | --- |
| `enabled` | `boolean` | yes | Whether this number should record future calls. |
| `acknowledged` | `boolean` | no | Must be true when enabling recording; optional when disabling. |

Request body:

```json
{
  "enabled": true,
  "acknowledged": true
}
```

Example:

```bash
curl -X PUT https://api.missblue.dev/v1/numbers/5d2c1f1e-6f4a-4d43-9d8e-1a2b3c4d5e6f/call-recording \
  -H "Authorization: Bearer $MISS_BLUE_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "enabled": true,
    "acknowledged": true
  }'
```

Response:

```json
{
  "enabled": true,
  "enabled_at": "2026-09-06T16:54:38Z",
  "enabled_by": "Ada Lovelace"
}
```

Errors:

- `400` — Enabling without acknowledging responsibility for consent law.

### POST /v1/numbers/{id}/rtc/calls/{session_id}/recording

Begin uploading a FaceTime call recording.

Recording must be enabled for the number and the call must belong to that line. Capture both the local microphone and remote audio in one WebM stream before sending parts. This endpoint does not record audio by itself.

Example:

```bash
curl -X POST https://api.missblue.dev/v1/numbers/YOUR_ID/rtc/calls/YOUR_SESSION_ID/recording \
  -H "Authorization: Bearer $MISS_BLUE_KEY"
```

Response:

```json
{ "recording_id": "f4075a5e-b819-4bcf-90c5-e0c43bc4c611", "call_id": "271b92eb-0a59-4491-bec3-314c7db48400", "status": "recording", "mime_type": "audio/webm", "byte_size": null, "parts": 0, "expires_at": null }
```

Errors:

- `409` — Call recording is not enabled for this number.

### PUT /v1/numbers/{id}/recordings/{recording_id}/parts/{part}

Upload one WebM recording part.

Part numbers start at 1 and may be at most 3000. Each part must be nonempty and at most 1 MiB. Upload consecutive parts from the same WebM stream in order; the first must contain the WebM header. Retry a part with the same bytes and part number before completion.

Headers beyond `Authorization` and `Content-Type`:

- `Content-Type`: audio/webm

Request body: the raw bytes of the file (`part-1.webm`), not JSON.

Example:

```bash
curl -X PUT https://api.missblue.dev/v1/numbers/YOUR_ID/recordings/f4075a5e-b819-4bcf-90c5-e0c43bc4c611/parts/1 \
  -H "Authorization: Bearer $MISS_BLUE_KEY" \
  -H "Content-Type: audio/webm" \
  --data-binary @part-1.webm
```

Errors:

- `400` — Part number or byte size is outside the accepted bounds.
- `409` — Recording is disabled or the recording is already finished.

### POST /v1/numbers/{id}/recordings/{recording_id}/complete

Finish and publish the uploaded FaceTime recording.

Call after the final part has uploaded. Parts are joined in order and call.recording.ready is emitted once the audio is durable. A missing part can leave only the playable prefix, so confirm every upload before completing. Repeating completion for a ready recording returns that recording. expires_at is null when no retention deadline was configured.

Example:

```bash
curl -X POST https://api.missblue.dev/v1/numbers/YOUR_ID/recordings/YOUR_RECORDING_ID/complete \
  -H "Authorization: Bearer $MISS_BLUE_KEY"
```

Response:

```json
{ "recording_id": "f4075a5e-b819-4bcf-90c5-e0c43bc4c611", "call_id": "271b92eb-0a59-4491-bec3-314c7db48400", "status": "ready", "mime_type": "audio/webm", "byte_size": 669427, "parts": 8, "expires_at": null }
```

Errors:

- `400` — No audio was captured.

### GET /v1/numbers/{number_id}/calls/{call_id}/recording

A short-lived signed URL for the audio.

Fetch it when somebody presses play rather than with the page: it is a link to a recorded conversation. Check recording_status on the call first — an answered call has audio only if the line was recording at the time.

Example:

```bash
curl -X GET https://api.missblue.dev/v1/numbers/5d2c1f1e-6f4a-4d43-9d8e-1a2b3c4d5e6f/calls/cf33aa62-1c89-40b0-9887-137108be4b48/recording \
  -H "Authorization: Bearer $MISS_BLUE_KEY"
```

Response:

```json
{ "url": "https://…" }
```

Errors:

- `404` — No recording, or it belongs to another organization.
- `409` — The recording failed, was deleted, or is not finished.
