Vocatech

By area

Old paths

Read from the API’s own reference file every hour.

Plain text

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/callsOld 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 -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"

Parameters

NameInTypeDescription
start_datequerystring (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_datequerystring (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.
timezonequerystringIANA 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.
directionquerystringincoming 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.
statusquerystringOnly calls with a leg in this status, for example answered or missed. missed never returns an outgoing call. Default any. For example missed.
extensionquerystringOnly calls with a leg on this extension (exact match). For example 104.
typequerystringOnly calls with a leg of this type, for example user, auto_attendant, hunt_group or call_center. Default any.
searchquerystringText to find in the call id, the other party's name or number, your number, or an extension.
sortquerystringOrder 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.
pagequeryintegerPage number, from 1. Default 1. At least 1.
limitqueryintegerRows per page, 1 to 500. A value outside that range is brought inside it. Default 500. From 1 to 500.
company_idqueryintegerOptional. 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.
FieldTypeDescription
query requiredobjectThe filters that were applied, echoed back. extension and type are applied but not echoed.
query.company_idinteger
query.start_datestring (date)The first local day used.
query.end_datestring (date)The last local day used.
query.directionstring
query.statusstring
query.searchstring
query.timezonestringThe time zone used.
calls requiredarray of objects
calls[].call_id requiredstringThe call's id.
calls[].direction requiredstringincoming or outgoing. A call your staff placed through the dial-out portal reads outgoing. A few calls read voicemail.
calls[].status requiredstringThe 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 requiredstring or nullThe last person who held the call. When no person did, the first extension on it.
calls[].extension_name requiredstring or nullThat extension's name, written First Last.
calls[].remote_name requiredstringThe other party's name from caller ID, written First Last. Can be empty.
calls[].remote_number requiredstringThe other party's number, digits without the leading 1. Can be empty.
calls[].group_number requiredstringYour number on the call, digits without the leading 1. Can be empty.
calls[].start_time requiredstring (date-time) or nullUTC, ISO 8601 with milliseconds.
calls[].end_time requiredstring (date-time) or nullUTC, ISO 8601 with milliseconds.
calls[].duration requiredintegerThe call's length in seconds.
calls[].journey requiredarray of objectsEvery leg, in order. When you filter by status, extension or type, only the matching legs are listed.
calls[].journey[].order requiredintegerPosition in the journey, from 1.
calls[].journey[].type requiredstringWhat 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 requiredstring or nullThe name of the person or feature on this leg, as set in the portal, written First Last.
calls[].journey[].extension requiredstring or nullThe extension on this leg.
calls[].journey[].start_time requiredstring (date-time) or nullUTC, ISO 8601 with milliseconds.
calls[].journey[].end_time requiredstring (date-time) or nullUTC, ISO 8601 with milliseconds.
calls[].journey[].duration requiredintegerSeconds on this leg.
calls[].journey[].summary requiredstring or nullThe 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 requiredstring or nullThe 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 requiredstring (uri) or nullWhere 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 requiredarray of objectsEvery 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 requiredintegerThis part's place, from 1, oldest first.
calls[].journey[].recordings[].parts requiredintegerHow many parts the leg's recording has.
calls[].journey[].recordings[].recording_id requiredintegerThe recording's id. The call.transcription event of this part carries the same id.
calls[].journey[].recordings[].start_time requiredstring (date-time) or nullWhen this part started. UTC, ISO 8601 with milliseconds.
calls[].journey[].recordings[].duration requiredinteger or nullSeconds.
calls[].journey[].recordings[].url requiredstring (uri) or nullWhere 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 requiredobjectPaging for list routes. Each list adds its own total.
meta.page requiredintegerThis page, from 1.
meta.limit requiredintegerRows per page, after limits were applied.
meta.total_pages requiredintegerHow many pages there are.
meta.total_calls requiredintegerCalls that match, across all pages.
Example
{
  "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 bodies
Example
{
  "error": {
    "code": 400,
    "type": "BAD_REQUEST",
    "message": "Invalid date format or timezone. Use YYYY-MM-DD and a valid IANA timezone."
  }
}
Example
{
  "error": {
    "code": 400,
    "type": "BAD_REQUEST",
    "message": "end_date is before start_date."
  }
}
Example
{
  "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.
HeaderTypeDescription
Retry-AfterintegerSeconds to wait before you try again.
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.
HeaderTypeDescription
Retry-AfterintegerSeconds to wait before you try again.

List messages (old path)

GET/v1/reports/messagesOld 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 -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"

Parameters

NameInTypeDescription
start_datequerystring (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_datequerystring (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.
timezonequerystringIANA 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.
directionquerystringincoming or outgoing. One of any, incoming or outgoing. Default any.
statusquerystringFilters 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.
channelquerystringtext for texts, whatsapp for WhatsApp, any for both. One of any, text or whatsapp. Default any.
searchquerystringA number or words to find in your number, the other party's number, or the message text.
sortquerystringOrder by time. Rows that tie keep a stable order, so paging is safe. One of day_desc or day_asc. Default day_desc.
pagequeryintegerPage number, from 1. Default 1. At least 1.
limitqueryintegerRows per page, 1 to 500. A value outside that range is brought inside it. Default 500. From 1 to 500.
company_idqueryintegerOptional. 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.
FieldTypeDescription
query requiredobjectThe filters that were applied, echoed back.
query.company_idinteger
query.start_datestring (date)
query.end_datestring (date)
query.directionstring
query.statusstring
query.channelstring
query.searchstring
query.timezonestring
messages requiredarray of objects
messages[].message_id requiredstring or nullFor 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 requiredstringincoming or outgoing.
messages[].status requiredstringA 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 requiredstringEmpty today.
messages[].remote_number requiredstring or nullThe other party's number, digits without the leading 1.
messages[].group_number requiredstring or nullYour number, digits without the leading 1.
messages[].sent_time requiredstring (date-time) or nullUTC, ISO 8601 with milliseconds.
messages[].channel requiredstringOne of text or whatsapp.
messages[].type requiredstringmedia when the message has an attachment. Otherwise a text reads webex, and a WhatsApp message carries its conversation type.
messages[].body requiredstringThe message text, trimmed.
messages[].attachments requiredarray of objects
messages[].attachments[].id requiredstringThe attachment's media id. Fetch it with GET /v1/media/{id}. Matches ^att_[0-9]+$.
messages[].attachments[].content_type requiredstring or nullThe file's type, for example image/jpeg.
messages[].attachments[].filename requiredstring or null
messages[].attachments[].size requiredinteger or nullBytes, when known.
meta requiredobjectPaging for list routes. Each list adds its own total.
meta.page requiredintegerThis page, from 1.
meta.limit requiredintegerRows per page, after limits were applied.
meta.total_pages requiredintegerHow many pages there are.
meta.total_messages requiredintegerMessages that match, across all pages.
Example
{
  "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 bodies
Example
{
  "error": {
    "code": 400,
    "type": "BAD_REQUEST",
    "message": "Invalid date format or timezone."
  }
}
Example
{
  "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.
HeaderTypeDescription
Retry-AfterintegerSeconds to wait before you try again.
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.
HeaderTypeDescription
Retry-AfterintegerSeconds to wait before you try again.