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 paramNotes
limitPage size — optional, default 50, maximum 200 (larger values are clamped to 200)
offsetRows to skip — page with offset += limit while hasMore is true

Thread fields:

FieldNotes
idThread id — use it in /threads/{id}/messages
contactPhoneThe customer’s number (E.164)
contactNameWhatsApp profile name, if known
lastMessageAtTimestamp of the most recent message (in or out)
lastInboundAtTimestamp of the last inbound message — the anchor for the 24-hour window
lastBodyPreview text of the last message
lastFromMetrue if the last message was outbound
unreadCountA running count of inbound messages on this thread — nothing on the API resets it, so keep your own read state (see below)
aiPausedtrue if AI auto-reply is paused for human takeover on this thread
createdAtWhen 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 paramNotes
limitPage size — optional, default 50, maximum 200 (larger values are clamped to 200)
beforeIdCursor: the oldest message id you hold; returns messages strictly older than it. An id that isn't in this thread returns 400

Message fields:

FieldNotes
idMessage id — the cursor for beforeId, and the {id} for GET /media/{id}
directioninbound (from the customer) or outbound (from you)
wamidWhatsApp message id — correlates with whatsapp.message.status events
fromSender’s number (E.164)
bodyText body / caption
typetext, image, audio, video, document, sticker, location, interactive, button, reaction, contacts, template (your template sends) — other WhatsApp types come through under WhatsApp's own name
sentByFor 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
statusOutbound: null until WhatsApp reports, then sent | delivered | read | failed. Inbound: null
contentStructured 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
createdAtTimestamp

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/read sends the customer a read receipt (blue ticks) but doesn't reset unreadCount either — 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.

MethodPathPurpose
GET/channels/{id}/threads?limit&offsetConversations, paged → { threads, total, limit, offset, hasMore } (limit default 50, max 200)
GET/threads/{id}/messages?limit&beforeIdMessage 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 keyOrg API key
ThreadsGET /api/app-whatsapp/channels/{id}/threadsGET /api/orgs/{orgId}/whatsapp-hub/channels/{channelId}/threads
MessagesGET /api/app-whatsapp/threads/{id}/messagesGET /api/orgs/{orgId}/whatsapp-hub/threads/{threadId}/messages
MediaGET /api/app-whatsapp/media/{messageId}GET /api/orgs/{orgId}/whatsapp-hub/media/{messageId}
Field namescamelCasesnake_case (database rows)
Pagingtotal, hasMore, beforeId cursorthreads: limit (default 100, max 200) and offset, no total; messages: the latest 200 only, no cursor
Read stateNever changedReading 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}/messages marks 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 a viewer key. Use the app-key route if your backend syncs history without a human reading it.