# Calls

Your call history. Each call carries its journey through menus, groups and people, with AI summaries, transcripts and recording links.

## List calls

`GET /v1/calls`

Your calls in a date window, newest first, one entry per call. Each call carries its `journey`: every leg it took through menus, groups, queues and people, with the AI summary, the transcript and a recording link when the leg was recorded.

**Dates.** Send `start_date` and `end_date` together, as local days in `timezone`. Send neither for today. A single date is ignored and you get today. At most 366 days per request; for more history, walk it a year at a time.

**Paging.** `limit` is 500 by default and at most 500. Calls that tie on the sort key keep a stable order, so paging is safe.

**Filters.** `status`, `extension` and `type` match single legs. A call is listed when one of its legs matches, and its `journey` then shows only the legs that match. `search` looks in the call id, the other party's name and number, your number and the extensions.

**Fields to know.** `status` is the status of the first leg in `journey`, not the call's final outcome: a call a menu answered and a person then missed reads `answered`. `extension` and `extension_name` name the last person who held the call. `recording_url` points at `GET /v1/media/rec_<n>`, which returns a signed link.

**Recordings in parts.** A call that is parked and picked up again is recorded in parts: part 1 up to the park, part 2 after the pick-up, rarely a part 3. Each leg lists its recordings in `recordings`, oldest first, each with its own `url` (`GET /v1/media/rec_<n>?part=N`) and its own start time and length. `recording_url`, `summary` and `transcription` are the latest part's. Nearly every leg has one part. The transcript of each part also comes by webhook (`call.transcription`, one event per part).

A search that runs longer than 30 seconds is stopped with `500`. Narrow the window or add filters.

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

### Request

**curl**

```bash
curl -G https://api.vocatech.com/v1/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/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/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/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.
