# Old paths

Older paths that still answer. They behave exactly like the routes they point to. Use the new paths in new code.

## List calls (old path)

`GET /v1/reports/calls` (old path)

The same as `GET /v1/calls`: the same parameters, the same answer, the same `reports` counter. Kept for older integrations. Use `GET /v1/calls`.

### Request

**curl**

```bash
curl -G https://api.vocatech.com/v1/reports/calls \
  -H "Authorization: Bearer YOUR_API_KEY" \
  --data-urlencode "start_date=2026-09-21" \
  --data-urlencode "end_date=2026-09-22" \
  --data-urlencode "timezone=America/New_York"
```

**Node**

```javascript
const params = new URLSearchParams({
  start_date: "2026-09-21",
  end_date: "2026-09-22",
  timezone: "America/New_York",
});

const res = await fetch(`https://api.vocatech.com/v1/reports/calls?${params}`, {
  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/reports/calls",
    headers={"Authorization": "Bearer YOUR_API_KEY"},
    params={
        "start_date": "2026-09-21",
        "end_date": "2026-09-22",
        "timezone": "America/New_York",
    },
    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/reports/calls', [
    'query' => [
        'start_date' => '2026-09-21',
        'end_date' => '2026-09-22',
        'timezone' => 'America/New_York',
    ],
]);

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

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `start_date` | query | string (date) |  | First local day of the window, `YYYY-MM-DD`, in `timezone`. Send it with `end_date`. Send neither for today. A single date is ignored and you get today. For example `2026-09-01`. |
| `end_date` | query | string (date) |  | Last local day of the window, `YYYY-MM-DD`, included in full. At most 366 days after `start_date`. For example `2026-09-07`. |
| `timezone` | query | string |  | IANA time zone for the dates, for example `America/Chicago`. An unknown zone falls back to `America/New_York`. The response echoes the zone used. Default `America/New_York`. |
| `direction` | query | string |  | `incoming` or `outgoing`, as each call's `direction` reads. `voicemail` matches the rare calls stored that way. One of `any`, `incoming`, `outgoing` or `voicemail`. Default `any`. |
| `status` | query | string |  | Only calls with a leg in this status, for example `answered` or `missed`. `missed` never returns an outgoing call. Default `any`. For example `missed`. |
| `extension` | query | string |  | Only calls with a leg on this extension (exact match). For example `104`. |
| `type` | query | string |  | Only calls with a leg of this type, for example `user`, `auto_attendant`, `hunt_group` or `call_center`. Default `any`. |
| `search` | query | string |  | Text to find in the call id, the other party's name or number, your number, or an extension. |
| `sort` | query | string |  | Order by start time or by duration. Calls that tie keep a stable order, so paging is safe. One of `day_desc`, `day_asc`, `duration_desc` or `duration_asc`. Default `day_desc`. |
| `page` | query | integer |  | Page number, from 1. Default `1`. At least 1. |
| `limit` | query | integer |  | Rows per page, 1 to 500. A value outside that range is brought inside it. Default `500`. From 1 to 500. |
| `company_id` | query | integer |  | Optional. If you send it, it must be your own company id, or the request is refused with `403`. The key already decides the company. |

### Answers

#### 200

The calls in the window.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `query` | object | yes | The filters that were applied, echoed back. `extension` and `type` are applied but not echoed. |
| `query.company_id` | integer |  |  |
| `query.start_date` | string (date) |  | The first local day used. |
| `query.end_date` | string (date) |  | The last local day used. |
| `query.direction` | string |  |  |
| `query.status` | string |  |  |
| `query.search` | string |  |  |
| `query.timezone` | string |  | The time zone used. |
| `calls` | array of objects | yes |  |
| `calls[].call_id` | string | yes | The call's id. |
| `calls[].direction` | string | yes | `incoming` or `outgoing`. A call your staff placed through the dial-out portal reads `outgoing`. A few calls read `voicemail`. |
| `calls[].status` | string | yes | The status of the first leg in `journey`, for example `answered` or `missed`. It is not the call's final outcome: a call a menu answered and a person then missed reads `answered`. Read the legs to see who answered. |
| `calls[].extension` | string or null | yes | The last person who held the call. When no person did, the first extension on it. |
| `calls[].extension_name` | string or null | yes | That extension's name, written First Last. |
| `calls[].remote_name` | string | yes | The other party's name from caller ID, written First Last. Can be empty. |
| `calls[].remote_number` | string | yes | The other party's number, digits without the leading 1. Can be empty. |
| `calls[].group_number` | string | yes | Your number on the call, digits without the leading 1. Can be empty. |
| `calls[].start_time` | string (date-time) or null | yes | UTC, ISO 8601 with milliseconds. |
| `calls[].end_time` | string (date-time) or null | yes | UTC, ISO 8601 with milliseconds. |
| `calls[].duration` | integer | yes | The call's length in seconds. |
| `calls[].journey` | array of objects | yes | Every leg, in order. When you filter by `status`, `extension` or `type`, only the matching legs are listed. |
| `calls[].journey[].order` | integer | yes | Position in the journey, from 1. |
| `calls[].journey[].type` | string | yes | What handled this leg, as the phone system wrote it. For example `user` (a person), `auto_attendant`, `hunt_group` or `call_center`. |
| `calls[].journey[].extension_name` | string or null | yes | The name of the person or feature on this leg, as set in the portal, written First Last. |
| `calls[].journey[].extension` | string or null | yes | The extension on this leg. |
| `calls[].journey[].start_time` | string (date-time) or null | yes | UTC, ISO 8601 with milliseconds. |
| `calls[].journey[].end_time` | string (date-time) or null | yes | UTC, ISO 8601 with milliseconds. |
| `calls[].journey[].duration` | integer | yes | Seconds on this leg. |
| `calls[].journey[].summary` | string or null | yes | The AI summary of this leg's recording (its latest part). Null when there is none, and when the leg's only recording belongs to another call leg. |
| `calls[].journey[].transcription` | string or null | yes | The transcript of this leg's recording (its latest part), with speaker labels. Null when there is none, and when the leg's only recording belongs to another call leg. |
| `calls[].journey[].recording_url` | string (uri) or null | yes | Where to fetch this leg's recording (its latest part). Call it with your key (`GET /v1/media/rec_<n>`) to get a signed link. Null when the leg was not recorded. |
| `calls[].journey[].recordings` | array of objects | yes | Every recording of this leg, oldest first. A call parked and picked up again has two parts (rarely three). Nearly every recorded leg has one. Empty when the leg was not recorded. |
| `calls[].journey[].recordings[].part` | integer | yes | This part's place, from 1, oldest first. |
| `calls[].journey[].recordings[].parts` | integer | yes | How many parts the leg's recording has. |
| `calls[].journey[].recordings[].recording_id` | integer | yes | The recording's id. The `call.transcription` event of this part carries the same id. |
| `calls[].journey[].recordings[].start_time` | string (date-time) or null | yes | When this part started. UTC, ISO 8601 with milliseconds. |
| `calls[].journey[].recordings[].duration` | integer or null | yes | Seconds. |
| `calls[].journey[].recordings[].url` | string (uri) or null | yes | Where to fetch this part (`GET /v1/media/rec_<n>?part=N`) with your key. Null while the part's file is not stored yet. |
| `meta` | object | yes | Paging for list routes. Each list adds its own total. |
| `meta.page` | integer | yes | This page, from 1. |
| `meta.limit` | integer | yes | Rows per page, after limits were applied. |
| `meta.total_pages` | integer | yes | How many pages there are. |
| `meta.total_calls` | integer | yes | Calls that match, across all pages. |

**Example**

```json
{
  "query": {
    "company_id": 1234567890,
    "start_date": "2026-09-21",
    "end_date": "2026-09-22",
    "direction": "any",
    "status": "any",
    "search": "",
    "timezone": "America/New_York"
  },
  "calls": [
    {
      "call_id": "3c9d7e1f-2a4b-4c6d-8e0f-1a2b3c4d5e6f",
      "direction": "outgoing",
      "status": "answered",
      "extension": "104",
      "extension_name": "Maya Chen",
      "remote_name": "",
      "remote_number": "7185550142",
      "group_number": "7185550100",
      "start_time": "2026-09-22T15:40:02.000Z",
      "end_time": "2026-09-22T15:41:10.000Z",
      "duration": 68,
      "journey": [
        {
          "order": 1,
          "type": "user",
          "extension_name": "Maya Chen",
          "extension": "104",
          "start_time": "2026-09-22T15:40:02.000Z",
          "end_time": "2026-09-22T15:41:10.000Z",
          "duration": 68,
          "summary": null,
          "transcription": null,
          "recording_url": null,
          "recordings": []
        }
      ]
    },
    {
      "call_id": "8a1f3c2e-5b7d-4e9a-9c1b-2d4e6f8a0b1c",
      "direction": "incoming",
      "status": "answered",
      "extension": "104",
      "extension_name": "Maya Chen",
      "remote_name": "Jordan Reyes",
      "remote_number": "7185550142",
      "group_number": "7185550100",
      "start_time": "2026-09-21T14:03:12.000Z",
      "end_time": "2026-09-21T14:09:40.000Z",
      "duration": 388,
      "journey": [
        {
          "order": 1,
          "type": "auto_attendant",
          "extension_name": "Main Menu",
          "extension": "100",
          "start_time": "2026-09-21T14:03:12.000Z",
          "end_time": "2026-09-21T14:03:31.000Z",
          "duration": 19,
          "summary": null,
          "transcription": null,
          "recording_url": null,
          "recordings": []
        },
        {
          "order": 2,
          "type": "user",
          "extension_name": "Maya Chen",
          "extension": "104",
          "start_time": "2026-09-21T14:03:31.000Z",
          "end_time": "2026-09-21T14:09:40.000Z",
          "duration": 369,
          "summary": "Jordan Reyes asked to move the Thursday appointment to Friday at 10 AM. Maya Chen confirmed the new time and said a reminder text will follow.",
          "transcription": "Speaker 1: Good morning, thanks for calling. This is Maya.\nSpeaker 2: Hi Maya, this is Jordan Reyes. I need to move my Thursday appointment.\nSpeaker 1: No problem. Would Friday at 10 AM work?\nSpeaker 2: Friday at 10 is perfect. Thank you.",
          "recording_url": "https://api.vocatech.com/v1/media/rec_48213907",
          "recordings": [
            {
              "part": 1,
              "parts": 2,
              "recording_id": 10577640,
              "start_time": "2026-09-21T14:03:32.000Z",
              "duration": 41,
              "url": "https://api.vocatech.com/v1/media/rec_48213907?part=1"
            },
            {
              "part": 2,
              "parts": 2,
              "recording_id": 10577702,
              "start_time": "2026-09-21T14:04:31.000Z",
              "duration": 309,
              "url": "https://api.vocatech.com/v1/media/rec_48213907?part=2"
            }
          ]
        }
      ]
    }
  ],
  "meta": {
    "page": 1,
    "limit": 500,
    "total_pages": 1,
    "total_calls": 2
  }
}
```

#### 400

A date that is not `YYYY-MM-DD`, an end before the start, or a window over 366 days.

**Example**

```json
{
  "error": {
    "code": 400,
    "type": "BAD_REQUEST",
    "message": "Invalid date format or timezone. Use YYYY-MM-DD and a valid IANA timezone."
  }
}
```

**Example**

```json
{
  "error": {
    "code": 400,
    "type": "BAD_REQUEST",
    "message": "end_date is before start_date."
  }
}
```

**Example**

```json
{
  "error": {
    "code": 400,
    "type": "BAD_REQUEST",
    "message": "Ask for at most 366 days at a time (this request covers 400). For more history, walk it a year at a time."
  }
}
```

#### 401

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

#### 403

Refused. The key is scoped away from this route, the request came from an address not on the key's allow-list, or the route refused the request (the message says why).

#### 429

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

#### 500

An unexpected fault on our side, or a search that ran longer than 30 seconds and was stopped.

#### 503

Try again shortly. The key could not be checked just now (`Retry-After: 5`), or a service this route needs could not be reached.

## List messages (old path)

`GET /v1/reports/messages` (old path)

The same as `GET /v1/messages`: the same parameters, the same answer, the same `reports` counter. Keys with the `messaging` scope can call it. Use `GET /v1/messages`.

### Request

**curl**

```bash
curl -G https://api.vocatech.com/v1/reports/messages \
  -H "Authorization: Bearer YOUR_API_KEY" \
  --data-urlencode "start_date=2026-09-21" \
  --data-urlencode "end_date=2026-09-21" \
  --data-urlencode "channel=any"
```

**Node**

```javascript
const params = new URLSearchParams({
  start_date: "2026-09-21",
  end_date: "2026-09-21",
  channel: "any",
});

const res = await fetch(`https://api.vocatech.com/v1/reports/messages?${params}`, {
  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/reports/messages",
    headers={"Authorization": "Bearer YOUR_API_KEY"},
    params={
        "start_date": "2026-09-21",
        "end_date": "2026-09-21",
        "channel": "any",
    },
    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/reports/messages', [
    'query' => [
        'start_date' => '2026-09-21',
        'end_date' => '2026-09-21',
        'channel' => 'any',
    ],
]);

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

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `start_date` | query | string (date) |  | First local day of the window, `YYYY-MM-DD`, in `timezone`. Send it with `end_date`. Send neither for today. A single date is ignored and you get today. For example `2026-09-01`. |
| `end_date` | query | string (date) |  | Last local day of the window, `YYYY-MM-DD`, included in full. At most 366 days after `start_date`. For example `2026-09-07`. |
| `timezone` | query | string |  | IANA time zone for the dates, for example `America/Chicago`. An unknown zone falls back to `America/New_York`. The response echoes the zone used. Default `America/New_York`. |
| `direction` | query | string |  | `incoming` or `outgoing`. One of `any`, `incoming` or `outgoing`. Default `any`. |
| `status` | query | string |  | Filters WhatsApp messages by their status, for example `read`. Texts always read `delivered` here, so `delivered` keeps every text and any other value leaves texts out. Default `any`. |
| `channel` | query | string |  | `text` for texts, `whatsapp` for WhatsApp, `any` for both. One of `any`, `text` or `whatsapp`. Default `any`. |
| `search` | query | string |  | A number or words to find in your number, the other party's number, or the message text. |
| `sort` | query | string |  | Order by time. Rows that tie keep a stable order, so paging is safe. One of `day_desc` or `day_asc`. Default `day_desc`. |
| `page` | query | integer |  | Page number, from 1. Default `1`. At least 1. |
| `limit` | query | integer |  | Rows per page, 1 to 500. A value outside that range is brought inside it. Default `500`. From 1 to 500. |
| `company_id` | query | integer |  | Optional. If you send it, it must be your own company id, or the request is refused with `403`. The key already decides the company. |

### Answers

#### 200

The messages in the window.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `query` | object | yes | The filters that were applied, echoed back. |
| `query.company_id` | integer |  |  |
| `query.start_date` | string (date) |  |  |
| `query.end_date` | string (date) |  |  |
| `query.direction` | string |  |  |
| `query.status` | string |  |  |
| `query.channel` | string |  |  |
| `query.search` | string |  |  |
| `query.timezone` | string |  |  |
| `messages` | array of objects | yes |  |
| `messages[].message_id` | string or null | yes | For a text, the carrier's message id: the one a send in `api` or `email` mode returns, and the one on message webhooks. For WhatsApp, the WhatsApp message id. |
| `messages[].direction` | string | yes | `incoming` or `outgoing`. |
| `messages[].status` | string | yes | A text always reads `delivered` here: this list does not carry carrier receipts (the `message.status_updated` webhook does). A WhatsApp message carries its own status, for example `sent`, `delivered`, `read` or `failed`. |
| `messages[].remote_name` | string | yes | Empty today. |
| `messages[].remote_number` | string or null | yes | The other party's number, digits without the leading 1. |
| `messages[].group_number` | string or null | yes | Your number, digits without the leading 1. |
| `messages[].sent_time` | string (date-time) or null | yes | UTC, ISO 8601 with milliseconds. |
| `messages[].channel` | string | yes | One of `text` or `whatsapp`. |
| `messages[].type` | string | yes | `media` when the message has an attachment. Otherwise a text reads `webex`, and a WhatsApp message carries its conversation type. |
| `messages[].body` | string | yes | The message text, trimmed. |
| `messages[].attachments` | array of objects | yes |  |
| `messages[].attachments[].id` | string | yes | The attachment's media id. Fetch it with `GET /v1/media/{id}`. Matches `^att_[0-9]+$`. |
| `messages[].attachments[].content_type` | string or null | yes | The file's type, for example `image/jpeg`. |
| `messages[].attachments[].filename` | string or null | yes |  |
| `messages[].attachments[].size` | integer or null | yes | Bytes, when known. |
| `meta` | object | yes | Paging for list routes. Each list adds its own total. |
| `meta.page` | integer | yes | This page, from 1. |
| `meta.limit` | integer | yes | Rows per page, after limits were applied. |
| `meta.total_pages` | integer | yes | How many pages there are. |
| `meta.total_messages` | integer | yes | Messages that match, across all pages. |

**Example**

```json
{
  "query": {
    "company_id": 1234567890,
    "start_date": "2026-09-21",
    "end_date": "2026-09-21",
    "direction": "any",
    "status": "any",
    "channel": "any",
    "search": "",
    "timezone": "America/New_York"
  },
  "messages": [
    {
      "message_id": "msg_4c1d7e",
      "direction": "incoming",
      "status": "delivered",
      "remote_name": "",
      "remote_number": "7185550142",
      "group_number": "7185550100",
      "sent_time": "2026-09-21T16:40:11.000Z",
      "channel": "whatsapp",
      "type": "media",
      "body": "Here is the photo of the leak.",
      "attachments": [
        {
          "id": "att_90211",
          "content_type": "image/jpeg",
          "filename": "photo-0921.jpg",
          "size": 184233
        }
      ]
    },
    {
      "message_id": "msg_7f3a9c",
      "direction": "incoming",
      "status": "delivered",
      "remote_name": "",
      "remote_number": "7185550142",
      "group_number": "7185550100",
      "sent_time": "2026-09-21T15:22:04.000Z",
      "channel": "text",
      "type": "webex",
      "body": "Running 10 minutes late, see you soon.",
      "attachments": []
    }
  ],
  "meta": {
    "page": 1,
    "limit": 500,
    "total_pages": 1,
    "total_messages": 2
  }
}
```

#### 400

A date that is not `YYYY-MM-DD`, an end before the start, or a window over 366 days.

**Example**

```json
{
  "error": {
    "code": 400,
    "type": "BAD_REQUEST",
    "message": "Invalid date format or timezone."
  }
}
```

**Example**

```json
{
  "error": {
    "code": 400,
    "type": "BAD_REQUEST",
    "message": "end_date is before start_date."
  }
}
```

#### 401

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

#### 403

Refused. The key is scoped away from this route, the request came from an address not on the key's allow-list, or the route refused the request (the message says why).

#### 429

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

#### 500

An unexpected fault on our side, or a search that ran longer than 30 seconds and was stopped.

#### 503

Try again shortly. The key could not be checked just now (`Retry-After: 5`), or a service this route needs could not be reached.
