Interactive and media messages
Send media, reply buttons, lists, links, reactions, locations, and contacts on WhatsApp, and receive the customer's replies.
Beyond text and templates, the unified messages API sends richer WhatsApp content. Every content type
below goes in the whatsapp object of a channel: "whatsapp" send, and a send carries exactly
one of them. They are free-form content, so each one needs an open 24-hour
service window; outside it the send is refused with a validation error
telling you to use an approved template. Samva never converts a
refused send into a template for you.
A payload that breaks a limit on this page is a 400 validation error that names the field.
Send media
An image, video, audio file, document, or sticker goes in media. Name its source with either url,
a public http or https link that WhatsApp fetches, or mediaId, a file you uploaded to Samva.
curl -X POST https://api.samva.dev/v1/messages \
-H "X-API-Key: samva_sk_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"channel": "whatsapp",
"to": [{ "phone": "+16175559876" }],
"whatsapp": {
"media": {
"type": "document",
"url": "https://example.com/receipt.pdf",
"filename": "receipt.pdf",
"caption": "Your receipt"
}
}
}'caption works on images, videos, and documents (up to 1,024 characters). filename names a
document. Audio and stickers carry neither.
WhatsApp accepts these formats and sizes:
| Type | Formats | Largest file |
|---|---|---|
image | JPEG, PNG | 5 MB |
video | MP4, 3GPP | 16 MB |
audio | AAC, AMR, MP3, M4A, OGG | 16 MB |
document | PDF, plain text, Word, Excel, and PowerPoint (legacy and Open XML) | 100 MB |
sticker | WebP | 500 KB |
Upload a file instead of linking it
When the file is not publicly reachable, upload it as media with the whatsapp_media purpose and send
its mediaId. Samva delivers the bytes to WhatsApp for you. An uploaded file is at most 16 MB, so link
a larger document by url. A file whose format or size WhatsApp would refuse is rejected when you send.
# 1. Create the media object; the response carries its id and the upload request to make.
curl -X POST https://api.samva.dev/v1/media \
-H "X-API-Key: samva_sk_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{ "filename": "menu.pdf", "contentType": "application/pdf", "sizeBytes": 48213, "purpose": "whatsapp_media" }'
# 2. PUT the bytes to upload.url with every header in upload.headers, then complete the upload.
curl -X POST https://api.samva.dev/v1/media/media_k9mvt4znw8rh7q2x/complete \
-H "X-API-Key: samva_sk_live_your_api_key"
# 3. Send it by id.
curl -X POST https://api.samva.dev/v1/messages \
-H "X-API-Key: samva_sk_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"channel": "whatsapp",
"to": [{ "phone": "+16175559876" }],
"whatsapp": { "media": { "type": "document", "mediaId": "media_k9mvt4znw8rh7q2x", "filename": "menu.pdf" } }
}'Send reply buttons
Reply buttons let the customer answer with one tap. A message has a body (up to 1,024 characters)
and one to three buttons. Each button has an id (up to 256 characters) that comes back to you when
the customer taps it, and a title (up to 20 characters) that they see. Ids and titles are distinct
within a message.
Add an optional footer (up to 60 characters) and an optional header: { "type": "text", "text": "…" }
(up to 60 characters), or an image, video, or document given by url or mediaId.
curl -X POST https://api.samva.dev/v1/messages \
-H "X-API-Key: samva_sk_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"channel": "whatsapp",
"to": [{ "phone": "+16175559876" }],
"whatsapp": {
"interactive": {
"type": "buttons",
"body": "Your order ships today. Should we leave it at the door?",
"footer": "Reply anytime",
"buttons": [
{ "id": "door", "title": "Leave at door" },
{ "id": "hold", "title": "Hold for pickup" }
]
}
}
}'Send a list
A list shows a button that opens a menu of choices. Use it when there are more than three.
curl -X POST https://api.samva.dev/v1/messages \
-H "X-API-Key: samva_sk_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"channel": "whatsapp",
"to": [{ "phone": "+16175559876" }],
"whatsapp": {
"interactive": {
"type": "list",
"body": "When should we deliver?",
"button": "Pick a slot",
"sections": [
{
"title": "Tomorrow",
"rows": [
{
"id": "tue-am",
"title": "Morning",
"description": "8am to 12pm"
},
{
"id": "tue-pm",
"title": "Afternoon",
"description": "12pm to 5pm"
}
]
},
{
"title": "Wednesday",
"rows": [
{
"id": "wed-am",
"title": "Morning"
}
]
}
]
}
}
}'| Field | Limit |
|---|---|
body | Up to 4,096 characters |
button | Up to 20 characters |
header | Text only, up to 60 characters |
sections | 1 to 10; every section needs a title when there are several |
Section title | Up to 24 characters |
| Rows | At most 10 across all sections, with distinct ids |
Row id | Up to 200 characters |
Row title | Up to 24 characters |
Row description | Optional, up to 72 characters |
Send a link button
A call-to-action message shows a button that opens a URL. It takes the same body, header, and
footer as reply buttons, plus a displayText for the button (up to 20 characters) and an http or
https url:
curl -X POST https://api.samva.dev/v1/messages \
-H "X-API-Key: samva_sk_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"channel": "whatsapp",
"to": [{ "phone": "+16175559876" }],
"whatsapp": {
"interactive": {
"type": "cta_url",
"body": "Track your order any time.",
"displayText": "Track order",
"url": "https://example.com/orders/ORD-12345"
}
}
}'Send a flow
A flow message opens a multi-screen form inside WhatsApp. It is an interactive message of type
flow that names a registered flow and the button that opens it. Authoring, registering, and
sending flows are covered in WhatsApp flows. To open a flow outside
the service window, use a template's flow button.
React to a message
A reaction puts one emoji on a message in the same chat: one the customer sent you, or one you sent them that reached WhatsApp. Pass its Samva message id and exactly one emoji.
curl -X POST https://api.samva.dev/v1/messages \
-H "X-API-Key: samva_sk_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"channel": "whatsapp",
"to": [{ "phone": "+16175559876" }],
"whatsapp": {
"reaction": {
"messageId": "msg_01j8k3m5n7p9q2r4s6tv",
"emoji": "👍"
}
}
}'A reaction cannot target another reaction, or a message from a different chat.
Share a location
curl -X POST https://api.samva.dev/v1/messages \
-H "X-API-Key: samva_sk_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"channel": "whatsapp",
"to": [{ "phone": "+16175559876" }],
"whatsapp": {
"location": {
"latitude": 37.7749,
"longitude": -122.4194,
"name": "Samva Pickup Point",
"address": "1 Market St, San Francisco, CA"
}
}
}'latitude is between -90 and 90 and longitude between -180 and 180. name and address are
optional.
Share contacts
Send one to 257 contact cards. Each card needs name.formattedName; everything else is optional.
Give a phone's waId and the card offers to message that contact on WhatsApp.
curl -X POST https://api.samva.dev/v1/messages \
-H "X-API-Key: samva_sk_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"channel": "whatsapp",
"to": [{ "phone": "+16175559876" }],
"whatsapp": {
"contacts": [
{
"name": {
"formattedName": "Ada Lovelace",
"firstName": "Ada",
"lastName": "Lovelace"
},
"phones": [
{
"phone": "+16175550100",
"type": "WORK",
"waId": "16175550100"
}
],
"emails": [
{
"email": "ada@example.com",
"type": "WORK"
}
],
"org": {
"company": "Analytical Engines",
"title": "Founder"
},
"birthday": "1815-12-10"
}
]
}
}'A card can also carry urls, addresses, and the remaining name parts (middleName, prefix,
suffix). birthday is YYYY-MM-DD.
Send from the CLI
samva whatsapp send covers the common cases with flags and takes any content type as JSON:
# Media by link
samva whatsapp send --to +16175559876 \
--media-url https://example.com/receipt.pdf --media-type document --caption "Your receipt"
# Reply buttons
samva whatsapp send --to +16175559876 \
--body "Should we leave it at the door?" --button "door:Leave at door" --button "hold:Hold for pickup"
# Reaction
samva whatsapp send --to +16175559876 --react msg_01j8k3m5n7p9q2r4s6tv --emoji 👍
# Anything else: the whatsapp object as JSON, inline, from a file, or on stdin
samva whatsapp send --to +16175559876 --content @list.json
cat location.json | samva whatsapp send --to +16175559876 --content -Each call names exactly one content source, and --idempotency-key makes a retry safe.
Receive replies
When a customer taps a button, picks a list row, submits a flow, reacts, shares a location or contacts, or sends media,
Samva publishes a message.received webhook with channel: "whatsapp". Subscribe an endpoint to
message.received and the whatsapp channel; the event catalog
describes delivery and signing.
body carries the message text, or the caption of a media message, and is absent when there is
neither. messageType names what arrived, and whatsapp carries the content. A tap on a reply button
looks like this:
{
"type": "message.received",
"timestamp": "2026-01-15T10:05:33.900Z",
"data": {
"messageId": "msg_01j8k4n6p8q2r4s6t9vw",
"channel": "whatsapp",
"status": "delivered",
"direction": "inbound",
"from": "+16175559876",
"to": "+16175550123",
"messageType": "interactive",
"providerMessageId": "wamid.HBgLMTYxNzU1NTk4NzYVAgASGBQz",
"whatsapp": {
"reply": { "type": "button", "id": "door", "title": "Leave at door" },
"context": {
"messageId": "msg_01j8k3m5n7p9q2r4s6tv",
"providerMessageId": "wamid.HBgLMTYxNzU1NTAxMjMVAgARGBI2"
}
}
}
}The whatsapp object holds whichever of these the message carries:
| Field | Contents |
|---|---|
reply | type (button, list, or template_button), id, title, and for a list row its description. |
flowResponse | response (the payload the flow's complete action sent), flowToken (the token the flow message carried), and, when present, body. See WhatsApp flows. |
media | type, providerMediaId, and, when present, mimeType, caption, and filename. |
reaction | messageId (the message reacted to; null when it is not one Samva holds), providerMessageId, and emoji (null when the customer removed the reaction). |
location | latitude, longitude, and, when present, name, address, and url. |
contacts | The shared contact cards, in the same shape you send. |
context | The message this one answers: messageId and providerMessageId. |
Read data by type, and ignore fields you do not recognize.
Match a reply to the message it answers
Two fields tie a reply back to what you sent:
whatsapp.context.messageIdis the Samva id of the message the customer answered. It is theidthat the send returned, so you can look up which order, ticket, or conversation step that message belonged to. It isnullwhen the message was not sent through Samva. It can also benullwhen the answer arrives before Samva records that Meta accepted the send. The stored message gets the id once Samva records it, soGET /v1/messages/{id}/contentreturns it.whatsapp.reply.idis theidyou gave the button or list row, returned exactly as you sent it. Put whatever your handler needs to route the answer in it, such asdoorabove orreschedule:ORD-12345.
import { isKnownEvent, type WebhookEvent } from "@samva/webhooks";
export async function handleEvent(event: WebhookEvent) {
if (!isKnownEvent(event) || event.type !== "message.received") return;
if (event.data.channel !== "whatsapp") return;
const reply = event.data.whatsapp?.reply;
const answered = event.data.whatsapp?.context?.messageId;
if (reply === undefined || answered == null) return;
// Load the order you stored against this outbound message id when you sent it.
const order = await orders.findByMessageId(answered);
if (reply.id === "door") await orders.setDelivery(order.id, "door");
if (reply.id === "hold") await orders.setDelivery(order.id, "hold");
}A template's quick-reply button arrives as reply.type: "template_button", and its id is the
button's payload. A customer can also reply to a message by quoting it, which sets context without a
reply.
GET /v1/messages/{id}/content returns the same whatsapp content for a stored message.
Related documentation
Broadcasts and journeys
Send approved WhatsApp templates to an audience with campaigns or journey steps, fill parameters from each contact, and read skips and message costs.
WhatsApp flows
Author a WhatsApp flow as a project file, publish it as a version, register it with Meta, send it in the service window, and read the customer's answers.