Voicemails Live
Voicemails are recorded when a caller is sent to voicemail (out-of-hours, no available agents, or explicit caller choice). Each voicemail carries the recording URL and an automatically-generated transcript so agents can triage from text without listening.
List voicemails
Not cursor-paginated in the documented convention's sense: cursor here is a plain ISO timestamp (created-before), plus limit (default 50, max 100). Filters: unread (boolean), state (active default, all, new, heard, or done), deptId.
Code samples
curl 'https://app.message.com/api/v1/voicemails?status=unread' \
-H 'Authorization: Bearer YOUR_WORKSPACE_JWT'const res = await fetch('https://app.message.com/api/v1/voicemails?status=unread', {
headers: { Authorization: 'Bearer ' + token }
});
const { voicemails } = await res.json();import requests
r = requests.get(
"https://app.message.com/api/v1/voicemails",
headers={"Authorization": f"Bearer {token}"},
params={"status": "unread"},
)
voicemails = r.json()["voicemails"]require "net/http"
require "json"
uri = URI("https://app.message.com/api/v1/voicemails?unread=true")
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer #{token}"
http.request(req)
end
voicemails = JSON.parse(res.body)["voicemails"]<?php
$ctx = stream_context_create([
"http" => ["method" => "GET", "header" => "Authorization: Bearer $token"]
]);
$voicemails = json_decode(file_get_contents("https://app.message.com/api/v1/voicemails?unread=true", false, $ctx), true)["voicemails"];Voicemail shape
There is no GET-by-ID; find the one you want in the list response.
{
"id": "uuid",
"conversationId": "uuid",
"phoneNumberId": "uuid",
"fromE164": "+14155550100",
"duration_ms": 22300,
"recordingUrl": "https://cdn.message.com/voicemails/...mp3",
"transcript": "Hi, this is Jane. I had a question about my order...",
"transcriptConfidence": 0.94,
"readAt": null,
"createdAt": "2026-05-13T15:30:00Z"
}Lifecycle: listened, done, reopen
There is no read/unread toggle. The real lifecycle has three states (new -> heard -> done) and three one-way actions, all idempotent, no body:
- listened: stamps
listenedAt/listenedByAgentId. Flipsnewtoheard; never touchesdone. - done: stamps
doneAt/doneByAgentIdand setsstate: "done", removing it from the active list. On a shared department box this resolves it for the whole team. - reopen: only acts on a
donerow, returning it toheardand clearing the done stamps. No-op on anything else.
Re-trigger transcription
Manually re-runs speech-to-text on the recording. There is no delete endpoint for a voicemail; this is the only mutation beyond the lifecycle actions above.
Errors
| Code | When |
|---|---|
400 invalid_query | List query params failed validation. |
403 dept_forbidden | deptId filter is outside the agent's department scope. |
404 not_found | Voicemail does not exist, or is outside the agent's department scope. |
Common pitfalls
- Looking for a delete endpoint. There isn't one. Use
doneto resolve a voicemail instead of trying to remove it. - Looking for read/unread. The real actions are
listened,done, andreopen, a three-state lifecycle, not a boolean. - Forgetting the conversation link. Every voicemail belongs to a call conversation. Reply via Messages on that conversation, not the voicemail record directly.