WhatsApp Operators DailyThe Blueticks DispatchFriday, August 21, 2026
Productivity

WhatsApp API to Read Messages and Chat History on Your Own Number (Python & Node, 2026)

Meta's Cloud API sends, but it never hands you the conversations you already have. Here is the read path on your own number, endpoint by endpoint, gaps included.

DRBy Daniel Roth · August 21, 2026 · 11 min read
WhatsApp API to Read Messages and Chat History on Your Own Number (Python & Node, 2026)

You have three years of customer conversations sitting in WhatsApp. Every quote you gave, every address a client typed instead of emailing, every "can you do Thursday instead". Your code cannot see one line of it. Every WhatsApp API tutorial on the internet teaches you to send. This one is the return trip: a whatsapp api read messages path, verified endpoint by endpoint against production.

Can you read your own WhatsApp messages through an API?

Yes. Blueticks exposes a REST read path on your own connected number: GET /v1/chats lists your conversations, GET /v1/messages returns message history, and GET /v1/search spans both. Everything comes back as JSON.

One boundary governs this whole article, and it is stated once here. This is the account owner reading their own WhatsApp, through their own connected number. Your chats, your history, your attachments. It is not a way to read anyone else's messages, and there is no third-party mode to ask about.

Separate two things that get confused constantly. Inbound webhooks are a push: a message arrives, your server gets poked (that path is here). A read API is a pull: you ask for history that already exists, including history from before your integration did. Push cannot give you last March. Pull can.

Connecting a number is covered in the own-number REST API guide. From here I assume one is connected.

Why doesn't Meta's Cloud API give you your chat history?

Because the Cloud API is a send pipe with an inbound notification stream attached, not a store you can query. Its message reference documents exactly one endpoint, POST /{Phone-Number-ID}/messages. There is no GET, and nothing in the product returns a conversation you already had.

Meta says it plainly in its own platform overview: "the contents of any message sent from a WhatsApp user to your business phone number is communicated via webhook" (About the WhatsApp Business Platform). Communicated, once, at the moment it happens. If your listener was down, or you had not built one yet, that message is gone from your side of the wire forever. The Cloud API messages reference confirms the shape of the surface: send only.

There is exactly one narrow exception, and it is worth knowing so nobody sells it to you as a chat history API. Meta ships a history webhook that fires when a partner onboards a WhatsApp Business app number onto Cloud API and the business agrees to share its chats. It covers "all messages sent or received within 180 days of the time when the business was onboarded", and per Meta's own history webhook reference, "messages that are part of a group chat will not be included" and media asset IDs only arrive for media sent within 14 days of onboarding.

One-time, partner-gated, 180 days, no groups, 14 days of media. That is a migration tool, not a read API. To answer "what did this customer say to me in March", the Cloud API has no move. That asymmetry, plus skipping Meta Business verification entirely, is why this article exists.

What can you actually read from your own connected number?

Eleven endpoints across chats, messages, search, media and delivery state. Every one is a GET except the batch delivery lookup, all of them run against the number you connected, and all of them return the same envelope, so one client covers the whole surface.

  • GET /v1/chats - lists your conversations, newest first
  • GET /v1/chats/{chatId} - one chat by JID
  • GET /v1/chats/{chatId}/participants - group members with admin standing
  • GET /v1/latest-chats - chats with their recent messages attached
  • GET /v1/messages - message history, one chat or all
  • GET /v1/messages/{waMessageKey} - a single message by key
  • GET /v1/search - contacts and chats matched by name
  • GET /v1/messages/media/{waMessageKey} - the bytes behind an attachment
  • GET /v1/messages/pinned/{chatId} - pinned messages in one chat
  • GET /v1/messages/ack/{waMessageKey} - delivery state of one message
  • POST /v1/messages/acks - delivery state, up to 200 keys at once

That is the whole whatsapp api read messages surface. Two shapes to internalise. List endpoints return { "success": true, "data": [...], "limit": N, "skip": N, "total": N }; single-object endpoints return { "success": true, "data": {...} }. Null fields are stripped on the way out, so a field you do not see is absent rather than null. Treat missing as normal.

You can point this at your own number today and see real rows come back. Connect your number and run your first GET /v1/chats call: no Meta Business verification, no per-message fees, and the chat history that the Cloud API structurally cannot hand you. Use the number you already message customers from, not a new one, because the history you want is the history that number already has.

Hands pulling one index card from a long library catalogue drawer, an analogue chat list lookup

How do you list and filter your chats?

GET /v1/chats returns your conversations newest-first, offset-paginated with limit (1 to 200, default 50) and skip. Narrow it with kinds for contacts, groups or newsletters, searchToken for a name substring, since for an activity cutoff, and includeLastMessage to get a preview without a second call.

This is the whatsapp api get chats baseline. In Python:

import os, requests

BASE = "https://api.blueticks.co"
HEADERS = {"Authorization": f"Bearer {os.environ['BLUETICKS_API_KEY']}"}

r = requests.get(
    f"{BASE}/v1/chats",
    headers=HEADERS,
    params={
        "kinds": "contact,group",          # INCLUDE semantics, comma-separated
        "limit": 100,                       # 1-200, default 50
        "skip": 0,
        "includeLastMessage": "true",
        "since": "2026-08-01T00:00:00Z",   # ISO 8601, offset allowed
    },
    timeout=30,
)
body = r.json()
for chat in body["data"]:
    print(chat["chatId"], chat["chatType"], chat["lastMessageAt"], chat.get("lastMessageText"))

print(body["total"], "matched;", "windowed:", body.get("sinceApplied", False))

And in Node, same call, no SDK:

const BASE = "https://api.blueticks.co";
const HEADERS = { Authorization: `Bearer ${process.env.BLUETICKS_API_KEY}` };

const qs = new URLSearchParams({
  kinds: "contact,group",
  limit: "100",
  includeLastMessage: "true",
  includeArchive: "false",
});

const res = await fetch(`${BASE}/v1/chats?${qs}`, { headers: HEADERS });
const { data, total, sinceApplied } = await res.json();

for (const c of data) {
  console.log(c.chatId, c.chatType, c.unreadCount, c.markedUnread, c.lastMessageText);
}

Three behaviours that will otherwise cost you an afternoon. Archived chats are excluded unless you pass includeArchive=true. Chats with no resolved name are skipped unless you pass includeWithoutName=true, which matters a lot if you deal with numbers that were never saved as contacts. And kinds beats filter when you send both, because kinds can express a subset that the single-value filter cannot.

Each row carries chatId, name, chatType (contact, group or newsletter), lastMessageAt, unreadCount, and markedUnread, the manual dot-badge state, which is deliberately separate from a real unread count. Full field lists are in the Blueticks API reference.

How do you pull message history from one chat?

GET /v1/messages?chatId=<JID> returns that chat's whatsapp message history, newest-first by default. Flip it with order=asc, bound it with since and until, page it with limit (1 to 200) and skip, and narrow it with messageTypes, sender, hasMedia or queryAny.

One endpoint does all of it, so the get whatsapp messages api pattern collapses to a single function in your client.

params = {
    "chatId": "972501234567@c.us",
    "order": "asc",                        # oldest-first; default is desc
    "since": "2026-03-01T00:00:00Z",
    "until": "2026-04-01T00:00:00Z",
    "limit": 200,
    "messageTypes": "chat,image,document", # comma-separated or repeated
    "loadFromPhoneIfNeeded": "true",
}
r = requests.get(f"{BASE}/v1/messages", headers=HEADERS, params=params, timeout=60)

for m in r.json()["data"]:
    who = "me" if m["fromMe"] else m.get("senderName") or m.get("author")
    print(m["timestamp"], who, m["type"], m.get("text", ""))

The Node equivalent, walking pages until a short page comes back:

async function history(chatId, { pageSize = 200 } = {}) {
  const out = [];
  for (let skip = 0; ; skip += pageSize) {
    const qs = new URLSearchParams({
      chatId, order: "asc", limit: String(pageSize), skip: String(skip),
    });
    const res = await fetch(`${BASE}/v1/messages?${qs}`, { headers: HEADERS });
    const { data } = await res.json();
    out.push(...data);
    if (data.length < pageSize) return out;   // short page = end of the window
  }
}

Message rows carry waMessageKey (an object, not a string: fromMe, remote, id, _serialized, participant), chatId, from, author, senderName, timestamp, text, type, fromMe, ack, and, on replies, a quotedMessage with the original's key and a preview. from mirrors WhatsApp's own semantics and is the chat JID, not the sender, so on an outgoing message it points at the recipient. Use author when you mean "who sent this". Getting those two backwards is the single most common bug I see in first-pass integrations.

type is one of chat, image, video, document, audio, ptt, sticker, gif, ptv, poll_creation, location, vcard or revoked. System events are excluded by default and only come back if you name them in messageTypes.

How do you search across every chat at once?

Two different searches, and picking the wrong one wastes calls. GET /v1/search matches names across contacts and chats in one shot. GET /v1/messages with chatId omitted searches content across every chat, which is what you want for "find the invoice someone sent me".

Name search first, with types to restrict the result kinds:

r = requests.get(f"{BASE}/v1/search", headers=HEADERS,
                 params={"searchToken": "hadar", "types": "contacts,chats"}, timeout=30)
found = r.json()["data"]
print(len(found["contacts"]), "contacts,", len(found["chats"]), "chats")

Content search is the read whatsapp messages api move that saves you N calls. queryAny takes up to 24 terms and matches a message if the body, caption or filename contains any of them, so one paged pass covers a whole hunt:

const qs = new URLSearchParams({
  queryAny: "invoice,receipt,fatura,חשבונית",  // OR across up to 24 terms
  hasMedia: "true",
  mime: "application/pdf",                      // prefix match
  since: "2026-01-01T00:00:00Z",
  limit: "200",
});
const res = await fetch(`${BASE}/v1/messages?${qs}`, { headers: HEADERS });
const { data, total } = await res.json();
console.log(`${total} PDF invoices across every chat`);

mime is a prefix, so image/ catches every image type and application/pdf catches only PDFs. filenameContains and sender are case-insensitive substrings. Combine them freely: they compose in a single pass rather than forcing you to filter client-side.

If you need a dashboard-style overview instead of a targeted hunt, GET /v1/latest-chats returns chats with their recent messages already attached, capped by numberOfChats (1 to 200, default 50) and numberOfMessages (0 to 100, default 20). One call, not fifty.

How do you read media and attachments?

In two steps. Message rows tell you an attachment exists but never carry the bytes, so first filter for media with hasMedia=true, mime and filenameContains, then fetch each file separately from GET /v1/messages/media/{waMessageKey}, passing the message's chatId as a query accelerator and reading mediaUnavailable before you touch the payload.

from urllib.parse import quote
import base64

key = msg["waMessageKey"]["_serialized"]
r = requests.get(
    f"{BASE}/v1/messages/media/{quote(key, safe='')}",
    headers=HEADERS,
    params={"chatId": msg["chatId"], "maxAttempts": 1},
    timeout=60,
)
media = r.json()["data"]

if media.get("mediaUnavailable"):
    print("no bytes:", media["mediaUnavailable"])
else:
    open(media.get("filename", "download.bin"), "wb").write(
        base64.b64decode(media["dataBase64"])
    )

The media object gives you url, mimetype, filename, dataBase64, an originalQuality flag, and, when the bytes could not be produced, mediaUnavailable with one of five reasons: expired, fetching, awaiting_sender, error or no_media. They are not interchangeable. fetching and awaiting_sender are worth retrying; expired means WhatsApp aged the file out of CDN retention and there is nothing to retry against.

maxAttempts=1 is the fast path that skips the lazy-fetch poll. Leave it off for background archiving where you want the retries; set it to 1 when a user is waiting.

Which message states can you trust, and which are best-effort?

Every message row carries an ack integer: -1 error, 0 pending, 1 server, 2 device, 3 read, 4 played. You can also read one key's state via GET /v1/messages/ack/{waMessageKey} or up to 200 keys via POST /v1/messages/acks. Trust the first three. Treat 3 as best-effort.

I am going to be blunt here because the alternative is you finding out in production. On the outbound side, queued, sending and delivered are verified end to end. Read state is not. It is a documented open defect in our own tracker: the read log is written from a real-time ack handler that frequently never observes the read receipt, so a message the recipient clearly read often stays at 2.

Part of that is nobody's bug. If your recipient has read receipts switched off in their privacy settings, WhatsApp never produces the receipt in the first place, so no implementation can produce a 3 for them. The WhatsApp Help Center on read receipts lists exactly that as a cause in its missing-read-receipts section. Groups run the other way, and the same page is explicit about it: "This won't disable the read receipts for group chats or play receipts for voice messages. There's no way to turn those settings off."

So: build alerting on delivered. Build reporting on read only if a soft undercount is acceptable. The outbound lifecycle in full, event by event, belongs to a different article, and I wrote it: tracking outbound delivery status with webhooks owns the messages you send. This one owns the messages you read back.

Half-stocked warehouse shelf with the far end empty, illustrating truncated WhatsApp chat history

What breaks: pagination, backfill, and history that isn't on the device yet

Six things, in roughly the order you will hit them: offset drift, unsynced history, JID validation, windowed since, lazy media and inlined payloads. The expensive one is number two, because it does not throw. It returns a clean 200 with a truncated window and no complaint at all.

  1. skip drifts under live traffic. The list is newest-first. A message arriving mid-walk shifts every row down one, so page 3 re-serves a row from page 2. Deduplicate on waMessageKey._serialized, or page a fixed window with since and until instead of raw offsets.
  2. The history is not on the device yet. The store holds what has been synced, not everything that ever existed. Pass loadFromPhoneIfNeeded=true to pull older messages on demand, or call POST /v1/messages/load_older/{chatId} with {"pages": 5, "until_date": "2025-01-01"}. Pages run 1 to 10 per call and are paced roughly 2.5 seconds apart, so this is slow, not free. Read historyUnavailable to tell "the phone has nothing left" from "already fully synced", and check coverage.oldestLoadedIso to answer "did I actually reach a year back" instead of assuming.
  3. chatId must be a JID. The shape is <digits>@c.us, @s.whatsapp.net, @g.us, @lid, @broadcast or @newsletter. A bare +14155551234 is a validation error, not a lookup. The suffix is not decorative: @c.us and @g.us are different addressing spaces.
  4. since on the chat list is windowed, not absolute. It filters a widened fetch rather than querying the whole store, so total is the match count inside that window. The response sets sinceApplied: true to tell you so. Do not report it as a store-wide figure.
  5. Media resolves lazily. hasMedia: true on a row means an attachment exists, not that bytes are ready. Branch on mediaUnavailable every time.
  6. includeMediaContent=true is heavy. It inlines payloads on every row. For anything past a handful of messages, list first and fetch media per key.

Paraphrasing an operator from a support thread rather than quoting him verbatim: the read call that scared him was not the one that errored, it was the one that returned eleven months of a fourteen-month relationship and looked completely healthy. Assume truncation. Verify coverage.

What a raw read endpoint can't do that Blueticks adds

A read endpoint hands you rows. What you actually want is a loop: read the conversation, decide something, then act on the same number, and know whether the action landed. Reading is half. The other half is sending, scheduling and delivery visibility on the identical connection.

You are not stitching a read vendor to a send vendor and reconciling two identities. The same connected number that answers GET /v1/chats also takes POST /v1/messages/{chatId} for an immediate send and POST /v1/scheduled-messages for a queued one, and emits the outbound lifecycle events to your webhook.

The same account is reachable four ways, each with live documentation: the REST API, the MCP server for agent hosts, your own number rather than a rented sender identity, and webhooks for the event stream.

There is an act half too, mark_read, archive, pin, mute, labels and notes, which turns the read path into real triage. That is an agent-shaped job rather than a REST-tutorial one, and it has its own article: AI-driven WhatsApp inbox management.

Two adjacent workstations connected by a single cable, the read then act loop on one WhatsApp number

Frequently asked questions

Can I read messages from someone else's WhatsApp account with this API? No. The read path only ever covers the number you connected yourself. It returns your chats and your history on your own account. There is no mechanism for reading a third party's messages, and monitoring another person's account is not a supported use.

Does the WhatsApp Cloud API have a chat history endpoint? No. Its messages reference documents a single POST endpoint and no GET. The only history Meta exposes is a one-time partner-onboarding history webhook covering 180 days, excluding group chats, per Meta's history webhook reference.

How far back does the whatsapp chat history api go? As far back as the connected device has synced, which is not automatically everything. Use loadFromPhoneIfNeeded=true or POST /v1/messages/load_older/{chatId} to pull older pages from the phone, then check coverage.oldestLoadedIso on the response to confirm the depth you actually reached.

What is the difference between this and an inbound webhook? An inbound webhook pushes new messages to you as they arrive and cannot show you anything from before you set it up. A read API pulls history that already exists, on demand. Most production systems use both: webhooks for reaction time, GET /v1/messages for context and backfill.

Can I search across all my chats in one call? Yes. Call GET /v1/messages without chatId and it searches every chat. Add queryAny with up to 24 terms for an OR match on body, caption and filename, and combine it with mime, hasMedia, sender or filenameContains to narrow the result set in the same pass.

Email

The Dispatch, every week.

One sharp WhatsApp growth tactic in your inbox each week. Joined by 238,000+ founders, marketers and support leads.

Free forever. No spam, unsubscribe in one click.