Skip to content

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

ParameterTypeDescription
cursorstringOpaque token returned in cursor by the previous page. Absent = first page.
limitintNumber of items per page. Range: 1 to 200. Default: 50.
group_idstringOnly the messages of this group (grp_ + 32 hex). The group must belong to the number in the URL.
contact_idstringOnly 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.
directionstringinbound (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:

read-a-group.sh
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):

what-one-person-said-in-a-group.sh
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:

received-only.sh
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

list-messages.sh
curl -X GET \
"https://api.turbonotify.com/v1/numbers/NUMBER_ALIAS/messages?limit=20" \
-H "Authorization: Bearer YOUR_KEY"

Response (200)

HTTP status: 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

FieldTypeDescription
dataarrayPage of messages, newest first
cursorstring|nullOpaque token for the next page; null when there are no more items
has_morebooleantrue if more pages are available
data[].idstringPublic identifier of the message (msg_ + 32 hex)
data[].organization_idstringPublic identifier of your organization (org_ + 32 hex)
data[].number_aliasstringThe 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_idstringIdentifier of the number (num_…), right after number_alias. It never changes, even if the alias is renamed.
data[].fromobjectSender. Received message: {contact_id?, name?, number}. Sent message: {contact_id?, name?, number, alias}
data[].toobjectRecipient. 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[].directionstringoutbound (sent) or inbound (received)
data[].content_typestringtext, image, audio, video, document, sticker, location, calendar, etc.
data[].channelstringAlways whatsapp
data[].originstringtn (sent through the API) or native (sent or received in WhatsApp itself)
data[].previewstringOpening excerpt of the content, never splitting an emoji or accented character (same value as GET /v1/numbers/{ref}/messages/{id})
data[].content_expiredbooleantrue 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[].atstringISO 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[].statusobject{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 why at can be slightly out of order within a page. The page is always complete, with no gaps.
  • Pagination uses cursor: pass the returned value as cursor on the next call, with the same filters, until has_more is false. 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: true and an empty preview. The sender and the group stay on the message, and it keeps showing up under the group_id and contact_id filters.

Possible errors

StatusError codeDescription
401not_authenticatedThe API key is invalid or missing.
403insufficient_scopeThe API key does not have the messages:read scope (the response includes required_scope).
404number_not_foundThe alias or id in the URL does not match any registered number.
400invalid_number_idThe value in the URL starts with num_ but is not a valid number id.
400invalid_group_idThe group_id is not in the grp_ + 32 hex format.
400invalid_contact_idThe contact_id is not in the ct_ + 32 hex format.
400invalid_cursorThe 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.
404group_not_foundThe group does not exist on this number. A group_id that belongs to another number answers this way instead of returning an empty list.
404contact_not_foundThe contact does not exist on this number. A contact_id that belongs to another number answers this way instead of returning an empty list.
422validation_errorA 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.
500internal_errorUnexpected internal server error.

See also: Get a message · Event polling.