← All posts
Tutorial

Handle Batch Delivery and Read Receipts With UnifyPort

A UnifyPort delivery or read receipt can cover more than one message. Process every ID in data.message.ids when a batch is present; use data.message.id for a single-message receipt. Store the receipt separately from message content so it survives arriving first. An accepted send, a recipient receipt, and your team’s inbox read state are different evidence—not interchangeable success flags.

Key takeaways

  • Match receipts within the workspace, provider, account, and conversation—not by message ID alone.
  • Preserve provider-reported receipt times separately from webhook arrival time.
  • A late delivery receipt must not erase an already observed read receipt.
  • Missing confirmation means unknown, not permission to resend.

Read the batch scope before updating the screen

The standard event contract defines message.delivered as delivery to the recipient’s device and message.read as a recipient read receipt. Its batch field is data.message.ids. Updating only the singular id loses the rest of the batch.

EvidenceWhat it establishesWhat it does not establish
Send response with status: acceptedRequest acceptanceDelivery or reading
message.deliveredDelivery for the identified messagesRecipient reading
message.readReading for the identified messagesTicket resolution or every group member reading
Local conversation read stateConnected account’s chat-list stateA recipient receipt

For the last row, use the separate shared-inbox read/unread workflow. Do not call a conversation read action to repair a missing outbound receipt.

This shortened illustrative payload uses documented fields from the read-receipt reference; it is not a captured customer event:

{
  "id": "evt_2f9c1a4b7e",
  "type": "message.read",
  "provider": "whatsapp",
  "account_id": "acc_8c21d0",
  "occurred_at": "2026-06-08T12:35:10Z",
  "data": {
    "conversation": { "id": "8613912345678", "type": "user" },
    "sender": { "id": "8613912345678", "type": "user" },
    "message": {
      "id": "wamid.HBgM",
      "ids": ["wamid.HBgM", "wamid.HBgN"],
      "read_at": "2026-06-08T12:35:10Z"
    }
  }
}

Here the receipt covers both listed messages. It does not establish that all earlier messages were read. Do not infer a range from numeric-looking IDs.

Store observations before building status badges

Recommended application design uses two layers: a durable authenticated event record and a per-message receipt view. These are your storage choices, not additional UnifyPort fields.

  1. Include both receipt event names in the endpoint’s subscriptions while preserving other required events and signing configuration.
  2. Follow the webhook delivery contract: verify HMAC-SHA256 over the timestamp, a dot, and raw body with signing_secret; check timestamp freshness before processing.
  3. Persist the verified event before acknowledging with 2xx. Make the event record and durable processing job atomic, or commit the projection and deduplication record together.
  4. Expand the batch into message-scoped observations. Keep workspace context, provider, account_id, data.conversation.id, event ID, and available sender identity.
  5. Retain occurred_at, the provider’s read_at or delivered_at, and your own arrival time separately. The receipt time is also mirrored at the corresponding data level. Missing time remains unknown; arrival time is not a replacement provider timestamp.

This JavaScript helper extracts only the documented message scope from an already authenticated event. It is not a complete receiver or database implementation:

function receiptMessageIds(event) {
  if (!['message.delivered', 'message.read'].includes(event.type)) return [];
  const message = event.data?.message;
  const ids = message?.ids === undefined ? [message?.id] : message.ids;
  if (!Array.isArray(ids) || ids.length === 0 ||
      ids.some(id => typeof id !== 'string' || id.length === 0)) {
    throw new Error('Receipt scope needs review');
  }
  return [...new Set(ids)];
}

Do not silently substitute the singular ID for a malformed batch. Preserve that event for investigation rather than applying a guessed scope.

Merge without inventing delivery evidence

Keep delivery and read observations independently. A read receipt arriving before a delivery receipt can produce a “read observed” badge without fabricating delivered_at. A later delivery event adds evidence; it must not downgrade the badge to “delivered.” Duplicate events must not create duplicate notifications.

If message content has not arrived, retain the receipt under its scoped message identity and attach it when a matching record becomes available. Do not create an inbound message or increment inbox counts from the receipt alone. Use occurred_at and event ID for deterministic event ordering, but do not implement last-arrival-wins status assignment.

For sends, save the returned message_id and provider_ref alongside your local operation. Establish which identifier corresponds to the observed provider message before joining receipts; the differently named fields are not a universal equality guarantee. An unmatched receipt belongs in a reconciliation queue, not on the nearest message by timestamp. Preserve available sender identity without turning one observation into “everyone read.”

The media-send troubleshooting guide covers source and acceptance failures. Receipt processing starts after that boundary; a timeout or absent receipt must not automatically dispatch another send.

Capability limits and acceptance tests

UnifyPort is an unofficial interface. The provider event matrix describes parser mappings, not guaranteed receipt emission by every account or deployment. Check the selected provider—including LINE—before promising delivery badges. No receipt is not proof of unread content, failed delivery, or blocking. Missed payloads do not have guaranteed replay.

Proposed tests: a batch covering two messages, a singular receipt, duplicate delivery, read before delivered, receipt before content, identical message IDs in different accounts, malformed batch data, and no receipt. Verify that only explicit IDs change and no test creates a resend. These are tests to perform, not reported results.

FAQ

Should I process id or ids?

Use the non-empty valid batch ids when present. Otherwise use the singular id. Treat invalid scope as an investigation case.

Does read imply I can fill in delivered_at?

No. Show the observed read evidence, but do not manufacture a provider delivery timestamp.

Can a missing receipt mark a send as failed?

Not by itself. Keep confirmation unknown and investigate capabilities, subscriptions, intake, and identifier matching.

Next step and sources

Start with the delivery-receipt reference and implement the scoped observation store before adding UI badges.

Product references checked on 2026-11-06:

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.