Implement message burn-after-reading and timed destruct
Feature overview
Burn-after-reading is a signature capability for secret chat and privacy-focused communication scenarios. Sensitive content does not stay in the chat history permanently; it is automatically deleted from both parties' conversation records upon expiration or after being read. Timed destruct means that messages are automatically deleted from both parties' conversation records after a specified time, regardless of whether they have been read.
| Requirement | Implementation | Description |
|---|---|---|
| Disappears only after the other party reads it (strict burn-after-reading) | Burn-after-reading (combined implementation) | Timed destruct has no "disappears after being read" semantics; using it alone also destructs unread messages |
| Disappears after a fixed time since being sent (unread messages included) | Timed destruct (native) | Lowest implementation cost; it is recommended to provide a "timed destruct" hint in the product UI |
| Both semantics are required (common in secret chat) | Burn-after-reading first | Timed destruct can be added as a fallback for unread scenarios; the two capabilities can be enabled at the same time without conflict |
Implementation
Conversation-level timed message destruct (native capability)
Conversation-level timed message destruct is a new capability added in 3.1.0 and requires the Enterprise plan. It is suitable for scenarios such as time-limited messages and confidential communication. After login, call setConversationMessageDestructDuration to set it. Set duration to 0 (cancel timed destruct) or 60 ~ 604800 seconds (7 days at most);
If you need a minimum destruct duration of 30 seconds, please contact ZEGOCLOUD Technical Support for configuration.
The setting takes effect on both the local end and the peer end of a one-on-one chat at the same time, and on all members of a group chat at the same time. Before calling, for a one-on-one chat, the user corresponding to conversationID must have been registered; for a group chat, the group must have been created and the current user must have joined it.
After the setting succeeds, messages sent afterwards carry the expiration timestamp destructTime (in milliseconds; 0 means a non-time-limited message);
Duration changes are perceived through onConversationChanged. ZIMConversation.messageDestructDuration indicates the current duration (0 if not set). When a message expires, onMessageDeleted is triggered (with the deletion type MessagesDestructed), and the client removes the message from the message list accordingly.
The duration setting only takes effect for messages sent afterwards; historical messages are not affected.
Key point: The countdown starts from when the message is sent. Even if the recipient never opens the conversation, the message is automatically destructed upon expiration. For details, refer to Set conversation message timed destruct.
In versions earlier than 3.1.0:
- The client can still receive and display the message before it expires. After expiration, if the message is already stored locally on the client, the client does not delete it automatically (local residue).
- If the client has not pulled the message before it expires, the message can no longer be obtained from the server after expiration.
Therefore, it is recommended to apply a minimum version gate (3.1.0+) to conversations using timed destruct. If the requirement is not met, block the enabling of timed destruct and guide users to upgrade the client.
Strict burn-after-reading (combined implementation)
If the product defines that a message must "disappear only after the other party reads it", ZIM can implement burn-after-reading by combining "message receipt read time + message deletion":
- Message extended data: extendedData (visible to the peer end) carries the "burn flag" so that the receiving end can identify that the message requires burn-after-reading.
- Message receipts form the read notification link (enable hasReceipt when sending; the recipient calls sendMessageReceiptsRead after viewing).
- Delete messages:
deleteMessages (isAlsoDeleteServerMessage = true)deletes the message records on both the local and server sides.
Message sending flow:
- Send: The sender calls sendMessage (extendedData carries the burn flag, hasReceipt = true) and writes the messageID to the local burn message table;
- Receive and read: The recipient parses extendedData, renders the burn style, and writes it to the local burn table; after viewing, the recipient calls sendMessageReceiptsRead (≤10 messages per call; conversation-level read is not applicable) to mark the messages as read;
- Destruct on both ends: The recipient calls
deleteMessages (isAlsoDeleteServerMessage = true)to delete the local and server records; the sender deletes likewise after receiving messageReceiptChanged (readTime has a value, 2.22.0+); - Multi-device sync: Other online devices on both ends remove the messages in sync through onMessageDeleted.
Four key design points:
- Put the burn flag in extendedData (visible to the peer end, 1 KB limit, which can be raised by contacting Technical Support), not in the message body, so that it does not affect search or rendering; if the same message also needs to carry other business flags (such as the receipt switch readReceipt), put them into the same JSON object and read them by key, so they do not conflict with each other;
- Both ends maintain a "burn message table" (local storage, keyed by messageID) as the sole basis for determining "whether destruct is needed";
- The read trigger must use message-level read receipts (sendMessageReceiptsRead), not conversation-level read receipts. Obtaining the receipt read time does not yet support conversation-level read, and conversation-level read would mistakenly destruct other burn-after-reading messages in the conversation;
- The deletion must set isAlsoDeleteServerMessage = true; otherwise, after switching devices or reinstalling, the message "reappears" from the server history and the destruct promise becomes invalid. After cleanup through the delete API and the timed destruct capability, the server no longer retains the message.
Precautions
- One-on-one chats first (burn-after-reading): Message receipts support one-on-one chats and group chats, but not rooms. The group-chat semantics of "destruct only after all members have read" are complex (the sender's readTime only has a value after all members have read). It is recommended to start with one-on-one chats; for group chats, consider timed destruct instead;
- Message types (burn-after-reading): Burn-after-reading messages can be carried by ordinary/rich media/custom messages; receipts are only unsupported for signaling messages and barrage messages;
- Offline messages (burn-after-reading): While the recipient is offline, messages are delivered as ordinary offline messages; destruct is triggered only after the recipient goes online and views them, which matches the "burn after reading" semantics;
- Destruct is not absolute secrecy (burn-after-reading): The deletion happens on both ends' local storage and server records, and cannot prevent the other party from taking screenshots or photos. Agree on the destruct timing and this boundary at the product agreement level;
- Retry on deletion failure (burn-after-reading): Destruct is a strong commitment. Failures enter a retry queue until success, and the retry process is hidden from users;
- Offline destruct reconciliation (burn-after-reading): If all of the sender's devices are offline when the recipient destructs, the sender's local messages will remain (onMessageDeleted is only synced to the online devices of the same account). Both ends should reconcile the burn table after successful login: messages that no longer exist on the server are cleared locally (for rich media, also clear the local media cache). The reconciliation logic must be compatible with both "receipt callbacks re-delivered after being offline" and "not re-delivered", and must not mistakenly clear messages that have not been destructed yet;
- Rich media destruct must be thorough (burn-after-reading): When images/videos are destructed, clear the local media cache files as well;
- Version/plan pre-checks (timed destruct): Check the 3.1.0+ version and Enterprise plan subscription status before calling; on failure, guide users explicitly in the UI instead of swallowing the error;
- Use the SDK timestamp for the countdown (timed destruct): Calibrate with getSDKTimestamp to prevent the perception of early/late destruct caused by users changing the local clock;
- Tighten group-chat permissions (timed destruct): For admin-type groups, contact Technical Support to allow only the group owner/admins to configure it;
- Handling onMessageDeleted is a must for multi-device login: Both paths rely on this callback to keep multiple devices consistent;
- UI expectation management: Display the burn indicator and the countdown/burn style on the chat page.
Integration example
// Set messages sent in this conversation to be automatically destructed after 5 minutes (minimum 60 seconds)
[zim setConversationMessageDestructDuration:300
conversationID:conversationID
conversationType:ZIMConversationTypePeer
callback:^(NSString *conversationID,
ZIMConversationType conversationType, ZIMError *errorInfo) {}];
// The peer perceives the change through the conversation change callback; timed destruct triggers messageDeleted (type MessagesDestructed)
- (void)zim:(ZIM *)zim conversationChanged:(NSArray<ZIMConversationChangeInfo *> *)infoList {
for (ZIMConversationChangeInfo *info in infoList) {
int duration = info.conversation.messageDestructDuration; // In seconds, 0 = disabled
}
}
- (void)zim:(ZIM *)zim messageDeleted:(ZIMMessageDeletedInfo *)deletedInfo {
if (deletedInfo.deleteType == ZIMMessageDeleteTypeMessagesDestructed) {
// Remove the corresponding message from the message list
}
}
// Countdown calibration: call callExperimentalAPI with {"method":"getSDKTimestamp"} to get the current SDK timestamp (ms)// Sender: send a burn-after-reading message
ZIMTextMessage *message = [[ZIMTextMessage alloc] init];
message.message = @"This is a burn-after-reading message";
message.extendedData = @"{\"burnAfterRead\":true}";
ZIMMessageSendConfig *config = [[ZIMMessageSendConfig alloc] init];
config.hasReceipt = true;
[self.zim sendMessage:message toConversationID:toUserID conversationType:ZIMConversationTypePeer
config:config notification:nil callback:^(ZIMMessage * _Nonnull message, ZIMError * _Nonnull errorInfo) {}];
// Recipient: trigger read + destruct after viewing
[[ZIM getInstance] sendMessageReceiptsRead:messageList conversationID:conversationID
conversationType:conversationType callback:^(NSString * _Nonnull conversationID,
ZIMConversationType conversationType, NSArray<NSNumber *> * _Nonnull errorMessageIDs,
ZIMError * _Nonnull errorInfo) {}];
ZIMMessageDeleteConfig *deleteConfig = [[ZIMMessageDeleteConfig alloc] init];
deleteConfig.isAlsoDeleteServerMessage = true; // The server record must be deleted; otherwise the message "reappears" after switching devices
[self.zim deleteMessages:messageList conversationID:conversationID conversationType:conversationType
config:deleteConfig callback:^(NSString * _Nonnull conversationID,
ZIMConversationType conversationType, ZIMError * _Nonnull errorInfo) {}];
// Sender: destruct after receiving the read receipt (readTime has a value, 2.22.0+); multiple devices sync via messageDeleted
- (void)zim:(ZIM *)zim messageReceiptChanged:(NSArray<ZIMMessageReceiptInfo *> *)infos {}
- (void)zim:(ZIM *)zim messageDeleted:(ZIMMessageDeletedInfo *)deletedInfo {}FAQ
There is no strict "destruct after reading" switch. The native capability is conversation-level timed message destruct (3.1.0+, Enterprise plan), but its countdown starts when the message is sent and is independent of whether it has been read, so it only satisfies the "disappears after a time limit" semantics. If the requirement is "disappears only after being read", it must be implemented by combining "receipt read time + message deletion", and timed destruct can also be added as a fallback for unread scenarios.
Yes. The countdown starts when the message is sent and is independent of whether it has been read. Align this expectation with the product team in advance; for strict "destruct only after being read", choose the burn-after-reading combined implementation.
60 seconds (the value is 0 or 60~604800 seconds; 0 means canceling). Therefore, the shortest implementation of "disappears within tens of seconds after being sent" is 1 minute.
The impact of deleteMessages is limited to the current account (one-sided deletion). It cannot delete the records on the other party's devices, so it cannot serve as the sole destruct method; it must be combined with the two-end convention of "the recipient also deletes + isAlsoDeleteServerMessage".
Timed destruct natively supports group chats (takes effect on all members; configurable by all members by default); burn-after-reading in group chats requires "destruct only after all members have read" (the sender's readTime only has a value after all members have read). "Destruct once any member has read" is inconsistent with the receipt mechanism and requires additional design on the business side. It is recommended to choose timed destruct for group chats.
No. After cleanup through the delete API (isAlsoDeleteServerMessage) and the timed destruct capability, the server no longer retains the message.
Revoking (revokeMessage) is a manual/admin operation on a specific message with a default time window; timed destruct is a conversation-level automated policy that takes effect on all new messages after being enabled, with a fixed duration. The two can coexist.
