Overview
Typing indicators and read receipts provide real-time feedback to users, making conversations feel more natural and interactive. These features help users understand when you’re responding and confirm when their messages have been seen.Key Features
- Typing Indicators: Show when you’re composing a response
- Read Receipts: Mark messages as read to acknowledge receipt
- Smart Duration: Customizable typing indicator duration (up to 25 seconds)
- Message Context: Link typing indicators to specific messages
- Automatic Management: Built-in timeout and state management
How It Works
Typing Indicators
When you send a typing indicator, the user sees the familiar “typing…” animation in their chat, similar to regular WhatsApp conversations. This provides immediate feedback that their message was received and you’re preparing a response.Read Receipts
Read receipts show blue checkmarks next to messages, indicating they’ve been read. This helps users know their messages were seen, even if you don’t respond immediately. User’s View:Copy
[Their message] ✓✓ (delivered)
[Their message] ✓✓ (read - blue checkmarks)
[Typing indicator] You are typing...
Types and Interfaces
Copy
// Enum additions
export enum WhatsAppMessageType {
// ... existing types
TYPING_INDICATOR = 'typing_indicator',
READ_RECEIPT = 'read_receipt',
}
// Typing indicator message structure
export interface TypingIndicatorMessage {
messaging_product: 'whatsapp';
recipient_type?: 'individual';
to: string;
message_id?: string;
status?: 'read';
typing_indicator: {
type: 'text';
};
}
// Read receipt message structure
export interface ReadReceiptMessage {
messaging_product: 'whatsapp';
status: 'read';
message_id: string;
}
// Response interface
export interface TypingIndicatorResponse {
success: boolean;
messageId?: string;
error?: string;
}
Methods
Typing Indicators
Copy
// Basic typing indicator (25 seconds default)
async sendTypingIndicator(to: string, messageId?: string): Promise<TypingIndicatorResponse>
// Typing indicator with custom duration
async sendTypingIndicatorWithDuration(
to: string,
duration: number = 25000,
messageId?: string
): Promise<TypingIndicatorResponse>
Read Receipts
Copy
// Mark specific message as read
async markMessageAsRead(messageId: string): Promise<TypingIndicatorResponse>
Usage Examples
Basic Typing Indicators
Copy
import { WhatsAppClient } from 'whatsapp-client-sdk';
const client = new WhatsAppClient(accessToken, phoneNumberId);
// Simple typing indicator
await client.sendTypingIndicator('+1234567890');
// Typing indicator linked to specific message
await client.sendTypingIndicator('+1234567890', messageId);
// Custom duration (15 seconds)
await client.sendTypingIndicatorWithDuration('+1234567890', 15000);
// Custom duration with message context
await client.sendTypingIndicatorWithDuration('+1234567890', 20000, messageId);
Read Receipts
Copy
// Mark a message as read
await client.markMessageAsRead('wamid.HBgLMTY1MDM4Nzk0MzkVAgARGBJDQjZCMzlEQUE4OTJCMTE4RTUA');
Intelligent Conversation Flow
Copy
async function handleUserMessage(message) {
// 1. Immediately mark as read
await client.markMessageAsRead(message.id);
// 2. Show typing indicator while processing
await client.sendTypingIndicator(message.from, message.id);
// 3. Simulate processing time
await new Promise(resolve => setTimeout(resolve, 3000));
// 4. Send response
await client.sendText(message.from, 'Thanks for your message! How can I help you?');
}
Webhook Integration
Smart Response System
Copy
const webhookProcessor = client.createWebhookProcessor({
onTextMessage: async (message) => {
// Mark as read immediately
await client.markMessageAsRead(message.id);
const query = message.text.toLowerCase();
if (query.includes('help') || query.includes('support')) {
// Show typing for support queries
await client.sendTypingIndicatorWithDuration(message.from, 10000, message.id);
// Process support request
const supportResponse = await processSupportQuery(query);
await client.sendText(message.from, supportResponse);
}
else if (query.includes('order') || query.includes('status')) {
// Longer typing for order lookups
await client.sendTypingIndicatorWithDuration(message.from, 20000, message.id);
// Look up order information
const orderInfo = await lookupOrder(query);
await client.sendText(message.from, orderInfo);
}
else {
// Quick acknowledgment
await client.sendTypingIndicator(message.from, message.id);
await new Promise(resolve => setTimeout(resolve, 2000));
await client.sendText(message.from, 'Got it! Let me help you with that.');
}
},
onImageMessage: async (message) => {
await client.markMessageAsRead(message.id);
await client.sendTypingIndicator(message.from, message.id);
// Process image
await new Promise(resolve => setTimeout(resolve, 5000));
await client.sendText(message.from, 'Thanks for sharing the image!');
},
onButtonReply: async (message) => {
await client.markMessageAsRead(message.id);
// Quick response for button clicks
await client.sendTypingIndicatorWithDuration(message.from, 5000);
const response = await processButtonAction(message.button.id);
await client.sendText(message.from, response);
}
});
Advanced Conversation Management
Copy
class ConversationManager {
private activeTyping = new Map();
async startTypingSession(userPhone, messageId, expectedDuration) {
// Prevent multiple typing indicators
if (this.activeTyping.has(userPhone)) {
return;
}
this.activeTyping.set(userPhone, {
messageId,
startTime: Date.now(),
duration: expectedDuration
});
await client.sendTypingIndicatorWithDuration(userPhone, expectedDuration, messageId);
// Auto-cleanup
setTimeout(() => {
this.activeTyping.delete(userPhone);
}, expectedDuration);
}
async handleComplexQuery(message) {
await client.markMessageAsRead(message.id);
const complexity = this.analyzeQueryComplexity(message.text);
// Adjust typing duration based on complexity
const duration = complexity === 'simple' ? 5000 :
complexity === 'medium' ? 15000 : 25000;
await this.startTypingSession(message.from, message.id, duration);
// Process based on complexity
const response = await this.processQuery(message.text, complexity);
await client.sendText(message.from, response);
}
analyzeQueryComplexity(text) {
const wordCount = text.split(' ').length;
const hasMultipleQuestions = (text.match(/\?/g) || []).length > 1;
const hasComplexTerms = /order|refund|technical|problem|issue/.test(text.toLowerCase());
if (wordCount > 20 || hasMultipleQuestions || hasComplexTerms) {
return 'complex';
} else if (wordCount > 10) {
return 'medium';
}
return 'simple';
}
}
Advanced Use Cases
Customer Service Bot
Copy
class CustomerServiceBot {
async handleCustomerMessage(message) {
// Always acknowledge receipt
await client.markMessageAsRead(message.id);
const intent = await this.classifyIntent(message.text);
switch (intent) {
case 'complaint':
await this.handleComplaint(message);
break;
case 'order_inquiry':
await this.handleOrderInquiry(message);
break;
case 'technical_support':
await this.handleTechnicalSupport(message);
break;
default:
await this.handleGeneralInquiry(message);
}
}
async handleComplaint(message) {
// Show empathy with immediate typing
await client.sendTypingIndicator(message.from, message.id);
await new Promise(resolve => setTimeout(resolve, 3000));
await client.sendText(
message.from,
'I understand your concern and I want to help resolve this for you. Let me look into this right away.'
);
// Continue with longer processing
await client.sendTypingIndicatorWithDuration(message.from, 20000);
// Escalate to human agent
await this.escalateToHuman(message);
}
async handleOrderInquiry(message) {
await client.sendTypingIndicatorWithDuration(message.from, 15000, message.id);
const orderInfo = await this.lookupOrderInfo(message);
if (orderInfo) {
await client.sendText(
message.from,
`Here's your order information:\n\n${orderInfo}`
);
} else {
await client.sendText(
message.from,
'I couldn\'t find that order. Could you please provide your order number?'
);
}
}
}
Progressive Response System
Copy
class ProgressiveResponder {
async handleLongProcess(message, processFunction) {
// Initial acknowledgment
await client.markMessageAsRead(message.id);
await client.sendTypingIndicator(message.from, message.id);
await client.sendText(message.from, 'Processing your request...');
// Show continued activity
const processSteps = [
'Analyzing your request...',
'Retrieving information...',
'Preparing response...'
];
for (let i = 0; i < processSteps.length; i++) {
await client.sendTypingIndicatorWithDuration(message.from, 8000);
const stepResponse = await client.sendText(message.from, processSteps[i]);
// React to our own message to show progress
await client.reactWithCheck(message.from, stepResponse.messages[0].id);
await new Promise(resolve => setTimeout(resolve, 3000));
}
// Final typing before result
await client.sendTypingIndicatorWithDuration(message.from, 10000);
const result = await processFunction(message);
await client.sendText(message.from, result);
}
}
Smart Retry Logic
Copy
class SmartTypingManager {
async safeTypingIndicator(userPhone, messageId, duration = 25000) {
try {
const response = await client.sendTypingIndicatorWithDuration(
userPhone,
duration,
messageId
);
if (!response.success) {
console.log('Typing indicator failed:', response.error);
// Continue without typing indicator
return false;
}
return true;
} catch (error) {
console.error('Typing indicator error:', error);
// Don't let typing failures break the conversation
return false;
}
}
async safeReadReceipt(messageId) {
try {
const response = await client.markMessageAsRead(messageId);
if (!response.success) {
console.log('Read receipt failed:', response.error);
}
return response.success;
} catch (error) {
console.error('Read receipt error:', error);
return false;
}
}
async smartConversationFlow(message, responseFunction) {
// Try to mark as read (non-blocking)
await this.safeReadReceipt(message.id);
// Estimate response time
const estimatedTime = await this.estimateResponseTime(message);
// Show typing if we have a good estimate
if (estimatedTime > 2000) {
await this.safeTypingIndicator(
message.from,
message.id,
Math.min(estimatedTime, 25000)
);
}
// Generate response
const response = await responseFunction(message);
// Send response
await client.sendText(message.from, response);
}
}
Message Structure
Typing Indicator Message
Copy
{
messaging_product: 'whatsapp',
recipient_type: 'individual',
to: '+1234567890',
message_id: 'wamid.HBgLMTY1MDM4Nzk0MzkVAgARGBJDQjZCMzlEQUE4OTJCMTE4RTUA', // Optional
typing_indicator: {
type: 'text'
}
}
Read Receipt Message
Copy
{
messaging_product: 'whatsapp',
status: 'read',
message_id: 'wamid.HBgLMTY1MDM4Nzk0MzkVAgARGBJDQjZCMzlEQUE4OTJCMTE4RTUA'
}
Best Practices
1. Timing and Duration
Copy
// ✅ Good: Match typing duration to actual processing time
const processingTime = estimateProcessingTime(query);
await client.sendTypingIndicatorWithDuration(userPhone, processingTime, messageId);
// ✅ Good: Quick acknowledgment for simple queries
await client.sendTypingIndicator(userPhone, messageId); // 25s default
await processSimpleQuery();
// ❌ Avoid: Overly long typing for simple responses
// ❌ Avoid: No typing indicator for complex processes
2. Read Receipt Management
Copy
// ✅ Good: Mark as read immediately upon processing
webhookProcessor.onTextMessage = async (message) => {
await client.markMessageAsRead(message.id); // First thing
await processMessage(message);
};
// ✅ Good: Mark as read even if auto-responding
if (isAutoResponse(message)) {
await client.markMessageAsRead(message.id);
await sendAutoResponse(message);
}
// ❌ Avoid: Forgetting to mark important messages as read
// ❌ Avoid: Marking messages as read without processing them
3. User Experience Flow
Copy
// ✅ Good: Complete interaction flow
async function completeInteractionFlow(message) {
// 1. Acknowledge receipt
await client.markMessageAsRead(message.id);
// 2. Show activity
await client.sendTypingIndicator(message.from, message.id);
// 3. Process
const response = await processMessage(message);
// 4. Respond
await client.sendText(message.from, response);
// 5. Optional: React to show satisfaction
if (wasSuccessful(response)) {
await client.reactWithCheck(message.from, message.id);
}
}
4. Error Handling
Copy
async function robustTypingFlow(message) {
try {
await client.markMessageAsRead(message.id);
} catch (error) {
console.log('Read receipt failed, continuing...');
}
try {
await client.sendTypingIndicator(message.from, message.id);
} catch (error) {
console.log('Typing indicator failed, continuing...');
}
// Always try to send the actual response
try {
const response = await processMessage(message);
await client.sendText(message.from, response);
} catch (error) {
await client.sendText(
message.from,
'Sorry, I encountered an error processing your request. Please try again.'
);
}
}
Limitations
- Duration Limit: Typing indicators can last maximum 25 seconds
- One at a Time: Only one typing indicator per conversation at a time
- Message Age: Can only mark messages as read within 30 days
- Rate Limits: These actions count toward your messaging rate limits
- No Persistence: Typing indicators don’t persist across app restarts
Configuration Tips
Response Time Optimization
Copy
class ResponseTimeOptimizer {
constructor() {
this.averageResponseTimes = new Map();
}
async optimizeTypingDuration(messageType, userPhone) {
const userHistory = this.averageResponseTimes.get(userPhone) || {};
const avgTime = userHistory[messageType] || this.getDefaultTime(messageType);
// Add 20% buffer but cap at 25 seconds
return Math.min(avgTime * 1.2, 25000);
}
getDefaultTime(messageType) {
const defaults = {
'simple_text': 3000,
'complex_query': 15000,
'order_lookup': 20000,
'technical_support': 25000
};
return defaults[messageType] || 10000;
}
recordResponseTime(messageType, userPhone, actualTime) {
if (!this.averageResponseTimes.has(userPhone)) {
this.averageResponseTimes.set(userPhone, {});
}
const userTimes = this.averageResponseTimes.get(userPhone);
const currentAvg = userTimes[messageType] || actualTime;
// Rolling average
userTimes[messageType] = (currentAvg * 0.8) + (actualTime * 0.2);
}
}
Next Steps
Message Reactions
Add emoji reactions for quick acknowledgments
Contextual Replies
Reply to specific messages with context
Webhook System
Set up automated typing and read receipt flows
Client API Reference
Explore all available client methods