> ## Documentation Index
> Fetch the complete documentation index at: https://docs.unpod.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Recording

> Get call recordings and download the audio

Unpod records a call as one WAV file, with each person on their own channel. Once the call
has ended, ask the API for the recording, then download the audio with the same
credentials.

> [!NOTE]
>
> Whether a call is recorded follows the recording policy (set by the Unpod team). To
> change it, ask them. `record=True` on a call doesn't override the policy today, and the
> `record` setting in a number's inbound config is stored but not applied yet.

## Get a recording

Export your [credentials](https://docs.unpod.ai/telephony/introduction.md#credentials) (`UNPOD_BASE_URL`,
`UNPOD_PLATFORM_TOKEN`, `UNPOD_ORG_HANDLE`). After the call ends, `get_recording` returns
the recording's details. Until the recording is saved it raises `409` (the call is still
going) or `404` (it has just ended), so wait a few seconds and ask again. A wrong
`call_id` also returns `404`, so it only fails once the retries run out:

```python Python theme={null}
import time

from unpod import Client
from unpod.telephony import TelephonyAPIError

CALL_ID = "..."  # from create, or ev.call_id on any event for the call

with Client() as client:
    for _ in range(20):  # about a minute
        try:
            rec = client.telephony.calls.get_recording(CALL_ID)
            break
        except TelephonyAPIError as e:
            if e.status_code not in (404, 409):  # 409 still going, 404 just ended
                raise
            time.sleep(3)
    else:
        raise SystemExit("Not ready yet. Try again later.")

print(rec.available, rec.duration_secs)
```

```bash cURL theme={null}
curl -s "https://unpod.ai/api/v2/platform/sbc/v1/calls/$CALL_ID/recording" \
  -H "Authorization: Token $UNPOD_PLATFORM_TOKEN" -H "Org-Handle: $UNPOD_ORG_HANDLE"
```

The response:

```json theme={null}
{"call_id": "...", "org_id": "...", "available": true,
 "url": "https://unpod.ai/api/v2/platform/sbc/v1/calls/<call_id>/recording",
 "duration_secs": 64, "format": "wav", "channels": 2}
```

| Field | Meaning |
| - | - |
| `available` | `true` when there's audio. `false` means the call wasn't recorded, and `url` and `duration_secs` are `null` |
| `url` | Where to [download the audio](#download-the-audio). It needs your credentials |
| `duration_secs` | Length in seconds |
| `format`, `channels` | Always `wav` and `2` today |

An unknown `call_id`, or one from another org, returns `404 not_found`. After hangup,
`GET /sbc/v1/calls/{id}` returns the same details, but its `url` is a path inside Unpod:
download from the `url` that `get_recording` returns.

## Download the audio

Request the same URL with the header `Accept: audio/wav`, plus the same `Authorization`
and `Org-Handle` headers as every other API call. Without `Accept: audio/wav` you get the
JSON above. The file is about 1 MB per minute of call.

```python Python theme={null}
import os

import httpx  # installed with the SDK

if not rec.available:  # rec from get_recording above
    raise SystemExit("This call wasn't recorded.")

headers = {
    "Authorization": f"Token {os.environ['UNPOD_PLATFORM_TOKEN']}",
    "Org-Handle": os.environ["UNPOD_ORG_HANDLE"],
    "Accept": "audio/wav",
}
with httpx.stream("GET", rec.url, headers=headers, timeout=60) as resp:
    resp.raise_for_status()
    with open("call.wav", "wb") as f:
        for chunk in resp.iter_bytes():
            f.write(chunk)
```

```bash cURL theme={null}
curl -s "https://unpod.ai/api/v2/platform/sbc/v1/calls/$CALL_ID/recording" \
  -H "Authorization: Token $UNPOD_PLATFORM_TOKEN" -H "Org-Handle: $UNPOD_ORG_HANDLE" \
  -H "Accept: audio/wav" -o call.wav -w "%{http_code}\n"
```

A `200` means `call.wav` holds the audio. A call with no recording returns `404`, and
`call.wav` then holds the error instead. The URL only works with your credentials, so
download the file and serve it from your own storage rather than handing the URL to a
browser.

## Format

| Property | Value |
| - | - |
| File | WAV |
| Audio | G.711 μ-law (A-law when the carrier negotiates it), 8-bit, 8000 Hz |
| Channels | 2, one person on each |

The first leg (A) is on the left channel and the second leg (B) on the right.

To get each person in their own file, split the channels with sox or ffmpeg:

```bash sox theme={null}
sox call.wav first.wav remix 1
sox call.wav second.wav remix 2
```

```bash ffmpeg theme={null}
ffmpeg -i call.wav -filter_complex "channelsplit=channel_layout=stereo[a][b]" \
  -map "[a]" -c:a pcm_mulaw first.wav -map "[b]" -c:a pcm_mulaw second.wav
```

Both keep the μ-law encoding; for an A-law file, use `-c:a pcm_alaw` with ffmpeg (sox keeps
the input encoding by itself). Python's `wave` module can't open G.711 WAV files (it fails
with `unknown format: 7` for μ-law, `6` for A-law), and `audioop` is gone from Python
3.13. If a tool needs 16-bit PCM, convert the file first with sox or ffmpeg:
`sox call.wav -b 16 -e signed-integer call-pcm.wav`.

## Recording events

When a recorded call ends, Unpod sends `MPCRecordingCompleted` to the call's
`recording_callback_url` only, never to `status_callback_url`. Set it on `create`, or in a
number's [inbound config](https://docs.unpod.ai/telephony/apis/incoming-calls.md#inboundconfig-fields). The event
carries `ev.call_id`, `ev.recording_url` and `ev.recording_duration`. Treat it as a hint
that the recording is ready:

* It isn't signed yet, so `parse_call_event` with your secret rejects it. Parse it with
  `verify=False` and trust only what `get_recording` returns for `ev.call_id`.
* It carries no `metadata`, so match it to your own records by `ev.call_id`.
* `ev.recording_url` is a path inside Unpod, not a download link. Call
  `get_recording(ev.call_id)` for the `url`.
* It may never arrive for a call started with `create`. The reliable way is to call
  `get_recording` after the call's `Hangup`, as [above](#get-a-recording).

The `/recording` route in [Number Masking](https://docs.unpod.ai/telephony/apis/number-masking.md#handle-events)
handles the event this way.

With `record_participant_track=True` (which needs `record=True`), the same URL also gets
`ParticipantRecordingInitiated` and `ParticipantRecordingCompleted` for each leg. Both
arrive together after the call ends, and both carry the leg's channel in
`ev.metadata["channel"]`: `1` for the first leg (left), `2` for the second (right). Only
`ParticipantRecordingCompleted` carries the recording, in `ev.metadata["recording_url"]`:
the same 2-channel file, at the same path inside Unpod.

**Answer URL `<Record>`.** When a `<Record>` in your
[answer-URL reply](https://docs.unpod.ai/telephony/apis/answer-url.md) reaches its `maxLength` (300 s by default),
Unpod sends `RecordStop` to your answer URL with `ev.recording_url`. That's the same path
inside Unpod: once the call ends, get the audio with `get_recording(ev.call_id)`.

## Record part of a call

`calls.record` asks Unpod to start recording a call that's still going:

```python Python theme={null}
from unpod import Client

CALL_ID = "..."  # a call that's still going

with Client() as client:
    client.telephony.calls.record(CALL_ID)
```

```bash cURL theme={null}
curl -s -X POST "https://unpod.ai/api/v2/platform/sbc/v1/calls/$CALL_ID/record" \
  -H "Authorization: Token $UNPOD_PLATFORM_TOKEN" -H "Org-Handle: $UNPOD_ORG_HANDLE" \
  -H "Content-Type: application/json" -d '{}'
```

It returns as soon as the request reaches the call, before recording starts, so it doesn't
report whether recording started. A call that has ended returns `404 not_found`. A call
the recording policy already records keeps that one recording. Whether `get_recording`
returns a recording started this way isn't confirmed yet. `transcribe=True` is accepted,
but no transcript is made yet.

## Tell callers

> [!WARNING]
>
> Many places require you to tell people, and sometimes to get their consent, before you
> record a call. Check the rules where you and the people you call are.

Say it at the start of the call. On an inbound call, put a `<Speak>` first in your
answer-URL reply:

```xml theme={null}
<Response>
    <Speak>This call is recorded.</Speak>
    <Dial><Number>+919000000003</Number></Dial>
</Response>
```

On a call you start with `create`,
[`speak`](https://docs.unpod.ai/telephony/apis/call-control#play-audio-and-speak) the same line when the leg
sends `Answered`.

If the recording policy records the call, you don't need `<Record/>`. Never put `<Record/>`
before `<Dial>`: Unpod waits for the recording to reach its `maxLength` (300 s by default)
before dialing, and the caller hears silence.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.