# Media

Short-lived links to message attachments, call recordings and fax documents.

## Get a media link

`GET /v1/media/{id}`

Returns a short-lived link to one file:

- `att_<n>`: a message attachment, from `GET /v1/messages` or a message webhook. A bare number is read as an attachment too.
- `rec_<n>`: the recording of one call leg, from `recording_url` or `recordings` in `GET /v1/calls`.
- `fax_<n>`: a fax document, from `GET /v1/faxes`.

`url` is a signed link, good for 30 minutes (`expires_at`). Download the file with a plain `GET`: the signed link needs no key. Ask again for a fresh link.

A call parked and picked up again is recorded in parts. `rec_<n>` answers the latest part; `rec_<n>?part=N` answers part N (from 1). The answer names the part it holds (`part` of `parts`) with its `recording_id` and `start_time`. A part that does not exist answers `404`, and the message says how many parts there are.

Two exceptions return a plain link with no `expires_at`: an attachment on a message you sent (you get back the URL you sent it with), and a picture received in the last few minutes, before its stored copy is ready.

Anything that is not yours answers `404`. A key with the `messaging` scope may fetch attachments only.

Each request is written to your account's HIPAA access log.

### Request

**curl**

```bash
curl https://api.vocatech.com/v1/media/rec_48213907 \
  -H "Authorization: Bearer YOUR_API_KEY"
```

**Node**

```javascript
const res = await fetch("https://api.vocatech.com/v1/media/rec_48213907", {
  headers: { Authorization: "Bearer YOUR_API_KEY" },
});

console.log(res.status, await res.json());
```

**Python**

```python
import requests

r = requests.get(
    "https://api.vocatech.com/v1/media/rec_48213907",
    headers={"Authorization": "Bearer YOUR_API_KEY"},
    timeout=30,
)
print(r.status_code, r.json())
```

**PHP**

```php
<?php
require 'vendor/autoload.php';

$client = new GuzzleHttp\Client([
    'base_uri' => 'https://api.vocatech.com',
    'headers' => ['Authorization' => 'Bearer YOUR_API_KEY'],
    'http_errors' => false,
]);

$res = $client->get('/v1/media/rec_48213907');

echo $res->getStatusCode(), PHP_EOL, $res->getBody(), PHP_EOL;
```

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes | `att_<n>`, `rec_<n>` or `fax_<n>`. Matches `^(att_\|rec_\|fax_)?[0-9]+$`. |
| `part` | query | integer |  | For a recording (`rec_<n>`) only. Which part of a recording saved in parts, from 1, oldest first. Leave it out for the latest part. `recordings` in `GET /v1/calls` gives each part's link. At least 1. For example `1`. |

### Answers

#### 200

A link to the file.

**When `type` is `attachment`**

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes | Matches `^att_[0-9]+$`. |
| `type` | string | yes | Always `attachment`. |
| `source` | string | yes | Where the message came from. One of `sms` or `whatsapp`. |
| `content_type` | string or null | yes |  |
| `filename` | string or null | yes |  |
| `size` | integer or null | yes | Bytes, when known. |
| `url` | string (uri) | yes | A signed link, good for 30 minutes. For an attachment on a message you sent, the URL you sent it with. |
| `expires_at` | string (date-time) |  | When the signed link stops working. Left out when `url` is not a signed link. |

**When `type` is `recording`**

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes | Matches `^rec_[0-9]+$`. |
| `type` | string | yes | Always `recording`. |
| `content_type` | string | yes | `audio/` and the file's extension, for example `audio/mp3` or `audio/wav`. |
| `format` | string | yes | The file's extension, for example `mp3`. |
| `duration` | integer or null | yes | Seconds. |
| `url` | string (uri) | yes | A signed link, good for 30 minutes. |
| `expires_at` | string (date-time) | yes |  |
| `part` | integer | yes | Which part this is, from 1. Without `?part=`, the latest part. |
| `parts` | integer | yes | How many parts the recording has. Nearly always 1. |
| `recording_id` | integer | yes | The recording's id, as in `recordings` and the `call.transcription` event. |
| `start_time` | string (date-time) or null | yes | When this part started. UTC, ISO 8601 with milliseconds. |

**When `type` is `fax`**

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes | Matches `^fax_[0-9]+$`. |
| `type` | string | yes | Always `fax`. |
| `content_type` | string | yes | Always `application/pdf`. |
| `format` | string | yes | Always `pdf`. |
| `pages` | integer or null | yes |  |
| `url` | string (uri) | yes | A signed link, good for 30 minutes. |
| `expires_at` | string (date-time) | yes |  |

**Message attachment**

```json
{
  "id": "att_90211",
  "type": "attachment",
  "source": "sms",
  "content_type": "image/jpeg",
  "filename": "photo-0921.jpg",
  "size": 184233,
  "url": "https://files.example.com/signed/att_90211.jpg?sig=7c2e9a41",
  "expires_at": "2026-09-24T14:35:09.000Z"
}
```

**Call recording (the latest part)**

```json
{
  "id": "rec_48213907",
  "type": "recording",
  "content_type": "audio/mp3",
  "format": "mp3",
  "duration": 309,
  "url": "https://files.example.com/signed/rec_48213907.mp3?sig=b71f0c9e",
  "expires_at": "2026-09-24T14:35:09.000Z",
  "part": 2,
  "parts": 2,
  "recording_id": 10577702,
  "start_time": "2026-09-21T14:04:31.000Z"
}
```

**Fax document**

```json
{
  "id": "fax_77120",
  "type": "fax",
  "content_type": "application/pdf",
  "format": "pdf",
  "pages": 3,
  "url": "https://files.example.com/signed/fax_77120.pdf?sig=3d5a8c20",
  "expires_at": "2026-09-24T14:35:09.000Z"
}
```

#### 400

The id is not in a form we know.

**Example**

```json
{
  "error": {
    "code": 400,
    "type": "BAD_REQUEST",
    "message": "Invalid recording segment ID."
  }
}
```

**Example**

```json
{
  "error": {
    "code": 400,
    "type": "BAD_REQUEST",
    "message": "Invalid attachment ID."
  }
}
```

**Example**

```json
{
  "error": {
    "code": 400,
    "type": "BAD_REQUEST",
    "message": "Invalid fax ID."
  }
}
```

**Example**

```json
{
  "error": {
    "code": 400,
    "type": "BAD_REQUEST",
    "message": "Invalid recording part. Use a whole number from 1."
  }
}
```

#### 401

No key, or a key that is wrong or turned off.

#### 403

A `messaging` key asked for a recording or a fax, the account has no call recording set up, or the key's scopes or address.

**Messaging key, recording id**

```json
{
  "error": {
    "code": 403,
    "type": "FORBIDDEN",
    "message": "This API key is limited to messaging and cannot fetch recording media."
  }
}
```

**No call recording**

```json
{
  "error": {
    "code": 403,
    "type": "FORBIDDEN",
    "message": "Company not configured for call recordings."
  }
}
```

#### 404

Not found, not yours, or not ready yet.

**Example**

```json
{
  "error": {
    "code": 404,
    "type": "NOT_FOUND",
    "message": "Attachment not found."
  }
}
```

**Example**

```json
{
  "error": {
    "code": 404,
    "type": "NOT_FOUND",
    "message": "Recording not found."
  }
}
```

**Example**

```json
{
  "error": {
    "code": 404,
    "type": "NOT_FOUND",
    "message": "Recording file not yet available."
  }
}
```

**Example**

```json
{
  "error": {
    "code": 404,
    "type": "NOT_FOUND",
    "message": "Recording part not found. This recording has 2 parts."
  }
}
```

**Example**

```json
{
  "error": {
    "code": 404,
    "type": "NOT_FOUND",
    "message": "Fax document not found."
  }
}
```

**Example**

```json
{
  "error": {
    "code": 404,
    "type": "NOT_FOUND",
    "message": "Fax document not yet available."
  }
}
```

#### 410

The attachment's file is no longer available.

**Example**

```json
{
  "error": {
    "code": 410,
    "type": "ERROR",
    "message": "Media file no longer available."
  }
}
```

#### 429

A rate limit was reached. Wait `Retry-After` seconds. The body names the counter, the window and the limit.

#### 500

Fax documents are not set up on our side.

**Example**

```json
{
  "error": {
    "code": 500,
    "type": "SERVER_ERROR",
    "message": "Fax service not configured."
  }
}
```

#### 503

Our storage refused us. The file exists. Contact support if it lasts. Or the key could not be checked.

**Example**

```json
{
  "error": {
    "code": 503,
    "type": "ERROR",
    "message": "Recording storage access failed on our side. The recording exists. Contact support."
  }
}
```
