# WhatsApp messages

This guide lists what you can send on WhatsApp, and what the person can send back. Formats are the ones in the [WhatsApp Cloud API](https://developers.facebook.com/docs/whatsapp/cloud-api/reference/messages) and [Kapso](https://docs.kapso.ai/docs/whatsapp/send-messages/buttons) documentation. What a format needs from a real number is said where it applies.

## Send inside the 24-hour window

Inside the 24 hours after the last message of the person, `POST /v2/cases/{id}/messages` with `author: "team"` takes `interactive` in place of `text`. The agent sends the same formats with its [system tools](/guides/actions-and-mcp).

| `type` | Content field | What it does | Agent tool |
|---|---|---|---|
| `buttons` | `buttons` | 1 to 3 quick replies. `headerMedia` puts an image, video or document above. | `send_buttons` |
| `list` | `list` | A menu of up to 10 rows. | `send_list` |
| `product` | `product` | One product of the catalog. | `send_products` |
| `product_list` | `products` | Up to 30 products in up to 10 sections. | `send_products` |
| `catalog` | `catalog` | Opens the catalog of the number. | `send_products` |
| `cta_url` | `cta` | One button that opens an https link. `headerMedia` is allowed. | `send_cta_url` |
| `flow` | `flow` | A button that opens a WhatsApp Flow (a form). | `send_flow` |
| `location_request` | none | Asks the person to share a location. Only `body`. | `request_location` |
| `contact_request` | none | Asks the person to share their phone. Only `body`. | |
| `call` | `call` | A button that starts a WhatsApp call. `call` is optional. | |
| `carousel` | `carousel` | 2 to 10 swipeable cards with an image or video and one button each. | `send_carousel` |

Limits that the API checks and names in `param`: body up to 1,024 characters; buttons and link labels up to 20; link and media URLs must be public https; a carousel card body takes up to 160 characters and 2 line breaks; every card of a carousel uses the same kind of button (all links or all quick replies, with the same count) and the quick reply ids are unique in the message.

```bash title="Open a Flow"
curl https://api.lelemon.cl/v2/cases/cse_.../messages \
  -H "Authorization: Bearer $LELEMON_API_KEY" \
  -H "Idempotency-Key: book-cse-0043" \
  --json '{
    "author": "team",
    "interactive": {
      "type": "flow",
      "body": "Book your visit",
      "flow": { "flowId": "1197715005513101", "cta": "Book now", "screen": "BOOKING", "token": "cse-0043" }
    }
  }'
```

```bash title="Carousel with links"
curl https://api.lelemon.cl/v2/cases/cse_.../messages \
  -H "Authorization: Bearer $LELEMON_API_KEY" \
  -H "Idempotency-Key: picks-cse-0043" \
  --json '{
    "author": "team",
    "interactive": {
      "type": "carousel",
      "body": "Our picks",
      "carousel": { "cards": [
        { "header": { "type": "image", "url": "https://cdn.example.com/chair.jpg" }, "body": "Chair", "cta": { "displayText": "View", "url": "https://shop.example.com/p/1" } },
        { "header": { "type": "image", "url": "https://cdn.example.com/table.jpg" }, "body": "Table", "cta": { "displayText": "View", "url": "https://shop.example.com/p/2" } }
      ] }
    }
  }'
```


A message needs `cases:send`, a person in control and the 24-hour window. Outside the window only an approved [template](#templates) goes out (`422 channel_window_closed`).

The Flow, the call button and the contact request need the feature on the WhatsApp account. The platform sends them in the shape the documentation shows; whether your account has them is Meta's answer, and it comes back in `error` (see [A message that fails](#a-message-that-fails)).

## What the person sends back

Everything arrives as a message of the case and as `message.received`.

| The person | Field of the message | `type` in `message.received` |
|---|---|---|
| Taps a button or a list row | `reply: { id, title }` | `reply` |
| Sends a cart from the catalog | `order: { catalogId, items }` | `order` |
| Shares a location | `location: { latitude, longitude, name?, address? }` | `location` |
| Finishes a Flow | `flowResponse: { token?, data }` | `flow_response` |
| Shares their phone for a `contact_request` | `contact: { phones }` | `contact` |

`text` always carries a sentence the agent reads, such as `Location: -33.4489, -70.6693 (Home)` or `Flow response: {"date":"2026-11-02"}`. A contact card the person forwards on their own is not kept.

The event of a location, a Flow answer and a shared phone carries only the `type`, because they are personal data of the person: read the data with `GET /v2/cases/{id}/messages`.

## Catalog and commerce settings

The catalog is created in Meta Commerce Manager and connected in WhatsApp Manager. Then:

1. Save its id on the number: `PATCH /v2/whatsapp/phone-numbers/{id}` with `catalogId`.
2. Read or change the cart button and the catalog icon: `GET` and `PATCH /v2/whatsapp/phone-numbers/{id}/commerce-settings` with `isCartEnabled` and `isCatalogVisible`.

A `product`, `product_list` or `catalog` message, and a template with a catalog button, only goes out when the number has a catalog and the `catalogId` of the message is that one. Otherwise: `422 catalog_not_connected`. The platform cannot ask Meta whether the catalog exists; a wrong id shows up as a failed message with its reason.

## Templates

Outside the 24-hour window you send an approved template with `POST /v2/whatsapp/messages`, a campaign or a trigger. Besides text variables a template can have:

| In the template | When you send |
|---|---|
| `header: { type: "image" \| "video" \| "document", exampleFileId }` | `components.headerImage`, `components.headerVideo` (`fileId`, `url` or `attachmentId`) or `document` |
| A `url` button ending in a variable, like `https://shop.example.com/orders/{{order_id}}` | the value in `variables.order_id` |
| A `catalog` button | `components.catalog.thumbnailProductId` (optional) |
| An `mpm` button (several products) | `components.mpm: { thumbnailProductId, sections }` (required) |
| A `flow` button (`flowId`, `flowAction`, `screen`) | `components.flow: { token, data }` (optional) |
| `carousel: { type: "media", cards }` | `components.cards`: one `image` or `video` per card, each with `fileId` or `url` |
| `carousel: { type: "product", buttonText }` | `components.catalogId` and `components.cards`: 2 to 10 `productId` (marketing category) |

A media carousel has the number of cards you create it with. A product carousel is created with two and sent with 2 to 10. WhatsApp reviews every combination when it reviews the template.

## Files for messages, campaigns and templates

Upload a file once with `POST /v2/files` (multipart, the file in `file`, the `purpose` next to it) and use the `fil_…` id wherever a message, a campaign or a template takes media. You do not need a public URL, and the file does not have to stay on your server.

| `purpose` | Where the `fileId` goes | Accepts |
|---|---|---|
| `message_media` | `document.fileId`, `components.headerImage.fileId`, `components.headerVideo.fileId` and the cards of `POST /v2/whatsapp/messages` | JPEG and PNG up to 5 MB, MP4 and 3GPP up to 16 MB, PDF, text, Word, Excel and PowerPoint up to 100 MB |
| `campaign_media` | the same fields, in a campaign | the same |
| `template_header` | `header.exampleFileId` and `carousel.cards[].header.exampleFileId` when you create a template | JPEG and PNG up to 5 MB, MP4 up to 16 MB, PDF |

The limits are the ones in the [WhatsApp media table](https://developers.facebook.com/docs/whatsapp/cloud-api/reference/media); for template samples, the types that the [resumable upload](https://developers.facebook.com/docs/graph-api/guides/upload) takes. The type is read from the content, not from the `Content-Type` you send, and a file only works where its `purpose` says (`400 invalid_request` with the field in `param` otherwise). A file too big is `413 payload_too_large`.

When the message goes out, Lelemon uploads the file to WhatsApp and sends it by media id, which is what Meta recommends over a link. A template sample is uploaded with the resumable upload and Meta returns the handle that the review needs. A URL still works in `url` and `exampleUrl`, but Meta caches a link for ten minutes and does not recommend it.

A document that carries one person's data, such as a statement, is uploaded with `contactId` and `purpose: "message_media"`. It can only be sent to that contact and it is erased with them.

`GET /v2/files` lists them (filter with `purpose`), `GET /v2/files/{id}` reads one and `DELETE /v2/files/{id}` removes the stored file. A file used by a campaign that has not finished, or by a message that has not gone out, answers `409 resource_in_use`. The scopes are `files:read` and `files:write`. `?dryRun=true` on the upload validates the file and the purpose and stores nothing.

`POST /v2/cases/{id}/attachments` stays as it was: those files belong to one case and its person, and you send them with `attachmentIds` in a case message. A file of `/v2/files` belongs to the organization and is reused by many messages.

## A message that fails

A message that WhatsApp or the platform could not send has `status: "failed"` and `error: { code, message }`. `code` is WhatsApp's code when WhatsApp rejected it, and `null` when the platform stopped it first (for example, `recipient_not_allowed`).

## Not available in Chile

- `order_details`, `order_status` and payment inside WhatsApp: Kapso Pix is only for Brazil. In Chile the payment stays an external link: use `cta_url`.
- The address message is only for India.
