List messages
List a number’s messages (sent and received), paginated by cursor, newest first. With no filters, the list is the number’s whole inbox; with group_id, contact_id and direction you read just one group, just one person or just what was received. Each item is a summary: for the full body of a specific message, use Get a message.
Use GET /v1/messages for the main number. For extra numbers, use GET /v1/numbers/{ref}/messages, where {ref} is the number’s current alias or its id (num_…, the number_id in the webhook/polling envelope and in each item). Prefer the id: an alias read from an older message may have been renamed since.
Required scope: messages:read
Query parameters
| Parameter | Type | Description |
|---|---|---|
cursor | string | Opaque token returned in cursor by the previous page. Absent = first page. |
limit | int | Number of items per page. Range: 1 to 200. Default: 50. |
group_id | string | Only the messages of this group (grp_ + 32 hex). The group must belong to the number in the URL. |
contact_id | string | Only the messages this contact sent or received (ct_ + 32 hex), including what they said in groups. The contact must belong to the number in the URL. |
direction | string | inbound (received only) or outbound (sent only). |
Filters combine: the list holds only the messages that match every filter you pass. With no filter, you get everything. Group and contact ids come from the number’s groups and contacts and from events (from.contact_id, to.group_id).
Filter the list
Read a group:
curl -G "https://api.turbonotify.com/v1/numbers/NUMBER_ALIAS/messages" \ -H "Authorization: Bearer YOUR_KEY" \ --data-urlencode "group_id=grp_0198d3c5a1b24d3e9f01a2b3c4d5e6f7" \ --data-urlencode "limit=50"Read what one person said in that group (group_id and contact_id together):
curl -G "https://api.turbonotify.com/v1/numbers/NUMBER_ALIAS/messages" \ -H "Authorization: Bearer YOUR_KEY" \ --data-urlencode "group_id=grp_0198d3c5a1b24d3e9f01a2b3c4d5e6f7" \ --data-urlencode "contact_id=ct_0198d3c5a1b24d3e9f01a2b3c4d5e6f8"Read only what the number received:
curl -G "https://api.turbonotify.com/v1/numbers/NUMBER_ALIAS/messages" \ -H "Authorization: Bearer YOUR_KEY" \ --data-urlencode "direction=inbound"To continue on the next page, repeat the call with the same filters and the cursor returned by the previous page. Do not change a filter in the middle of a read: the cursor belongs to the list that produced it. To switch filters, start again without cursor.
List messages
curl -X GET \ "https://api.turbonotify.com/v1/numbers/NUMBER_ALIAS/messages?limit=20" \ -H "Authorization: Bearer YOUR_KEY"const response = await fetch( 'https://api.turbonotify.com/v1/numbers/NUMBER_ALIAS/messages?limit=20', { method: 'GET', headers: { 'Authorization': 'Bearer YOUR_KEY' } });
const { data, cursor, has_more } = await response.json();console.log(data);import requests
url = "https://api.turbonotify.com/v1/numbers/NUMBER_ALIAS/messages"headers = {"Authorization": "Bearer YOUR_KEY"}params = {"limit": 20}
response = requests.get(url, headers=headers, params=params)result = response.json()print(result["data"])package main
import ( "fmt" "io" "net/http")
func main() { req, _ := http.NewRequest( "GET", "https://api.turbonotify.com/v1/numbers/NUMBER_ALIAS/messages?limit=20", nil, ) req.Header.Set("Authorization", "Bearer YOUR_KEY") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) fmt.Println(string(body))}<?php$url = 'https://api.turbonotify.com/v1/numbers/NUMBER_ALIAS/messages?limit=20';
$options = [ 'http' => [ 'header' => ['Authorization: Bearer YOUR_KEY'], 'method' => 'GET' ]];
$context = stream_context_create($options);$result = file_get_contents($url, false, $context);$response = json_decode($result, true);var_dump($response['data']);?>var client = new HttpClient();client.DefaultRequestHeaders.Add("Authorization", "Bearer YOUR_KEY");
var response = await client.GetAsync( "https://api.turbonotify.com/v1/numbers/NUMBER_ALIAS/messages?limit=20");var result = await response.Content.ReadAsStringAsync();Console.WriteLine(result);import java.net.http.*;import java.net.URI;
public class ListMessages { public static void main(String[] args) throws Exception { var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder() .uri(URI.create( "https://api.turbonotify.com/v1/numbers/NUMBER_ALIAS/messages?limit=20")) .header("Authorization", "Bearer YOUR_KEY") .GET() .build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); }}Response (200)
{
"data": [
{
"id": "msg_01993c5a1b24d3e9f01a2b3c4d5e6f70",
"organization_id": "org_a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4",
"number_alias": "main",
"number_id": "num_0f8e2c4a6b1d4e7f9a3c5d7e9b1a3c5e",
"from": {
"contact_id": "ct_0198d3c5a1b24d3e9f01a2b3c4d5e6f8",
"name": "Lucas",
"number": "5511988887777"
},
"to": {
"group_id": "grp_0198d3c5a1b24d3e9f01a2b3c4d5e6f7",
"name": "Sales Team"
},
"direction": "inbound",
"content_type": "text",
"channel": "whatsapp",
"origin": "native",
"preview": "Order 4521 picked",
"content_expired": false,
"at": "2026-06-01T13:04:07Z",
"status": {
"state": "delivered",
"at": "2026-06-01T13:04:09Z",
"error": null
}
},
{
"id": "msg_01993c5a1b24d3e9f01a2b3c4d5e6f71",
"organization_id": "org_a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4",
"number_alias": "main",
"number_id": "num_0f8e2c4a6b1d4e7f9a3c5d7e9b1a3c5e",
"from": {
"number": "5511973117056",
"alias": "main"
},
"to": {
"contact_id": "ct_0198d3c5a1b24d3e9f01a2b3c4d5e6f9",
"name": "Ana",
"number": "5511977776666"
},
"direction": "outbound",
"content_type": "text",
"channel": "whatsapp",
"origin": "tn",
"preview": "Hello, your order is confirmed!",
"content_expired": false,
"at": "2026-06-01T13:00:05Z",
"status": {
"state": "delivered",
"at": "2026-06-01T13:01:00Z",
"error": null
}
},
{
"id": "msg_01993c5a1b24d3e9f01a2b3c4d5e6f72",
"organization_id": "org_a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4",
"number_alias": "main",
"number_id": "num_0f8e2c4a6b1d4e7f9a3c5d7e9b1a3c5e",
"from": {
"contact_id": "ct_0198d3c5a1b24d3e9f01a2b3c4d5e6f9",
"name": "Ana",
"number": "5511977776666"
},
"to": {
"number": "5511973117056",
"alias": "main"
},
"direction": "inbound",
"content_type": "image",
"channel": "whatsapp",
"origin": "native",
"preview": "Product photo",
"content_expired": false,
"at": "2026-06-01T12:58:00Z",
"status": {
"state": "delivered",
"at": "2026-06-01T12:58:00Z",
"error": null
}
}
],
"cursor": "eyJpZCI6Im1zZ18wMSJ9",
"has_more": true
} Each item carries the same envelope as the message.* event for that message (see Webhooks), plus content_expired and status. Mentions, transcription and the full content (text, media) are only in the Get a message response.
Response fields
| Field | Type | Description |
|---|---|---|
data | array | Page of messages, newest first |
cursor | string|null | Opaque token for the next page; null when there are no more items |
has_more | boolean | true if more pages are available |
data[].id | string | Public identifier of the message (msg_ + 32 hex) |
data[].organization_id | string | Public identifier of your organization (org_ + 32 hex) |
data[].number_alias | string | The alias the number had at the time of the message ("main" or the alias of an extra number), the same value as in the event. If the number is renamed later, older messages keep the old alias. |
data[].number_id | string | Identifier of the number (num_…), right after number_alias. It never changes, even if the alias is renamed. |
data[].from | object | Sender. Received message: {contact_id?, name?, number}. Sent message: {contact_id?, name?, number, alias} |
data[].to | object | Recipient. In a group: {group_id, name?}. Received in a one-to-one chat: {contact_id?, name?, number, alias}. Sent in a one-to-one chat: {contact_id?, name?, number} |
data[].direction | string | outbound (sent) or inbound (received) |
data[].content_type | string | text, image, audio, video, document, sticker, location, calendar, etc. |
data[].channel | string | Always whatsapp |
data[].origin | string | tn (sent through the API) or native (sent or received in WhatsApp itself) |
data[].preview | string | Opening excerpt of the content, never splitting an emoji or accented character (same value as GET /v1/numbers/{ref}/messages/{id}) |
data[].content_expired | boolean | true once the content passed your plan’s retention period and was deleted. The message stays in the list for 12 months, with an empty preview |
data[].at | string | ISO 8601: the stable occurrence time of the message (outbound: actual send; inbound: receipt). It does not move as the message progresses sent → delivered → read. |
data[].status | object | {state, at, error}: current delivery state and the timestamp of that transition. error is filled in ({code, message, hint}) when state is "failed" and is null in every other state, on both routes. |
Ordering and pagination
- Items come in the order Turbo Notify accepted each message (newest
first), not strictly by
at. For most messages the two match; for outbound sends,at(the actual send time) can be a few seconds after acceptance, because the send simulates typing time before it reaches WhatsApp. That is whyatcan be slightly out of order within a page. The page is always complete, with no gaps. - Pagination uses
cursor: pass the returned value ascursoron the next call, with the same filters, untilhas_moreisfalse. Treat the cursor as an opaque token and never build or alter the value. - Only content expires: a message past your plan’s
content retention period stays in the list for
12 months, with
content_expired: trueand an emptypreview. The sender and the group stay on the message, and it keeps showing up under thegroup_idandcontact_idfilters.
Possible errors
| Status | Error code | Description |
|---|---|---|
401 | not_authenticated | The API key is invalid or missing. |
403 | insufficient_scope | The API key does not have the messages:read scope (the response includes required_scope). |
404 | number_not_found | The alias or id in the URL does not match any registered number. |
400 | invalid_number_id | The value in the URL starts with num_ but is not a valid number id. |
400 | invalid_group_id | The group_id is not in the grp_ + 32 hex format. |
400 | invalid_contact_id | The contact_id is not in the ct_ + 32 hex format. |
400 | invalid_cursor | The cursor is not a value returned by a previous page. Use the exact value from the last response, with the same filters, or start again without cursor. |
404 | group_not_found | The group does not exist on this number. A group_id that belongs to another number answers this way instead of returning an empty list. |
404 | contact_not_found | The contact does not exist on this number. A contact_id that belongs to another number answers this way instead of returning an empty list. |
422 | validation_error | A query parameter failed validation: for example, limit outside the 1 to 200 range, or a direction other than inbound and outbound. The response includes details.errors with the rejected fields. |
500 | internal_error | Unexpected internal server error. |
See also: Get a message · Event polling.