Skip to main content

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:
[Their message] ✓✓ (delivered)
[Their message] ✓✓ (read - blue checkmarks)
[Typing indicator] You are typing...

Types and Interfaces

// 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

// 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

// Mark specific message as read
async markMessageAsRead(messageId: string): Promise<TypingIndicatorResponse>

Usage Examples

Basic Typing Indicators

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

// Mark a message as read
await client.markMessageAsRead('wamid.HBgLMTY1MDM4Nzk0MzkVAgARGBJDQjZCMzlEQUE4OTJCMTE4RTUA');

Intelligent Conversation Flow

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

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

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

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

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

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

{
  messaging_product: 'whatsapp',
  recipient_type: 'individual',
  to: '+1234567890',
  message_id: 'wamid.HBgLMTY1MDM4Nzk0MzkVAgARGBJDQjZCMzlEQUE4OTJCMTE4RTUA', // Optional
  typing_indicator: {
    type: 'text'
  }
}

Read Receipt Message

{
  messaging_product: 'whatsapp',
  status: 'read',
  message_id: 'wamid.HBgLMTY1MDM4Nzk0MzkVAgARGBJDQjZCMzlEQUE4OTJCMTE4RTUA'
}

Best Practices

1. Timing and Duration

// ✅ 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

// ✅ 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

// ✅ 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

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

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