← All posts
Tutorial

Load Older WhatsApp Messages With On-Demand History Requests

To load older WhatsApp messages through UnifyPort, subscribe to conversation.history, then request history using a stored message as the before anchor. The HTTP response does not contain the messages: 202 and status: accepted only acknowledge the request. Available history arrives asynchronously. Build a best-effort Load older messages control, not an archive export or a pagination loop that claims to know when history is complete.

Key takeaways

  • The documented operation supports provider=whatsapp private chats, not groups, channels, or whatsapp-protocol.
  • Subscribe and prepare durable storage before requesting history.
  • Advance using an actual native content message with its ID, send time, and direction.
  • Duplicate, late, multiple, or absent batches are possible; silence is not completion.
  • Historical content should enrich the timeline, not trigger new-message auto-replies.

Separate the request from the returned history

The request history reference documents POST /v1/accounts/{account_id}/conversations/history/request. It initiates asynchronous delivery; it is not a REST read endpoint returning stored messages.

ObservationWhat it establishesWhat it does not establish
HTTP 202, data.status: acceptedThe request was acceptedMessages were delivered or the operation completed
HTTP request_idA reference for troubleshooting that requestA task ID or callback correlation key
conversation.history with data.history.source: on_demandAn on-demand history batch arrivedThe batch is the only or final result
Empty or short batchWhat this batch containsThe end of available history
HTTP timeoutThe client did not receive a conclusive responseThat the request had no effect

The runtime recovery runbook addresses restoring a connection. That is a different task: neither reconnecting nor requesting history guarantees recovery of everything missed during an outage.

Prepare the receiver before the button

Add conversation.history to the endpoint’s subscription without dropping events your inbox already needs. Keep message.received for live traffic. Use the webhook configuration reference when updating an existing endpoint; do not accidentally clear its signing_secret.

Follow the delivery contract: verify X-Device-Signature with HMAC-SHA256 over X-Device-Timestamp, a dot, and the raw body. Check timestamp freshness, then durably store the authenticated delivery before acknowledging with 2xx.

History needs its own deduplication branch. Top-level event IDs can be reused across WhatsApp HistorySync chunks. Do not discard an entire history batch just because its event ID appeared earlier. Within your workspace, merge messages by provider, account_id, data.conversation.id, and each data.messages[].id.

Keep these messages outside the live auto-reply trigger. Receiving an old customer question today is not evidence that the customer just asked it again.

Construct a valid before anchor

Use an observed message from the same account and private conversation. The anchor requires message_id, sent_at, and direction. Do not guess these values from the conversation title, receipt time, or a phone number.

This illustrative request body follows the documented shape; replace its sample identifiers with stored values:

{
  "conversation_id": "100000000000002@lid",
  "before": {
    "message_id": "MSG_HISTORY_ANCHOR_001",
    "sent_at": "2026-09-28T03:00:00Z",
    "direction": "inbound"
  },
  "limit": 50
}

Send it to the documented POST operation with backend-held X-Api-Key authentication. account_id must be one non-empty path segment without whitespace or slashes, including encoded slashes. Do not put the conversation identifier into that account path segment.

For a subsequent request, select the earliest received native content message with complete anchor fields. Exclude synthetic records with type: call. The following JavaScript only builds an anchor from an already selected and validated message; it is not a receiver or a pagination worker:

function beforeFromMessage(message) {
  if (!message || message.type === 'call' ||
      typeof message.id !== 'string' || !message.id ||
      typeof message.sent_at !== 'string' ||
      !Number.isFinite(Date.parse(message.sent_at)) ||
      !['inbound', 'outbound'].includes(message.direction)) {
    throw new Error('Select a native content message with complete anchor fields');
  }
  return {
    message_id: message.id,
    sent_at: message.sent_at,
    direction: message.direction
  };
}

Notice that type is not copied into before. If there is no valid anchor, disable the request and explain why. Do not synthesize a call anchor or invent an earlier timestamp.

Model uncertainty in the inbox

The following are recommended application behaviors, not additional API statuses:

  1. Before sending: record the account, conversation, anchor, and local request time. Serialize operator requests per conversation to reduce overlapping work.
  2. After acceptance: show “Request accepted; waiting for available history.” Continue receiving live messages independently.
  3. On each batch: merge child messages idempotently. Preserve live edits and deletion barriers rather than overwriting newer state with older history.
  4. After receiving older content: allow an explicit next request with the earliest eligible anchor. If there is no earlier eligible anchor, report “No earlier anchor received,” not “All history loaded.”
  5. After a timeout or no callback: retain an uncertain state and continue accepting late batches. Do not automatically resend the same request.

There is no documented next-page cursor or completion status. account.history.synced is a HistorySync batch/chunk summary, not proof that an on-demand request finished or that an archive is complete. Do not turn it into an invented task-completion signal.

Keep historical reply relationships distinct from send capabilities. History messages do not include reply_token; the quoted-reply guide explains why a parent message ID cannot replace it. Similarly, an unavailable attachment should remain visibly unavailable rather than being presented as a downloaded file.

Errors and acceptance checks

A 400 can indicate invalid_request, provider_invalid_request—including a synthetic call anchor—or unsupported_conversation_type. Other providers return 501 unsupported_by_provider. Correct the scope or input instead of creating a retry loop. Save the HTTP request_id for troubleshooting, without treating it as callback correlation.

Before production, test duplicate batches, two different chunks with the same event ID, a callback after an HTTP timeout, live traffic interleaved with history, and an anchor candidate missing direction. These are proposed tests, not reported results.

UnifyPort is an unofficial interface. This feature supplies best-effort conversation context, not a complete backup, guaranteed replay, or access to arbitrary accounts. Keep your own authorized message store and retention controls.

FAQ

Does 202 mean older messages were fetched?

No. It means the request was accepted. Available messages arrive through asynchronous history events.

Can I keep requesting until a short batch arrives?

Do not use that as an end condition. Batch size does not establish completeness, and automatic requests can overlap with late results.

Can I use this for WhatsApp groups, LINE, or Zalo?

This operation is documented for provider=whatsapp private chats only. A shared webhook schema does not imply shared history-request support.

Next step and sources

Implement the receiver and uncertainty states against the request conversation history reference before enabling the inbox button.

Official references checked on 2026-09-30:

UnifyPort API

Turn messaging integration into a stable product pipeline.

Start by sending through one API, then bring every inbound message back into your business system with standard events.