Conversations
The 24-hour window
WhatsApp is built around consent. When a customer messages you, a 24-hour service window opens with them. Inside it, you may send free text and anything else you like. Outside it, only an approved template will be delivered.
The window is extended by each new inbound message, not by anything you send. A conversation with a customer who never replies closes 24 hours after their last message, however much you send in the meantime.
MALIPOPAY accepts and bills a free-text send whether or not a window is open. Meta is what silently drops it. If a message "vanished", check whether the window was open before you look anywhere else.
A conversation is a billing window, not a thread
This trips people up. A WabaConversation is a per-category 24-hour billing window, so one customer can have several open at once (a MARKETING window and a SERVICE window), and a single ongoing chat spans many conversations over time.
The timeline route below hides that from you: it resolves the conversation to its contact and returns everything you have exchanged with that customer on that number, so a template you sent appears in the same thread as the reply it produced. It is the windows, not the thread, that are the billing detail.
Read a timeline
curl https://core-prod.malipopay.co.tz/api/v1/waba/conversations/{id}/messages \
-H "apiToken: YOUR_API_KEY"
Returns the whole exchange with that contact, oldest first, scoped to your own project. On a number you share with other merchants you never see theirs.
List conversations
curl "https://core-prod.malipopay.co.tz/api/v1/waba/conversations?category=SERVICE" \
-H "apiToken: YOUR_API_KEY"
Filter by phoneNumberId, contactWaId and category; paginated.
Reply inside an open window
curl -X POST https://core-prod.malipopay.co.tz/api/v1/waba/conversations/{id}/reply \
-H "apiToken: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "text": "Thanks, your refund is on its way." }'
This is the free-text path, and it only works while the window is open. If it has closed, send a template instead (see below).
PATCH /api/v1/waba/conversations/{id}/read clears the unread badge, for building your own inbox.
Reply to a specific message
Add replyToMessageDocId and WhatsApp renders your reply attached to that message, the way tapping "reply" in the app does. Useful when a customer asks four things in one message and you are answering the second.
curl -X POST https://core-prod.malipopay.co.tz/api/v1/waba/conversations/{id}/reply \
-H "apiToken: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"text": "Yes, delivery within Dar is included.",
"replyToMessageDocId": "66b2f1c9e4d3a20012a4c8e1"
}'
That is the _id of the message as returned by the timeline route, not Meta's wamid. We read the wamid off our own record, which is what stops a caller quoting a message outside their own conversation.
Quoted replies work on attachments too, and the same field arrives on inbound messages as contextWamid when a customer replies to something specific.
Send an attachment
Multipart. The file goes to Meta, we keep our own copy so the thread still renders it after Meta's 30-day expiry, and it is sent as the WhatsApp type matching its MIME type.
curl -X POST https://core-prod.malipopay.co.tz/api/v1/waba/conversations/{id}/attachment \
-H "apiToken: YOUR_API_KEY" \
-F "file=@invoice.pdf" \
-F "caption=Your invoice for August"
| Field | Required | Notes |
|---|---|---|
file | yes | Type inferred from MIME. image/webp becomes a sticker, not an image. |
caption | no | Ignored for audio and stickers, which cannot carry one. |
replyToMessageDocId | no | Quote a message, as above. |
Meta's size ceilings apply and differ sharply by type. We check before uploading, so an oversized file is refused with the limit and the actual size named rather than by Meta after the whole file has gone over the wire:
| Type | Limit |
|---|---|
| image | 5 MB |
| sticker | 512 KB |
| audio | 16 MB |
| video | 16 MB |
| document | 100 MB |
An attachment is a session message, so it needs an open window.
React to a message
curl -X POST https://core-prod.malipopay.co.tz/api/v1/waba/conversations/{id}/reaction \
-H "apiToken: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "messageDocId": "66b2f1c9e4d3a20012a4c8e1", "emoji": "👍" }'
Send an empty emoji to remove a reaction. That is Meta's own model and there is no separate delete call.
Like quoted replies, this takes our message id rather than a wamid. Reactions are never charged.
Send a template into a conversation
The one message WhatsApp still delivers once the window has closed.
curl -X POST https://core-prod.malipopay.co.tz/api/v1/waba/conversations/{id}/template \
-H "apiToken: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"templateName": "invoice_share",
"language": "en",
"values": { "body": ["Asha", "TZS 40,000"], "buttons": { "0": "inv-8821" } }
}'
Prefer values over a hand-built components array. We read the stored template definition and assemble the right shape, which is the only way an authentication template gets its code into both the body and the OTP button. See Sending for what values accepts.
Inbound media
An inbound media message carries a Meta media id, not the file. Fetch the bytes with:
curl https://core-prod.malipopay.co.tz/api/v1/waba/messages/{id}/media \
-H "apiToken: YOUR_API_KEY" \
-o attachment.jpg
Do this promptly. Meta expires media ids after 30 days.
Billing
Conversations are what you pay for, priced per category:
curl https://core-prod.malipopay.co.tz/api/v1/waba/billing/conversations \
-H "apiToken: YOUR_API_KEY"
Five messages inside one open window cost one conversation. Batching updates into a single window is genuinely cheaper than spreading them out, which is worth designing for.