# Retrieving recordings and transcripts

**Availability: Available**

After a call settles you can read its transcript and get a link to its recording. This guide covers when the artifacts are available, the shape of a transcript and how to download recordings through short-lived links.

## Before you begin

- `transcripts:read`: [`GET /calls/{call_id}/transcript`](/api/transcripts#retrieve-a-call-transcript)
- `recordings:read`: [`GET /calls/{call_id}/recording`](/api/recordings#retrieve-a-call-recording-link)
- `calls:read`: Reading the call itself to see `artifacts`

Artifact scopes are separate from `calls:read` on purpose: a key that only needs call outcomes does not have to see what was said. The requests below use the documented example call id; use the id of your call. Any test call to `+12025550100` produces both artifacts.

## When artifacts are ready

Artifacts are produced after the call ends. Each call reports their state in `artifacts`:

```json
"artifacts": {
  "transcript": { "state": "ready" },
  "recording": { "state": "ready" },
  "analysis": { "state": "ready" }
}
```

| State | Meaning | What to do |
|---|---|---|
| `pending` | Not ready yet | Wait for `call.settled`, or read the call again later |
| `ready` | Available | Read it from its endpoint |
| `absent` | There is none for this call | Nothing to fetch (for example a call nobody answered) |
| `failed` | It could not be produced | Nothing to fetch; contact support with the call id if it keeps happening |

The simplest rule: **fetch artifacts after `call.settled`** (or once `settled` is `true` when polling). In live mode they are usually ready within a minute of the call ending, sometimes several minutes later. In test mode they are ready about 30 seconds after the call is created. Transcripts are not available while a call is in progress (`capabilities.live_transcript` is `false`).

Which test-mode calls have which artifacts:

| `to` | Transcript | Recording |
|---|---|---|
| `+12025550100`, `+12025550105`, `+12025550106` | `ready` | `ready` |
| `+12025550104` (answered, nobody spoke) | `absent` | `ready` |
| `+12025550101`, `+12025550102`, `+12025550103` (not connected) | `absent` | `absent` |

## Transcript

[`GET /calls/{call_id}/transcript`](/api/transcripts#retrieve-a-call-transcript) returns the conversation turn by turn, verbatim. Nothing is masked: it is what was said.

```bash
curl -X GET https://voice-api.relationsai.com/v1/calls/call_6Vb3Nm8Qw2Er5Ty9Ui4Op7/transcript \
     -H "Authorization: Bearer <token>"
```
```js
const response = await fetch("https://voice-api.relationsai.com/v1/calls/call_6Vb3Nm8Qw2Er5Ty9Ui4Op7/transcript", {
  method: "GET",
  headers: {
    "Authorization": "Bearer <token>",
  },
});
const transcript = await response.json();
```
```python

response = requests.get(
    "https://voice-api.relationsai.com/v1/calls/call_6Vb3Nm8Qw2Er5Ty9Ui4Op7/transcript",
    headers={
        "Authorization": "Bearer <token>",
    },
    timeout=30,
)
transcript = response.json()
```

Complete runnable example: [cURL script](/examples/guides/retrieving-recordings-and-transcripts/get-transcript.sh) · [JavaScript](/examples/guides/retrieving-recordings-and-transcripts/get-transcript.mjs) · [Python](/examples/guides/retrieving-recordings-and-transcripts/get-transcript.py)

The response (`200`):

```json
{
  "object": "call_transcript",
  "livemode": true,
  "call_id": "call_2Hd8Ks4Lq7Mw3Nx9Pz5Rc1",
  "state": "ready",
  "simulated": false,
  "turns": [
    { "index": 0, "speaker": "agent", "text": "Hello, this is an AI assistant calling from Acme Lending. Am I speaking with Asha?", "start_ms": 400, "end_ms": 4900 },
    { "index": 1, "speaker": "caller", "text": "Yes, speaking.", "start_ms": 5600, "end_ms": 6500 }
  ]
}
```

- `speaker` is `agent` or `caller` (the person called). Open enum.
- `index` orders the turns from 0. `start_ms` and `end_ms` are milliseconds from the start of the call, or `null` when not known.
- `turns` is empty unless `state` is `ready`. A transcript that is still `pending` is answered with `200` and `state: "pending"`, not an error.
- `simulated` is `true` for test-mode fixture transcripts. Do not import them into production systems.

## Recording

**Availability: Preview** The recording endpoints are in preview and may change.

[`GET /calls/{call_id}/recording`](/api/recordings#retrieve-a-call-recording-link) returns the recording's state and, when it is ready, a short-lived HTTPS link to the audio.

First read the recording to get a fresh link:

```bash
curl -X GET https://voice-api.relationsai.com/v1/calls/call_6Vb3Nm8Qw2Er5Ty9Ui4Op7/recording \
     -H "Authorization: Bearer <token>"
```
```js
const response = await fetch("https://voice-api.relationsai.com/v1/calls/call_6Vb3Nm8Qw2Er5Ty9Ui4Op7/recording", {
  method: "GET",
  headers: {
    "Authorization": "Bearer <token>",
  },
});
const recording = await response.json();
```
```python

response = requests.get(
    "https://voice-api.relationsai.com/v1/calls/call_6Vb3Nm8Qw2Er5Ty9Ui4Op7/recording",
    headers={
        "Authorization": "Bearer <token>",
    },
    timeout=30,
)
recording = response.json()
```

Then, while `state` is `ready` and before `expires_at`, download the audio from its `url`. The link needs no API key (see the response below for its shape):

```bash
curl https://voice-api.relationsai.com/v1/recordings/rec_tok_Y2FsbF82VmIzTm04UXcyRXI1VHk5VWk0T3A3 \
     -o call-recording.audio
```
```js

const response = await fetch("https://voice-api.relationsai.com/v1/recordings/rec_tok_Y2FsbF82VmIzTm04UXcyRXI1VHk5VWk0T3A3");
await writeFile("call-recording.audio", Buffer.from(await response.arrayBuffer()));
```
```python

response = requests.get("https://voice-api.relationsai.com/v1/recordings/rec_tok_Y2FsbF82VmIzTm04UXcyRXI1VHk5VWk0T3A3", timeout=60)
open("call-recording.audio", "wb").write(response.content)
```

Complete runnable example: [cURL script](/examples/guides/retrieving-recordings-and-transcripts/download-recording.sh) · [JavaScript](/examples/guides/retrieving-recordings-and-transcripts/download-recording.mjs) · [Python](/examples/guides/retrieving-recordings-and-transcripts/download-recording.py)

The response (`200`):

```json
{
  "object": "call_recording",
  "livemode": true,
  "call_id": "call_2Hd8Ks4Lq7Mw3Nx9Pz5Rc1",
  "state": "ready",
  "simulated": false,
  "url": "https://voice-api.relationsai.com/v1/recordings/rec_tok_Y2FsbF8ySGQ4S3M0THE3TXczTng5UHo1UmMx",
  "expires_at": "2026-10-21T10:00:00.000Z",
  "content_type": null
}
```

How the link works:

- `url` is valid for about 15 minutes, until `expires_at`. After that it answers [`media_link_expired`](/errors/media_link_expired) (410); call the recording endpoint again for a fresh link.
- The link **needs no API key**: the token in the path is the credential. It is tied to the key that obtained it: it stops working (`404`) as soon as that key is revoked or expires, loses `recordings:read`, or its creator leaves the workspace, even before `expires_at`.
- Until then it works for anyone who has it. Treat it like a password. Do not log it, store it, put it in a URL you share, or send it to a browser you do not control. Download the audio on your server and serve it from your own storage if people need to play it.
- `content_type` is `audio/wav` for test-mode audio and is usually `null` in live mode, where the format is known only when you download it: the download's `Content-Type` header is authoritative.
- The download supports `Range` requests, so a player can seek. A range that cannot be served is answered with the whole file (`200`).
- In test mode the audio is a short synthetic file and `simulated` is `true`.

If you need to keep recordings, copy them into your own storage promptly and apply your own retention rules.

## Handling call content

Transcripts and recordings are personal data of the person you called: their voice and what they said. RelationsAI returns them to your workspace in full. Store and share them according to your own data-protection obligations and your agreements with the people you call: restrict who can read them, keep them only as long as you need, and do not copy them into logs, analytics tools or tickets. See [Security and data handling](/resources/security-and-data-handling).

## What can go wrong

- **`state: "pending"`**: The call has not settled yet. Fix: Wait for `call.settled` and read again.
- **`state: "absent"`**: The call never connected, or nobody spoke (transcript). Fix: Nothing to fetch; check `outcome` on the call.
- **[`media_link_expired`](/errors/media_link_expired) (410) on download**: The link passed `expires_at`. Fix: Get a fresh link and download straight away.
- **[`insufficient_scope`](/errors/insufficient_scope) (403)**: The key lacks `transcripts:read` or `recordings:read`. Fix: Ask an admin for a key with that scope.
- **[`resource_not_found`](/errors/resource_not_found) (404)**: The call id is from the other mode or another workspace. Fix: Use the key that created the call.

## Next steps

[Working with analysis results](/guides/working-with-analysis-results).
