Reading conversations
Read your WhatsApp history over HTTP: list threads (one per contact) and page through the messages in a thread. Use this to backfill, build your own inbox, or reconcile against the webhooks that push messages to you in real time.
With an app key, both endpoints need the whatsapp scope and work on Meta Cloud numbers (native or your own Meta app). With your org API key you can read every number in your inbox — but that route behaves differently, so read that section first.
List threads
GET /channels/{id}/threads returns the channel’s conversations, newest activity first, paged with limit and offset.
curl "https://api.telenow.ai/api/app-whatsapp/channels/b1e2c3d4-…/threads?limit=20&offset=0" \
-H "Authorization: Bearer vai_app_…"
{ "success": true, "data": {
"threads": [
{
"id": "a17c9e0b-…",
"contactPhone": "+919876543210",
"contactName": "Asha Rao",
"lastMessageAt": "2026-07-18T09:14:22Z",
"lastInboundAt": "2026-07-18T09:14:22Z",
"lastBody": "Where is my order?",
"lastFromMe": false,
"unreadCount": 2,
"aiPaused": false,
"createdAt": "2026-07-01T11:02:10Z"
}
],
"total": 137,
"limit": 20,
"offset": 0,
"hasMore": true
} }
| Query param | Notes |
|---|---|
limit | Page size — optional, default 50, maximum 200 (larger values are clamped to 200) |
offset | Rows to skip — page with offset += limit while hasMore is true |
Thread fields:
| Field | Notes |
|---|---|
id | Thread id — use it in /threads/{id}/messages |
contactPhone | The customer’s number (E.164) |
contactName | WhatsApp profile name, if known |
lastMessageAt | Timestamp of the most recent message (in or out) |
lastInboundAt | Timestamp of the last inbound message — the anchor for the 24-hour window |
lastBody | Preview text of the last message |
lastFromMe | true if the last message was outbound |
unreadCount | A running count of inbound messages on this thread — nothing on the API resets it, so keep your own read state (see below) |
aiPaused | true if AI auto-reply is paused for human takeover on this thread |
createdAt | When the thread was first created |
Page until hasMore is false. total is the full thread count for the channel.
Read messages in a thread
GET /threads/{id}/messages returns the most recent messages first, and you page backwards in time with a cursor (beforeId), not an offset. Within each page, messages are ordered oldest → newest.
curl "https://api.telenow.ai/api/app-whatsapp/threads/a17c9e0b-…/messages?limit=30" \
-H "Authorization: Bearer vai_app_…"
{ "success": true, "data": {
"threadId": "a17c9e0b-…",
"contactPhone": "+919876543210",
"messages": [
{
"id": "e5b1…",
"direction": "outbound",
"wamid": "wamid.HBgLMTQxNTU1NTAxMDAVBgAREg…",
"from": "+14155550100",
"body": "Sure — could you send the receipt?",
"type": "text",
"sentBy": "agent",
"status": "read",
"content": null,
"media": null,
"createdAt": "2026-07-18T09:13:05Z"
},
{
"id": "e5b2…",
"direction": "inbound",
"wamid": "wamid.HBgLMTQxNTU1NTAxMDAVAgAREg…",
"from": "+919876543210",
"body": "Here's the receipt you asked for",
"type": "image",
"sentBy": null,
"status": null,
"content": { "media": { "id": "9f8e7d…", "mime": "image/jpeg", "caption": "receipt" } },
"media": { "mime": "image/jpeg", "filename": "receipt.jpg", "url": "https://api.telenow.ai/api/app-whatsapp/media/e5b2…" },
"createdAt": "2026-07-18T09:14:22Z"
}
],
"limit": 30,
"hasMore": true
} }
| Query param | Notes |
|---|---|
limit | Page size — optional, default 50, maximum 200 (larger values are clamped to 200) |
beforeId | Cursor: the oldest message id you hold; returns messages strictly older than it. An id that isn't in this thread returns 400 |
Message fields:
| Field | Notes |
|---|---|
id | Message id — the cursor for beforeId, and the {id} for GET /media/{id} |
direction | inbound (from the customer) or outbound (from you) |
wamid | WhatsApp message id — correlates with whatsapp.message.status events |
from | Sender’s number (E.164) |
body | Text body / caption |
type | text, image, audio, video, document, sticker, location, interactive, button, reaction, contacts, template (your template sends) — other WhatsApp types come through under WhatsApp's own name |
sentBy | For outbound: what sent it — human (a person in the inbox, or any API send), agent (the AI auto-reply), template (a broadcast) or csat (a rating request); null for inbound |
status | Outbound: null until WhatsApp reports, then sent | delivered | read | failed. Inbound: null |
content | Structured extract for non-text types (media / location / reply / button / reaction / contacts), else null |
media | { mime, filename, url } once the attachment has been downloaded, else null |
createdAt | Timestamp |
Paging backwards with beforeId
To load older messages, pass the id of the oldest message you have — the first item of the page — as beforeId; the endpoint walks backwards from there. Repeat while hasMore is true:
# Next (older) page: beforeId = the FIRST (oldest) message id on the previous page
curl "https://api.telenow.ai/api/app-whatsapp/threads/a17c9e0b-…/messages?limit=30&beforeId=e5b1…" \
-H "Authorization: Bearer vai_app_…"
{ "success": true, "data": {
"threadId": "a17c9e0b-…",
"contactPhone": "+919876543210",
"messages": [ /* the 30 messages immediately older than e5b1… */ ],
"limit": 30,
"hasMore": false
} }
When hasMore is false, you’ve reached the start of the thread. (hasMore is true whenever a page comes back full, so a thread whose last page is exactly full needs one more, empty request.)
Downloading media
When media is present, media.url points at the attachment: GET /api/app-whatsapp/media/{messageId}, served as a nosniff attachment. See Sending & receiving media for the full media flow.
Reading does not clear unread
Reading a thread with an app key doesn't send read receipts to the customer and doesn't change any read state.
POST /messages/readsends the customer a read receipt (blue ticks) but doesn't resetunreadCounteither — that field is a running count of inbound messages, so track what your backend has handled yourself.
So a backend that syncs history through these endpoints won’t accidentally “read” a customer’s messages out from under your support team. When your bot has actually handled a message and you want the customer to see blue ticks, call /messages/read deliberately — see Typing indicators & read receipts. The org-key messages route is different: it does mark the thread read.
Endpoint reference
Base /api/app-whatsapp, scope whatsapp, Meta Cloud numbers.
| Method | Path | Purpose |
|---|---|---|
GET | /channels/{id}/threads?limit&offset | Conversations, paged → { threads, total, limit, offset, hasMore } (limit default 50, max 200) |
GET | /threads/{id}/messages?limit&beforeId | Message history, cursor-paged → { threadId, contactPhone, messages, limit, hasMore } (limit default 50, max 200) |
With your org API key
With your org API key (X-API-Key: vai_live_…) you can read every number in your inbox — Meta Cloud, partner numbers and WhatsApp Web — but the org routes are the dashboard's inbox routes, so they differ from the app-key ones above:
| App key | Org API key | |
|---|---|---|
| Threads | GET /api/app-whatsapp/channels/{id}/threads | GET /api/orgs/{orgId}/whatsapp-hub/channels/{channelId}/threads |
| Messages | GET /api/app-whatsapp/threads/{id}/messages | GET /api/orgs/{orgId}/whatsapp-hub/threads/{threadId}/messages |
| Media | GET /api/app-whatsapp/media/{messageId} | GET /api/orgs/{orgId}/whatsapp-hub/media/{messageId} |
| Field names | camelCase | snake_case (database rows) |
| Paging | total, hasMore, beforeId cursor | threads: limit (default 100, max 200) and offset, no total; messages: the latest 200 only, no cursor |
| Read state | Never changed | Reading messages marks the thread read — see below |
Threads take extra filters: status (open, pending, resolved, snoozed), assigned (mine — meaning the key's creator — or unassigned), q (searches name, number, last message and contact name) and tag (a contact label). The response is { threads, statusCounts, limit, offset }; each thread also carries status, assigned_to, assignee_name, tags, snoozed_until and first_response_at. Managing those — assigning, resolving, snoozing — is on Inbox, contacts & segments.
Messages returns { messages, thread } — the latest 200 messages, oldest → newest. Fields are id, direction, provider_msg_id (the wamid), from_phone, body, msg_type, sent_by, status, media_mime, media_filename, content and created_at; download media with the media route above (images, audio and video are served inline there). A message whose status is failed also says why: error_code, error_title and error_details are the first error of WhatsApp's failure report, verbatim (for example 131042, a payment-method problem on the WhatsApp Business Account), and error_hint is a suggested fix for that code, or null when there is none. All four are null on messages that did not fail, on failures whose provider sent no error detail, and on failures recorded before these fields existed.
Reading messages with the org key has side effects.
GET …/threads/{threadId}/messagesmarks the thread read for the person who created the key, and if they had unread messages on a Meta Cloud number it also sends the customer a read receipt (blue ticks) — even with aviewerkey. Use the app-key route if your backend syncs history without a human reading it.
Related
- Typing indicators & read receipts — mark read and clear the unread badge.
- Webhooks & events — get inbound messages pushed instead of polling.
- Sending & receiving media — download attachments referenced by
media.url. - WhatsApp API · WhatsApp product guide.