All Blog

Read Receipts: Chat API & SDK Guide for Apps 2026

12 min read
Jun 1, 2026

Read Receipts: Chat API & SDK Guide for Apps 2026

TL;DR: How Read Receipts Work and How to Build Them

  • Read receipts tell a sender that a message has been opened or seen by the recipient. They are different from delivery receipts, which only confirm that a message reached the recipient’s device or inbox.
  • A production-ready read receipts system needs four layers: message state, client events, realtime sync, and backend storage. The simplest implementation is usually through a managed chat SDK or chat API.
  • For web and JavaScript apps, you typically listen for incoming messages, render unread state, call a “mark as read” API when the conversation is visible, and subscribe to receipt updates.
  • For mobile apps, read receipts must handle background state, push notifications, offline sync, notification previews, app lifecycle changes, and privacy settings.
  • If you want read receipts without building a realtime messaging stack from scratch, Tencent RTC provides Chat SDKs for web, iOS, Android, Flutter, React Native, and more. See TRTC Chat documentation and the Free Chat API — free forever: 1,000 MAU, no concurrency limits, push notifications included.

What Are Read Receipts and Why They Matter in Modern Chat Apps

Read receipts are message status signals that show whether a recipient has read, opened, or viewed a message. In a one-to-one chat, this may appear as “Seen,” “Read,” a timestamp, or a double-check icon. In a group chat or chat room API, read receipts may show a count such as “Read by 12,” a list of readers, or per-member read status.

In modern chat apps, read receipts are not just cosmetic. They directly shape user trust, response expectations, workflow speed, and engagement. A sales representative wants to know whether a customer has seen a pricing proposal. A doctor in a telehealth app needs assurance that a patient received follow-up instructions. A multiplayer game squad needs to know whether teammates saw a strategy message before a match. A marketplace seller wants to know whether a buyer viewed shipping details.

At a technical level, read receipts are small events attached to a message or conversation. At a product level, they are part of the communication contract between users.

Tencent RTC supports this pattern through TRTC Chat, a cross-platform chat messaging SDK that can be used for private chats, group chats, customer support, social communities, in-app messaging, and realtime collaboration. Developers can use Tencent RTC Chat when they need a managed messaging layer rather than building read receipts, unread counters, push notifications, and message synchronization from scratch.

A simple definition

A read receipt is a realtime signal emitted when a user has opened a message in a conversation context where the app can reasonably infer that the message was visible.

That definition matters because “read” is not always literal. If a push notification preview shows the message on a lock screen, should it count as read? If a user opens a chat list but not the thread, is it read? If a group member scrolls past 200 messages, should all 200 be marked as read? These are product decisions that your chat api and client implementation must support consistently.

Why read receipts improve product experience

Read receipts reduce uncertainty. Messaging is asynchronous, but users still want feedback. Without read receipts, senders often wonder whether a message was lost, ignored, or simply not opened yet.

Common benefits include:

Use caseWhy read receipts matter
Social chatCreates familiar messaging behavior and reduces uncertainty
Customer supportHelps agents prioritize follow-up when customers have viewed instructions
Team collaborationConfirms whether operational updates were seen
Marketplace chatHelps buyers and sellers coordinate faster
Healthcare and educationConfirms that sensitive instructions were opened
Live commerceLets hosts and moderators understand participant engagement
GamingConfirms that team instructions or room invitations were seen

For apps that combine text chat with calling or live interactions, read receipts can also work alongside voice and video features. For example, a tutoring app might use TRTC Call for live lessons and Chat for lesson materials, homework links, and post-call summaries.

Why not build read receipts manually?

You can build read receipts yourself with WebSockets, a database, and device-specific push notification logic. The WebSocket protocol is standardized by the IETF in RFC 6455, and browser APIs are documented by MDN Web Docs. However, the hard part is not sending one event. The hard part is making message state correct across:

  • Multiple devices per user
  • Offline users
  • Group conversations
  • Message history pagination
  • Push notifications
  • App reinstall and token refresh
  • Network reconnection
  • Moderation and compliance requirements
  • Privacy preferences
  • High-volume chat rooms

A managed realtime chat api reduces this complexity. With TRTC Chat, the SDK handles connection management, message delivery, conversation state, unread counts, and platform differences. Developers can focus on product logic and UX.

For complete API reference and platform-specific guides, see the TRTC Chat SDK documentation.

Read Receipts vs Delivery Receipts vs Typing Indicators

Read receipts are often confused with delivery receipts and typing indicators. They are related, but they answer different questions.

Message state comparison

SignalQuestion answeredTypical UITrigger
SentDid my app submit the message?Single check or “Sent”Client successfully sends request
DeliveredDid the recipient’s account/device receive it?Double check or “Delivered”Server or recipient client acknowledges delivery
ReadDid the recipient open or view it?“Read,” “Seen,” avatar, timestampRecipient opens conversation or visible message range
TypingIs the recipient composing a reply?“Maya is typing…”Client sends transient typing event
Unread countHow many messages are unseen?Badge countServer or client calculates unread state

Delivery receipts

A delivery receipt confirms that a message reached the recipient’s message stream, inbox, device, or account. In some systems, “delivered” means the server accepted the message for that recipient. In stricter systems, it means at least one recipient device acknowledged receipt.

Delivery receipts are useful for diagnosing connectivity and push delivery. They are not proof that a human saw the message.

Read receipts

Read receipts confirm that the recipient opened the conversation or viewed a message range. For one-to-one chats, a common implementation is conversation-level read state: the recipient marks the conversation as read up to the latest message sequence. For group chats, the system may store per-message read members or per-member last-read sequence numbers.

Conversation-level read state is usually more scalable. Instead of writing one database row per user per message, you store each member’s last read message ID or sequence number.

Typing indicators

Typing indicators are ephemeral presence events. They should not be stored like read receipts. A typing event usually expires after a few seconds and should not be replayed from message history.

According to the W3C Web Notifications specification, notification systems are separate from in-app state. That distinction is important: viewing a notification does not necessarily mean reading a message inside the app. Your read receipts policy should define whether notification interactions mark a conversation as read.

For most apps, use this message lifecycle:

  1. Sending: message is created locally and queued.
  2. Sent: chat api accepts the message.
  3. Delivered: message is available to the recipient account or device.
  4. Read: recipient opens the conversation and the app marks visible messages as read.
  5. Archived or deleted: optional product-specific state.

Avoid making read receipts too granular unless your product requires it. Per-message read tracking in large groups can become expensive. A conversation-level “last read message” model is often enough.

Core Architecture: How Read Receipts Work in a Realtime Chat API

A reliable read receipts architecture has six components: user identity, message IDs, conversation state, realtime events, durable storage, and client rendering.

Architecture overview

Sender Client
   |
   | 1. sendMessage()
   v
Chat API / Messaging Server
   |
   | 2. persist message and assign message ID / sequence
   v
Recipient Client(s)
   |
   | 3. user opens conversation
   | 4. markMessageAsRead(conversationID)
   v
Chat API / Messaging Server
   |
   | 5. update lastRead sequence
   | 6. emit read receipt event
   v
Sender Client updates UI

Core entities

EntityDescriptionExample
UserAuthenticated chat participantuser_123
ConversationOne-to-one, group, room, or channelC2C_user_456
MessageImmutable chat item with ID and timestampmsg_789
Message sequenceMonotonic order valueseq=1042
Read cursorLast message read by a memberlastReadSeq=1042
Receipt eventRealtime event sent to other clientsconversationRead
Unread countDerived from message sequence and read cursorunread=5

The read cursor pattern

The read cursor is the most important concept in read receipts. Instead of storing a boolean read=true for every message, store a cursor that says:

User A has read conversation C up to message sequence N.

This model has several advantages:

  • Fewer writes
  • Faster unread count calculation
  • Easier multi-device sync
  • Better performance in large conversations
  • Cleaner pagination behavior

For one-to-one chat, the sender can show “Read” when the recipient’s read cursor is greater than or equal to the message sequence. For group chat, the app can calculate how many members have read up to a message.

Client-side event flow

A robust client should not mark messages as read the moment they arrive. It should mark them as read only when the conversation is actively visible.

A practical rule:

  • If the user is inside the conversation, the app is foregrounded, and the message list is visible, mark incoming messages as read.
  • If the app is backgrounded, only update unread count.
  • If a push notification is tapped and opens the conversation, mark the relevant conversation as read.
  • If the user only sees a notification preview, do not mark it as read unless your product explicitly chooses that behavior.

Realtime transport choices

Most realtime chat systems use persistent connections such as WebSocket, long polling fallback, or platform-specific push channels. WebRTC is often used for audio/video media, while WebSocket-style messaging is common for chat control and events. The WebRTC project documents the media and realtime communication stack used by many voice and video applications, but chat message state still typically requires application-level APIs.

TRTC combines realtime communication capabilities across chat, call, live, and conference scenarios. If your app needs messaging plus voice or video, you can pair TRTC Chat with TRTC Call documentation for calling or TRTC Live documentation for live streaming.

Group chat and chat room API considerations

Read receipts become more complex in group chat. You have three common models:

ModelBest forTradeoff
No group read receiptsLarge public roomsSimple and scalable, less feedback
Read count onlyCommunities, classroomsGood privacy, moderate complexity
Full reader listTeams, healthcare, enterpriseMaximum clarity, more storage and privacy work

For very large live chat rooms, full per-message read receipts are usually unnecessary. A read count or unread marker is more practical. For small teams or high-trust enterprise apps, full reader lists can be valuable.

How to Implement Read Receipts with a Chat SDK for Web and JavaScript

This section shows how to implement read receipts using a JavaScript chat sdk web approach. The example uses the Tencent Cloud Chat SDK package and demonstrates initialization, login, message loading, sending messages, marking a conversation as read, and cleanup.

Before you start, create a project and obtain your SDKAppID and UserSig from the TRTC console. You can download SDKs and platform resources from TRTC SDK downloads.

Web implementation checklist

For a web or JavaScript chat api implementation, you need to:

  1. Initialize the chat SDK.
  2. Log in the current user.
  3. Subscribe to message events.
  4. Load message history for the selected conversation.
  5. Mark the conversation as read when the thread is visible.
  6. Update UI when read receipt events or conversation updates arrive.
  7. Log out and remove listeners when the app closes.

Runnable JavaScript example

Install the SDK:

npm install @tencentcloud/chat

Create chat-read-receipts.js:

import TencentCloudChat from '@tencentcloud/chat';

const SDKAppID = Number(import.meta.env.VITE_TRTC_SDK_APP_ID);
const userID = import.meta.env.VITE_TRTC_USER_ID;
const userSig = import.meta.env.VITE_TRTC_USER_SIG;

// For one-to-one chat, Tencent Cloud Chat conversation IDs commonly use C2C + userID.
const peerUserID = 'user_b';
const conversationID = `C2C${peerUserID}`;

let chat;

function renderMessage(message, currentUserID) {
  const mine = message.from === currentUserID;
  const text = message.payload?.text || '';
  const status = mine ? `status=${message.status || 'sent'}` : '';

  console.log(`${mine ? 'Me' : message.from}: ${text} ${status}`);
}

async function initChat() {
  chat = TencentCloudChat.create({ SDKAppID });

  chat.setLogLevel(1);

  chat.on(TencentCloudChat.EVENT.SDK_READY, async () => {
    console.log('Chat SDK ready');

    const history = await chat.getMessageList({
      conversationID,
      count: 15
    });

    history.data.messageList.forEach((message) => {
      renderMessage(message, userID);
    });

    // Mark the conversation as read only after the user opens the thread.
    await markConversationAsRead();
  });

  chat.on(TencentCloudChat.EVENT.MESSAGE_RECEIVED, async (event) => {
    const messages = event.data || [];

    for (const message of messages) {
      renderMessage(message, userID);

      if (message.conversationID === conversationID && document.visibilityState === 'visible') {
        await markConversationAsRead();
      }
    }
  });

  chat.on(TencentCloudChat.EVENT.CONVERSATION_LIST_UPDATED, (event) => {
    const conversations = event.data || [];
    const current = conversations.find((item) => item.conversationID === conversationID);

    if (current) {
      console.log('Unread count:', current.unreadCount);
      console.log('Last message:', current.lastMessage?.messageForShow);
    }
  });

  await chat.login({ userID, userSig });
}

async function sendText(text) {
  const message = chat.createTextMessage({
    to: peerUserID,
    conversationType: TencentCloudChat.TYPES.CONV_C2C,
    payload: { text }
  });

  const result = await chat.sendMessage(message);
  renderMessage(result.data.message, userID);
}

async function markConversationAsRead() {
  try {
    await chat.setMessageRead({ conversationID });
    console.log(`Marked ${conversationID} as read`);
  } catch (error) {
    console.error('Failed to mark as read:', error);
  }
}

async function destroyChat() {
  if (!chat) return;

  await chat.logout();
  chat.destroy();
  chat = null;
}

document.addEventListener('visibilitychange', async () => {
  if (document.visibilityState === 'visible' && chat) {
    await markConversationAsRead();
  }
});

window.demoChat = {
  initChat,
  sendText,
  markConversationAsRead,
  destroyChat
};

Use it in a Vite app:

<button onclick="demoChat.initChat()">Login</button>
<button onclick="demoChat.sendText('Hello with read receipts')">Send</button>
<button onclick="demoChat.markConversationAsRead()">Mark Read</button>
<button onclick="demoChat.destroyChat()">Logout</button>

This example is intentionally simple. In production, you should generate UserSig on your backend, not in the browser. You should also debounce setMessageRead() to avoid calling it repeatedly when multiple messages arrive quickly.

React pattern for read receipts

In React, place the markConversationAsRead() call inside an effect that runs when:

  • The selected conversation changes
  • The SDK is ready
  • The browser tab becomes visible
  • New messages arrive while the thread is active

A typical React hook structure:

import { useEffect } from 'react';

export function useReadReceipts({ chat, conversationID, isThreadVisible }) {
  useEffect(() => {
    if (!chat || !conversationID || !isThreadVisible) return;

    let cancelled = false;

    async function markRead() {
      if (cancelled) return;
      try {
        await chat.setMessageRead({ conversationID });
      } catch (error) {
        console.error('mark read failed', error);
      }
    }

    markRead();

    const onVisibilityChange = () => {
      if (document.visibilityState === 'visible') {
        markRead();
      }
    };

    document.addEventListener('visibilitychange', onVisibilityChange);

    return () => {
      cancelled = true;
      document.removeEventListener('visibilitychange', onVisibilityChange);
    };
  }, [chat, conversationID, isThreadVisible]);
}

This hook is compatible with a javascript chat sdk architecture and can be adapted to Vue, Angular, Svelte, or plain JavaScript.

Web UX details

On web, avoid marking messages as read when:

  • The conversation is open in a hidden browser tab
  • The message list is behind a modal
  • The user is viewing a different thread
  • The app is disconnected and showing stale content

A strong implementation uses both visibility state and route state. For example, document.visibilityState === 'visible' and activeConversationID === message.conversationID.

Mobile Implementation Patterns: Flutter, Swift, React Native, and Android

Mobile read receipts require more lifecycle awareness than web. Phones switch between foreground, background, locked screen, push notification view, split screen, and network transitions. A good chat api for mobile app must handle all of these without creating false read receipts.

Mobile read receipt rules

Use these rules as defaults:

SituationMark as read?Reason
User opens conversation screenYesMessage is visible
User receives push notificationNoNotification does not prove reading
User taps notification into conversationYesUser entered thread
App is foregrounded but user is on chat listNoThread not visible
User scrolls to a message rangeSometimesUse visible range for strict read logic
App resumes to an open conversationYesConversation is visible again
User disables read receiptsNoRespect privacy setting

Flutter chat SDK implementation

A Flutter chat sdk should initialize once, log in the user, add message listeners, and mark messages as read when the conversation screen appears.

Add the dependency in pubspec.yaml:

dependencies:
  flutter:
    sdk: flutter
  tencent_cloud_chat_sdk: ^8.0.0

Create a conversation screen service:

import 'package:flutter/material.dart';
import 'package:tencent_cloud_chat_sdk/tencent_cloud_chat_sdk.dart';
import 'package:tencent_cloud_chat_sdk/enum/V2TimConversationType.dart';
import 'package:tencent_cloud_chat_sdk/enum/log_level_enum.dart';
import 'package:tencent_cloud_chat_sdk/models/v2_tim_callback.dart';
import 'package:tencent_cloud_chat_sdk/models/v2_tim_message.dart';
import 'package:tencent_cloud_chat_sdk/models/v2_tim_value_callback.dart';

class ChatReadReceiptService {
  final int sdkAppID;
  final String userID;
  final String userSig;
  final String peerUserID;

  ChatReadReceiptService({
    required this.sdkAppID,
    required this.userID,
    required this.userSig,
    required this.peerUserID,
  });

  Future<void> init() async {
    await TencentImSDKPlugin.v2TIMManager.initSDK(
      sdkAppID: sdkAppID,
      loglevel: LogLevelEnum.V2TIM_LOG_INFO,
      listener: V2TimSDKListener(
        onConnectSuccess: () => debugPrint('Chat connected'),
        onKickedOffline: () => debugPrint('Kicked offline'),
        onUserSigExpired: () => debugPrint('UserSig expired'),
      ),
    );

    await TencentImSDKPlugin.v2TIMManager.login(
      userID: userID,
      userSig: userSig,
    );

    TencentImSDKPlugin.v2TIMManager
        .getMessageManager()
        .addAdvancedMsgListener(
          listener: V2TimAdvancedMsgListener(
            onRecvNewMessage: (V2TimMessage message) async {
              debugPrint('New message: ${message.msgID}');
              if (message.userID == peerUserID) {
                await markC2CAsRead();
              }
            },
          ),
        );
  }

  Future<List<V2TimMessage>> loadHistory() async {
    V2TimValueCallback<List<V2TimMessage>> result =
        await TencentImSDKPlugin.v2TIMManager
            .getMessageManager()
            .getC2CHistoryMessageList(
              userID: peerUserID,
              count: 20,
            );

    return result.data ?? [];
  }

  Future<void> sendText(String text) async {
    final createResult = await TencentImSDKPlugin.v2TIMManager
        .getMessageManager()
        .createTextMessage(text: text);

    final messageID = createResult.data?.id;
    if (messageID == null) {
      throw Exception('Failed to create text message');
    }

    await TencentImSDKPlugin.v2TIMManager.getMessageManager().sendMessage(
          id: messageID,
          receiver: peerUserID,
          groupID: '',
          priority: 0,
          onlineUserOnly: false,
          isExcludedFromUnreadCount: false,
          isExcludedFromLastMessage: false,
        );
  }

  Future<void> markC2CAsRead() async {
    V2TimCallback result = await TencentImSDKPlugin.v2TIMManager
        .getMessageManager()
        .markC2CMessageAsRead(userID: peerUserID);

    if (result.code == 0) {
      debugPrint('Marked C2C conversation as read');
    } else {
      debugPrint('Mark read failed: ${result.desc}');
    }
  }

  Future<void> dispose() async {
    await TencentImSDKPlugin.v2TIMManager.logout();
    TencentImSDKPlugin.v2TIMManager.unInitSDK();
  }
}

Use it in a StatefulWidget:

@override
void initState() {
  super.initState();
  service.init().then((_) async {
    await service.loadHistory();
    await service.markC2CAsRead();
  });
}

@override
void didChangeAppLifecycleState(AppLifecycleState state) {
  if (state == AppLifecycleState.resumed) {
    service.markC2CAsRead();
  }
}

This pattern avoids marking messages as read while the app is only receiving background push notifications.

Swift implementation pattern

For a chat sdk swift implementation, the same principles apply:

  1. Initialize the SDK in app startup.
  2. Log in after your backend returns a user token or UserSig.
  3. Add an advanced message listener.
  4. Mark C2C or group messages as read in viewDidAppear.
  5. Avoid marking as read from push notification receipt alone.

A simplified Swift-style flow:

import UIKit
import ImSDK_Plus

final class ChatViewController: UIViewController, V2TIMAdvancedMsgListener {
    let peerUserID = "user_b"

    override func viewDidLoad() {
        super.viewDidLoad()
        V2TIMManager.sharedInstance().addAdvancedMsgListener(listener: self)
        loadMessages()
    }

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        markAsRead()
    }

    func loadMessages() {
        V2TIMManager.sharedInstance().getC2CHistoryMessageList(
            userID: peerUserID,
            count: 20,
            lastMsg: nil,
            succ: { messages in
                print("Loaded messages: \(messages?.count ?? 0)")
            },
            fail: { code, desc in
                print("Load failed: \(code), \(desc ?? "")")
            }
        )
    }

    func onRecvNewMessage(msg: V2TIMMessage!) {
        if msg.userID == peerUserID && view.window != nil {
            markAsRead()
        }
    }

    func markAsRead() {
        V2TIMManager.sharedInstance().markC2CMessage(asRead: peerUserID, succ: {
            print("Marked as read")
        }, fail: { code, desc in
            print("Mark read failed: \(code), \(desc ?? "")")
        })
    }

    deinit {
        V2TIMManager.sharedInstance().removeAdvancedMsgListener(listener: self)
    }
}

For production iOS apps, connect this with UIApplication lifecycle callbacks so that read receipts are emitted only when the conversation is visible and the app is active.

React Native chat api pattern

A react native chat api implementation usually mirrors Flutter:

  • Initialize in a top-level provider
  • Store active conversation in state
  • Mark as read on screen focus
  • Use AppState to detect foreground/background
  • Debounce read calls
  • Keep push notification handling separate

If you use React Navigation, call markAsRead() inside useFocusEffect. If the user opens a notification, navigate to the conversation screen first, then mark the thread as read after the screen is mounted.

Android implementation pattern

For Android, combine:

  • Activity or Fragment lifecycle
  • ViewModel state
  • Message listener
  • Push notification click intent
  • onResume() mark-read logic

For the best chat api for android, look for SDK support for offline message sync, unread count, push integration, and group conversation read state. Android apps are often killed and restarted by the OS, so your implementation should recover from cached conversation state and request fresh unread counts after reconnect.

Voice chat api and read receipts

A voice chat api is not the same as a text chat api, but many social, gaming, and community apps need both. For example, a multiplayer game may use Tencent RTC GVoice for in-game voice and Chat for text channels, room invitations, and read receipts. This separation keeps media transport and message state optimized for their own workloads.

Backend Considerations: Laravel Chat API, Message States, and Data Storage

Even when you use a managed chat messaging SDK, your backend still matters. Your server usually owns authentication, user profiles, business permissions, audit logs, and sometimes message metadata.

What your backend should own

Backend responsibilityWhy it matters
User authenticationPrevents impersonation
UserSig or token generationKeeps secrets out of clients
Role and permission checksControls who can join conversations
Conversation metadataSupports business workflows
Webhooks or callbacksSyncs message events to your system
Compliance logsRequired in regulated industries
Privacy preferencesControls whether read receipts are enabled

Never expose secret keys in client-side JavaScript, mobile apps, or public repositories. Generate short-lived credentials on your backend.

Laravel chat api example for secure UserSig distribution

A laravel chat api often acts as a thin authentication layer in front of the chat sdk. The frontend asks your backend for a token. The backend verifies the logged-in user and returns a UserSig.

This example shows the structure. Replace buildUserSig() with your official server-side UserSig generation implementation.

<?php

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\Auth;

Route::middleware('auth:sanctum')->get('/chat/credential', function (Request $request) {
    $user = Auth::user();

    $sdkAppId = (int) config('services.trtc.sdk_app_id');
    $secretKey = config('services.trtc.secret_key');

    return response()->json([
        'sdkAppID' => $sdkAppId,
        'userID' => (string) $user->id,
        'userSig' => buildUserSig($sdkAppId, $secretKey, (string) $user->id),
        'expiresIn' => 3600,
    ]);
});

function buildUserSig(int $sdkAppId, string $secretKey, string $userId): string
{
    // Use Tencent RTC's official UserSig generation library in production.
    // This placeholder intentionally avoids exposing signing internals.
    // Keep SECRET_KEY only on the server.
    return app(\App\Services\TencentUserSigService::class)
        ->generate($sdkAppId, $secretKey, $userId, 3600);
}

Then your JavaScript app calls:

async function fetchChatCredential() {
  const response = await fetch('/api/chat/credential', {
    headers: {
      Authorization: `Bearer ${localStorage.getItem('app_token')}`
    }
  });

  if (!response.ok) {
    throw new Error('Failed to fetch chat credential');
  }

  return response.json();
}

This backend pattern works for web, Flutter, Swift, React Native, and Android clients.

Database design if you store read state yourself

If your product requires custom analytics or compliance records, you may store your own read state in addition to the managed chat api. A scalable schema uses conversation cursors.

Example tables:

CREATE TABLE conversation_members (
  conversation_id VARCHAR(128) NOT NULL,
  user_id VARCHAR(128) NOT NULL,
  last_read_message_id VARCHAR(128),
  last_read_seq BIGINT DEFAULT 0,
  last_read_at TIMESTAMP NULL,
  read_receipts_enabled BOOLEAN DEFAULT TRUE,
  PRIMARY KEY (conversation_id, user_id)
);

CREATE TABLE message_audit_log (
  message_id VARCHAR(128) PRIMARY KEY,
  conversation_id VARCHAR(128) NOT NULL,
  sender_id VARCHAR(128) NOT NULL,
  message_seq BIGINT NOT NULL,
  sent_at TIMESTAMP NOT NULL
);

For most apps, avoid writing a row for every message-reader pair. In a group with 10,000 members, a single message could create 10,000 receipt rows. That model becomes expensive quickly.

Message states and idempotency

Read receipt APIs should be idempotent. If a client calls markAsRead(conversationID, seq=100) five times, the final state should still be lastReadSeq=100. If a later call marks seq=120, do not allow an older retry to move the cursor backward to 100.

Use monotonic updates:

UPDATE conversation_members
SET last_read_seq = GREATEST(last_read_seq, :new_seq),
    last_read_at = CASE
      WHEN :new_seq >= last_read_seq THEN NOW()
      ELSE last_read_at
    END
WHERE conversation_id = :conversation_id
  AND user_id = :user_id;

Webhooks and analytics

If your chat provider supports webhooks or server callbacks, you can record read receipt events for analytics. Useful metrics include:

  • Average time to read
  • Read rate by conversation type
  • Support agent response after customer read
  • Drop-off after unread messages
  • Push notification open-to-read conversion

Be careful with privacy. Read behavior can be sensitive personal data, especially in healthcare, workplace, education, and finance apps. The GDPR text published by the European Union defines principles around personal data processing that may apply depending on your users and region. Consult legal counsel for compliance requirements.

UX, Privacy, and Performance Best Practices for Read Receipts

Read receipts are powerful, but they can also create pressure. A good implementation balances transparency, user control, and performance.

UX best practices

Use clear labels
“Read” and “Delivered” should mean different things. Do not use a read icon for delivery.

Show timestamps when helpful
In business messaging, “Read at 14:03” can be useful. In casual chat, a subtle icon may be enough.

Avoid noisy UI in groups
For large groups, show a count instead of a long reader list.

Handle offline state gracefully
If a user is offline, show “Sent” or “Delivered” based on actual server state. Do not guess.

Make unread markers obvious
A “New messages” divider helps users understand what was unread since their last visit.

Do not mark hidden content as read
If messages are loaded below the fold, decide whether opening the thread is enough or whether visibility tracking is required.

Privacy best practices

Read receipts can expose behavior. Some users do not want others to know when they read a message. This is especially important in dating apps, mental health apps, workplace messaging, and communities involving sensitive topics.

Recommended privacy controls:

ControlRecommendation
Disable read receiptsOffer user-level setting when appropriate
One-to-one visibilityLet users hide read status from individual chats
Group visibilityPrefer read counts over reader lists in large groups
Admin overrideUse only for compliance-heavy enterprise scenarios
Notification previewsDo not mark as read by default
Data retentionDo not keep read logs longer than needed

If your app offers a setting to disable read receipts, define whether it is reciprocal. Many consumer chat apps make it reciprocal: if you turn off your read receipts, you also cannot see others’ read receipts.

Performance best practices

Read receipts can create event storms. Imagine a chat room where 5,000 users open the same conversation at once. If every client emits per-message read events, your system can overload.

Use these performance patterns:

  • Debounce mark-read calls: wait 300–1000 ms before sending.
  • Use read cursors: update last read sequence, not every message.
  • Batch updates: mark a range as read instead of one message at a time.
  • Avoid full reader lists in large groups: use counts or disable group receipts.
  • Cache unread counts: avoid recalculating on every page render.
  • Use pagination: load message history in chunks.
  • Handle reconnects: sync read state after network recovery.

Common mistakes and fixes

MistakeImpactFix
Marking as read on push receiptFalse read receiptsMark only when user opens conversation
Storing one row per user per messageHigh database costStore per-member read cursor
Calling mark-read for every incoming messageExcess API callsDebounce and batch
Ignoring multi-device usersInconsistent stateSync read cursor across devices
No privacy settingUser trust issueAdd user or conversation-level control
Moving read cursor backwardIncorrect unread countUse monotonic updates
Treating typing as persistentStale UIExpire typing indicators quickly

Accessibility considerations

Read receipt UI should not rely only on color. A gray check and blue check may be hard to distinguish. Use text labels or accessible names such as aria-label="Read" in web apps. Screen reader users should be able to understand message state without visual icons.

Security considerations

Secure chat implementation includes more than read receipts. Protect credentials, validate permissions, and avoid leaking message metadata to unauthorized users. If you are building an api for chat application, your backend should verify that a user belongs to a conversation before returning message history or read status.

For apps that require realtime audio/video alongside chat, review TRTC Conference documentation for meeting scenarios and TRTC Conversational AI documentation if you are building AI voice interactions.

Pro Tip: If you're building AI-powered voice interactions, Tencent RTC's Conversational AI provides low-latency speech interaction capabilities for natural conversations.

Choosing a Free Chat SDK or Chat API with Read Receipts Support

Choosing the best chat api is not only about read receipts. You need to evaluate platform coverage, scalability, moderation, push notifications, pricing, SDK quality, and how fast your team can ship.

Selection matrix

RequirementWhy it mattersWhat to look for
Read receiptsCore messaging feedbackConversation read state, group read support
Delivery receiptsMessage reliabilitySent, delivered, failed states
Unread countsNavigation and engagementConversation list unread counters
Push notificationsMobile re-engagementAPNs, FCM, vendor push integrations
Web SDKBrowser appschat sdk web, JavaScript support
Mobile SDKsNative appsiOS, Android, Flutter, React Native
Group chatCommunities and teamsMember management, roles, mute, moderation
Chat room APILive eventsHigh concurrency and message control
Backend APIsBusiness integrationREST APIs, callbacks, webhooks
SecurityUser trustToken-based login, permissions
PricingLong-term costFree tier and predictable scaling
DocumentationDeveloper speedClear guides and runnable demos

Tencent RTC is a strong option when you need a cross-platform in app chat sdk with room to expand into calling, live streaming, conference, AI, or gaming voice. Start with TRTC Chat for messaging, use TRTC Call for 1v1 and group calls, add TRTC Live for interactive live streaming, and use GVoice for gaming voice scenarios.

Free Chat API — free forever: 1,000 MAU, no concurrency limits, push notifications included.

Free chat api evaluation checklist

Before choosing a free chat sdk, ask:

  • Does the free plan include read receipts or only basic messaging?
  • Are push notifications included?
  • Are there concurrency limits?
  • Which platforms are supported?
  • Can I use the same chat api javascript implementation on web and backend-assisted apps?
  • Does the SDK support Flutter, Swift, Android, and React Native?
  • Is group chat supported?
  • Is there a chat room api for live communities?
  • Can I export or audit message events?
  • How are credentials generated and protected?
  • Is documentation complete enough for my team?

TRTC’s Free Chat API is useful for early-stage apps because it is not merely a short trial. It supports a free-forever model with 1,000 MAU, no concurrency limits, and push notifications included. That makes it practical for prototypes, MVPs, internal tools, student projects, and early production launches.

When to build yourself

Building your own realtime chat api may make sense if:

  • Messaging is not central to your product
  • You only need basic comments or notifications
  • Your team already operates realtime infrastructure
  • You need an unusual protocol or data model
  • You have strict internal platform constraints

However, for most product teams, a managed chat messaging SDK is faster and safer. Read receipts seem simple at first, but the edge cases multiply as soon as you add multiple devices, offline sync, push, group chats, and privacy controls.

Accelerate Integration with MCP

Instead of reading documentation page by page, use Tencent RTC's MCP server to let your AI coding assistant generate integration code directly:

Setup (Cursor / VS Code / Claude Code):

{
  "mcpServers": {
    "tencent-rtc": {
      "command": "npx",
      "args": ["-y", "@tencent-rtc/mcp@latest"],
      "env": {
        "SDKAPPID": "YOUR_SDKAPPID",
        "SECRETKEY": "YOUR_SECRET_KEY"
      }
    }
  }
}

Example prompts you can use:

  • "Create a video calling app using TRTC Web SDK with Vue 3"
  • "Integrate real-time chat into my React app with message history and read receipts"
  • "Add live streaming to my existing Express backend"
  • "Generate a Flutter chat screen that marks C2C messages as read on view focus"
  • "Create a Laravel endpoint that returns TRTC Chat credentials securely"

The MCP server has access to TRTC SDK documentation and can generate working code with your credentials pre-filled. For the full MCP setup guide, see the official MCP documentation.

💡 Pro Tip for AI-assisted development: If you use Cursor or CodeBuddy, the TRTC MCP server (@tencent-rtc/mcp) can scaffold your real-time communication layer in minutes — from project setup to credential handling to working chat and video calls.

Implementation Checklist for Production Read Receipts

Use this checklist before launching read receipts in production.

Product checklist

  • Define what “read” means in your app.
  • Decide whether notification previews count as read.
  • Decide whether users can disable read receipts.
  • Define one-to-one behavior separately from group behavior.
  • Choose between read count and reader list for groups.
  • Add accessible labels for read states.
  • Document behavior in privacy policy if needed.

Engineering checklist

  • Use stable message IDs or sequence numbers.
  • Store read cursor per conversation member.
  • Make mark-read idempotent.
  • Prevent read cursor rollback.
  • Debounce client read calls.
  • Sync read state across devices.
  • Handle offline and reconnect flows.
  • Keep credentials on backend.
  • Test with slow networks and background app state.
  • Monitor read event volume.

QA scenarios

ScenarioExpected behavior
User opens a one-to-one chatConversation unread count becomes zero
User receives message in backgroundUnread count increases, no read receipt
User taps push notificationApp opens thread, then marks as read
User opens same account on two devicesRead cursor syncs across devices
User disables read receiptsApp stops publishing read state
Network disconnects during mark-readApp retries safely without duplicate state
Group has 1,000 membersUI uses count or scalable model
Sender deletes a messageRead state does not break message list

FAQ About Read Receipts and Chat APIs

What are read receipts in a chat app?

Read receipts are status signals that tell a sender when a recipient has opened or viewed a message. They usually appear as “Read,” “Seen,” a timestamp, an avatar, or a read count in group chats.

Are read receipts the same as delivery receipts?

No. Delivery receipts confirm that a message reached the recipient’s account, device, or inbox. Read receipts confirm that the recipient opened the conversation or viewed the message according to your app’s rules.

What is the best way to implement read receipts?

The most scalable approach is to use a read cursor. Store the latest message sequence each user has read in each conversation. Then calculate whether specific messages are read based on that cursor. A managed chat sdk can provide this behavior without requiring you to build realtime infrastructure.

Can I implement read receipts with a JavaScript chat SDK?

Yes. With a javascript chat sdk, initialize the SDK, log in the user, load message history, listen for new messages, and call the SDK’s mark-read method when the conversation is visible. The example above shows this pattern using Tencent Cloud Chat.

How do read receipts work in Flutter?

In Flutter, initialize the chat SDK, add a message listener, and call markC2CMessageAsRead or the relevant group read API when the conversation screen appears or resumes. Do not mark messages as read only because a push notification arrived.

Should group chats show everyone who read a message?

It depends on the product. Small enterprise groups may benefit from full reader lists. Large communities and live chat rooms should usually show read counts or avoid group read receipts to reduce storage, event volume, and privacy concerns.

Do read receipts create privacy risks?

Yes. Read receipts can reveal user behavior and availability. Consider adding a setting to disable them, avoid marking notification previews as read, and minimize how long you store read event logs.

Is there a free chat API that supports read receipts?

Yes. Tencent RTC offers a Free Chat API that is free forever with 1,000 MAU, no concurrency limits, and push notifications included. It is a practical starting point for apps that need messaging, unread counts, and read receipt behavior.

Conclusion: Build Read Receipts That Users Trust

Read receipts are small signals with a large impact. They help users understand whether messages were seen, reduce uncertainty, and make chat apps feel responsive. But they must be implemented carefully. A production-grade read receipts system needs clear product rules, correct message state, realtime updates, privacy controls, and mobile lifecycle handling.

If you are building from scratch, use read cursors, idempotent updates, debounced client calls, and strict backend permission checks. If you want to ship faster, use a managed chat api with cross-platform SDKs, unread counts, push notifications, and read state built in.

To start building, explore Tencent RTC Chat, review the TRTC Chat SDK documentation, and try the Free Chat API for a free-forever messaging foundation. For apps that need communication beyond text, combine Chat with TRTC Call, TRTC Live, or Tencent RTC GVoice.

Author

Maya Chen is a developer content strategist focused on realtime communication, chat infrastructure, and SDK integration patterns. She writes practical engineering guides for teams building messaging, voice, video, and live interactive applications.