Tutorial

WhatsApp Typing Indicators and Read Receipts: Make Your Bot Feel Human

How to show typing indicators, mark incoming messages as read, and check whether a contact is online via the WhatsApp API — with Node.js code and the timing mistakes that make bots feel fake.

August 17, 2026By Retention Stack
Open in AI

Meta Description: How to show typing indicators, mark incoming messages as read, and check whether a contact is online via the WhatsApp API — with Node.js code and the timing mistakes that make bots feel fake instead of human.


Introduction

A bot that replies in 40 milliseconds doesn't feel fast. It feels fake. Real people read a message, think for a second, and start typing — and WhatsApp users have spent years reading those three animated dots as "someone's actually there." Skip that signal entirely and an instant reply reads as a canned auto-responder, even when the answer behind it came from a real support queue.

There's a second, quieter problem: a message you send can sit in a chat with two grey ticks and nothing else. The user has no idea if a human (or bot) even saw what they sent. WhatsApp gives you three separate signals to close both gaps — a typing indicator, a read receipt, and the other side's presence — and this API exposes all three as plain HTTP calls, no template approval or extra permissions required.

What you'll build: a reply flow that marks the incoming message as seen, shows "typing…", does the actual work, then sends the response — the same rhythm a human has, in code.


The Three Signals

SignalEndpointWhat it does
Typing indicatorPOST /v1/startTyping / POST /v1/stopTypingShows "typing…" (or "recording…") under your bot's name in the recipient's chat
Read receiptPOST /v1/sendSeenMarks their incoming message as read — the two blue ticks they see
PresenceGET /v1/sessions/{session}/presence/{chatId}Tells you if the contact is currently online, typing, or was last seen when

They're independent. You can mark a message as seen without ever showing a typing indicator, or check presence without touching either. Most bots that feel "human" use all three together, which is the flow below.


Show a Typing Indicator Before You Reply

startTyping takes a chatId and session — nothing else:

javascript
const axios = require('axios');

const API_HOST = 'whatsapp-messaging-bot.p.rapidapi.com';
const headers = {
  'x-rapidapi-key': process.env.RAPIDAPI_KEY,
  'x-rapidapi-host': API_HOST,
  'Content-Type': 'application/json',
};

async function startTyping(chatId, session) {
  await axios.post(`https://${API_HOST}/v1/startTyping`, { chatId, session }, { headers });
}

async function stopTyping(chatId, session) {
  await axios.post(`https://${API_HOST}/v1/stopTyping`, { chatId, session }, { headers });
}

chatId accepts a plain phone number ("14155552671", auto-formatted with the @c.us suffix), a group ID (...@g.us), or a channel ID. The same helper works whether you're replying to a DM or a group message.

There's no auto-expiry on this call — it stays on until you explicitly call stopTyping, or until you send a message to that chat. That's different from Twilio's WhatsApp typing indicator, which is tied to a specific message ID and auto-dismisses after 25 seconds if you never respond. Here, you own the full lifecycle — which means you're also responsible for calling stopTyping if your reply logic errors out. A typing indicator that never turns off is worse than no indicator at all.


Mark the Incoming Message as Seen

sendSeen is what puts the two blue ticks on the message they sent you:

javascript
async function markSeen(chatId, session, messageId) {
  await axios.post(
    `https://${API_HOST}/v1/sendSeen`,
    { chatId, session, messageId }, // messageId is optional
    { headers }
  );
}

Leave messageId out and it marks the entire chat as read, matching WhatsApp's own behavior — reading one message reads everything before it. Pass a specific messageId when you want to be precise, for example a support bot that's only processed one message out of a burst of three.

This is the mirror image of the message.ack webhook event covered in WhatsApp API Webhooks: message.ack tells you when a message you sent gets read on their end. sendSeen is you doing the same thing back — marking their message as read on yours. A bot that never calls sendSeen can still function correctly, but every message sits at "delivered" forever from the user's point of view, which reads as "nobody's home."


Check Whether They're Actually Online

Before you commit to an expensive reply — an LLM call, a database write, a lookup against three services — it can be worth knowing whether the contact is still there:

javascript
async function getPresence(session, chatId) {
  const { data } = await axios.get(
    `https://${API_HOST}/v1/sessions/${session}/presence/${chatId}`,
    { headers }
  );
  return data; // { presence: 'online' | 'offline' | 'typing' | ..., lastSeen }
}

Presence isn't pushed by default — subscribe to a specific chat first with POST /v1/sessions/{session}/presence/{chatId}/subscribe, then listen for the presence.update event on your webhook (see WhatsApp API Webhooks for webhook setup). This matters most for support handoff: route to a human agent only if the customer is still online, or hold a broadcast send for a contact who went offline mid-conversation instead of firing it into a chat nobody's looking at.


Putting It Together: A Human-Feeling Reply

javascript
async function replyLikeAHuman(chatId, session, messageId, incomingText) {
  await markSeen(chatId, session, messageId);
  await startTyping(chatId, session);

  try {
    const replyText = await generateReply(incomingText); // your bot logic, an LLM call, a lookup, etc.
    await stopTyping(chatId, session);
    await axios.post(
      `https://${API_HOST}/v1/sendText`,
      { chatId, session, text: replyText },
      { headers }
    );
  } catch (err) {
    await stopTyping(chatId, session); // never leave typing on after a failure
    throw err;
  }
}

Seen, then typing, then the actual work, then the message — in that order, every time. If generateReply takes 2 seconds or 8, the typing indicator covers exactly that gap instead of the reply appearing to materialize out of nowhere.


Timing Mistakes That Undo the Effect

Starting typing before marking seen. Users expect the blue ticks first, then the dots — reversing the order looks like a UI glitch, not a bug most people can name, but one they'll notice.

Leaving stopTyping uncalled on error paths. A typing indicator stuck on for minutes is a worse signal than none — it reads as "the bot is broken," which, at that point, it is.

Showing typing for a reply that's actually instant. Padding a canned FAQ answer with 3 seconds of fake typing to "seem human" is a step past what this feature is for. Use it to cover real latency, not to manufacture the appearance of effort.

Polling presence instead of subscribing. Calling GET /v1/sessions/{session}/presence/{chatId} on a timer burns request quota for information the presence.update webhook event already pushes you for free once you've subscribed.


Key Takeaways

  1. 1.Three independent signals — typing indicator, read receipt, and presence — each map to one endpoint and don't require each other.
  2. 2.startTyping has no auto-expiry here — call stopTyping explicitly, including on error paths, or the indicator sticks.
  3. 3.sendSeen is the read-receipt half of message.ack — one confirms messages you sent, the other confirms messages you received.
  4. 4.Subscribe to presence, don't poll itpresence.update over a webhook is the same information for zero extra requests.
  5. 5.Order matters: mark seen, then start typing, then do the work, then send — matching the rhythm a real person types in.

Related guides:

External resources:


Stop Shipping Bots That Feel Robotic

Three endpoints — sendSeen, startTyping, stopTyping — are the difference between a reply that appears out of nowhere and one that reads like someone's actually there.

Try Free on RapidAPI → Subscribe to Basic ($0) → Scan the QR code → Wire sendSeen and startTyping into your existing reply handler in a few lines.

Ready to Get Started?

Try the WhatsApp API free on RapidAPI with no credit card required.

Try Free on RapidAPI