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.
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
| Signal | Endpoint | What it does |
|---|---|---|
| Typing indicator | POST /v1/startTyping / POST /v1/stopTyping | Shows "typing…" (or "recording…") under your bot's name in the recipient's chat |
| Read receipt | POST /v1/sendSeen | Marks their incoming message as read — the two blue ticks they see |
| Presence | GET /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:
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:
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:
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
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.Three independent signals — typing indicator, read receipt, and presence — each map to one endpoint and don't require each other.
- 2.
startTypinghas no auto-expiry here — callstopTypingexplicitly, including on error paths, or the indicator sticks. - 3.
sendSeenis the read-receipt half ofmessage.ack— one confirms messages you sent, the other confirms messages you received. - 4.Subscribe to presence, don't poll it —
presence.updateover a webhook is the same information for zero extra requests. - 5.Order matters: mark seen, then start typing, then do the work, then send — matching the rhythm a real person types in.
Related guides:
- •Build a WhatsApp Customer Support Bot — a full bot this reply flow drops straight into
- •Build a WhatsApp Chatbot with Python — the same pattern in Python/FastAPI instead of Node.js
- •WhatsApp API Webhooks — registering
presence.updateandmessage.ackso you're not polling for either
External resources:
- •Meta: mark a WhatsApp message as read — how the official Cloud API handles the same read-receipt concept, for comparison
- •Twilio: WhatsApp typing indicators — the 25-second auto-dismiss design referenced above, from a different provider's implementation
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