# Messages

> Send and receive WhatsApp messages

Source: https://docs.soosh.io/reference/api/messages/

## Overview

The Messages API allows you to send various types of WhatsApp messages including text, media, templates, and interactive messages.

## Get Messages

Retrieve messages for a specific contact.

`GET /api/contacts/{id}/messages`

### Query Parameters

| Parameter | Type | Description |
|-----------|------|-------------|
| `page` | integer | Page number (default: 1). Page 1 is the **most recent** messages. |
| `limit` | integer | Page size, 1–100. Default 50. |
| `before_id` | string | Cursor — return the `limit` messages immediately older than this message UUID. Mutually exclusive with `page`. |
| `account` | string | Restrict to one WhatsApp account. |

Messages are always returned in chronological order (oldest first) within the page.

> **Careful:** The cursor parameter is `before_id`, not `before`. There is no `after` parameter.

### Response

```json
{
  "status": "success",
  "data": {
    "messages": [
      {
        "id": "uuid",
        "contact_id": "uuid",
        "direction": "incoming",
        "message_type": "text",
        "content": { "body": "Hello!" },
        "media_url": "",
        "media_mime_type": "",
        "media_filename": "",
        "interactive_data": null,
        "status": "delivered",
        "wamid": "wamid.xxx",
        "error_message": "",
        "is_reply": false,
        "reply_to_message_id": null,
        "reply_to_message": null,
        "reactions": [],
        "whatsapp_account": "Main Account",
        "created_at": "2024-01-01T12:00:00Z",
        "updated_at": "2024-01-01T12:00:00Z"
      }
    ],
    "total": 100,
    "page": 1,
    "limit": 50,
    "has_more": true
  }
}
```

The field is `message_type` (not `type`), the WhatsApp ID is `wamid` (not `wa_message_id`), and
timestamps are `created_at` / `updated_at` (there is no `timestamp`). A `before_id` request returns
`messages`, `total` and `has_more` only — no `page`/`limit`.

> **Note:** Fetching a contact's messages also marks that contact's incoming messages as read as a side
> effect. Callers without `contacts:read` are restricted to contacts assigned to them, and — when
> the org enables `agent_current_conversation_only` — to messages from the contact's most recent
> chatbot session onwards.

## Send Text Message

Send a text message to a contact.

`POST /api/contacts/{id}/messages`

### Request Body

```json
{
  "type": "text",
  "content": {
    "body": "Hello! How can I help you today?"
  }
}
```

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `type` | string | Yes | `text` or `interactive` on this endpoint. Media is sent via `POST /api/messages/media`. |
| `content.body` | string | Yes (for text) | The message text |
| `interactive` | object | Yes (for `interactive`) | See [Send Interactive Message](#send-interactive-message) |
| `reply_to_message_id` | string | No | UUID of a message to quote-reply to. Must belong to the same contact, otherwise it is silently ignored. |
| `whatsapp_account` | string | No | Name of the WhatsApp account to send from; defaults to the contact's account |

### Response

Returns the created message object (same shape as in the list response above):

```json
{
  "status": "success",
  "data": {
    "id": "uuid",
    "contact_id": "uuid",
    "direction": "outgoing",
    "message_type": "text",
    "content": { "body": "Hello! How can I help you today?" },
    "status": "sent",
    "wamid": "wamid.xxx",
    "whatsapp_account": "Main Account",
    "created_at": "2024-01-01T12:00:00Z",
    "updated_at": "2024-01-01T12:00:00Z"
  }
}
```

> **Note:** A legacy `POST /api/messages` route is kept for backwards compatibility. It
> maps to the same handler as `POST /api/contacts/{id}/messages` and expects the
> same request body. Prefer the contact-scoped endpoint for new integrations.

## Send Template Message

Send a pre-approved template message.

`POST /api/messages/template`

### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `contact_id` | string | One of contact_id or phone_number | UUID of existing contact |
| `phone_number` | string | One of contact_id or phone_number | Phone number (creates contact if not exists) |
| `template_name` | string | One of template_name or template_id | Name of the template |
| `template_id` | string | One of template_name or template_id | UUID of the template |
| `template_params` | object | No | Named or positional body parameters |
| `header_params` | object | No | Value for a TEXT header with a `{{var}}` (max 1). Keyed by the variable's name for named templates, or `"1"` for positional. Falls back to `template_params` if omitted. |
| `button_params` | object | No | Dynamic URL button parameters (button index → value) |
| `account_name` | string | No | Specific WhatsApp account to use |

### Examples

**Using phone number (creates contact if needed):**

```bash
curl -X POST "http://your-server:8080/api/messages/template" \
  -H "X-API-Key: soosh_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "phone_number": "919876543210",
    "template_name": "hello_world"
  }'
```

```json
{
  "phone_number": "919876543210",
  "template_name": "hello_world"
}
```

**With named parameters:**

If your template has placeholders like `Hello {{name}}, your order {{order_id}} is ready`:

```json
{
  "contact_id": "uuid",
  "template_name": "order_confirmation",
  "template_params": {
    "name": "John",
    "order_id": "12345"
  }
}
```

**With positional parameters:**

```json
{
  "phone_number": "919876543210",
  "template_name": "order_confirmation",
  "template_params": {
    "1": "John",
    "2": "12345"
  }
}
```

**With a TEXT header variable:**

Meta allows at most one variable in a TEXT header. If your template's header is `Our {{season}} sale is on!`, supply the value via `header_params`:

```json
{
  "phone_number": "919876543210",
  "template_name": "seasonal_promotion",
  "header_params": {
    "season": "Summer"
  },
  "template_params": {
    "customer_name": "John",
    "discount": "25%"
  }
}
```

For a positional header (`Our {{1}} sale is on!`), use `"1"` as the key. If you omit `header_params`, the value is looked up in `template_params` by the same name — convenient for named templates where the header variable doesn't collide with a body variable.

**With URL button parameters:**

If your template has a URL button with a dynamic variable (e.g., `https://example.com/track/{{1}}`), provide the dynamic value via `button_params`. The key is the button index (starting from `"0"`):

```json
{
  "contact_id": "uuid",
  "template_name": "order_shipped",
  "template_params": {
    "name": "John",
    "order_id": "12345"
  },
  "button_params": {
    "0": "12345"
  }
}
```

> **Note:** The `button_params` key corresponds to the button's position index in the template (zero-based). For example, if the first button is a URL button with `{{1}}`, use `"0"` as the key. The value replaces the dynamic part of the URL.

### Response

```json
{
  "status": "success",
  "data": {
    "message_id": "uuid",
    "phone_number": "919876543210",
    "status": "pending",
    "template_name": "order_confirmation"
  }
}
```

> **Tip:** When using `phone_number`, a new contact is automatically created if one doesn't exist for that number.

> **Careful:** If required template parameters are missing, you'll receive an error:
> `"Missing template parameters: name, order_id. Expected parameters: [name, order_id]"`

## Send Media Message

Upload and send an image, video, document, or audio message.

`POST /api/messages/media`

> **Careful:** This endpoint takes **`multipart/form-data` with a file upload**, not JSON. There is no
> `media_url` field — the file is uploaded to Soosh, which then sends it to Meta.

### Form Fields

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `contact_id` | text | Yes | UUID of the recipient contact |
| `file` | file | Yes | The media file |
| `type` | text | No | `image` (default), `video`, `audio`, or `document` |
| `caption` | text | No | Caption text |
| `whatsapp_account` | text | No | Account to send from; defaults to the contact's account |

```bash
curl -X POST "http://your-server:8080/api/messages/media" \
  -H "X-API-Key: soosh_your_api_key" \
  -F "contact_id=<uuid>" \
  -F "type=image" \
  -F "caption=Check out this product!" \
  -F "file=@./product.jpg"
```

Meta's own size limits apply (roughly 5 MB images, 16 MB audio/video, 100 MB documents); Soosh
does not enforce its own cap.

### Response

Returns the same message object shape as the other send endpoints:

```json
{
  "status": "success",
  "data": {
    "id": "uuid",
    "contact_id": "uuid",
    "direction": "outgoing",
    "message_type": "image",
    "content": { "body": "Check out this product!" },
    "media_url": "images/…",
    "media_mime_type": "image/jpeg",
    "media_filename": "product.jpg",
    "status": "sent",
    "whatsapp_account": "Main Account",
    "created_at": "2024-01-01T12:00:00Z",
    "updated_at": "2024-01-01T12:00:00Z"
  }
}
```

## Send Interactive Message

Send interactive messages with buttons, CTA URLs, WhatsApp Flows, or a Call button.

`POST /api/contacts/{id}/messages`

The `interactive.type` selects the variant:

| `interactive.type` | Fields used |
|---|---|
| `button` | `body`, `buttons: [{id, title}]` |
| `list` | `body`, `button_text` |
| `cta_url` | `body`, `button_text`, `url` |
| `flow` | `flow_id` (required), `body`, `button_text` (CTA label, default `"Open"`), `header`, `first_screen` |
| `voice_call` | `body`, `display_text`, `ttl_minutes` |

### Button Message

Send a message with up to 3 quick reply buttons:

```bash
curl -X POST "http://your-server:8080/api/contacts/{contact_id}/messages" \
  -H "X-API-Key: soosh_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "interactive",
    "interactive": {
      "type": "button",
      "body": "How would you like to proceed?",
      "buttons": [
        { "id": "yes", "title": "Yes" },
        { "id": "no", "title": "No" }
      ]
    }
  }'
```

```json
{
  "type": "interactive",
  "interactive": {
    "type": "button",
    "body": "How would you like to proceed?",
    "buttons": [
      { "id": "yes", "title": "Yes" },
      { "id": "no", "title": "No" }
    ]
  }
}
```

### CTA URL Button

Send a message with a call-to-action URL button:

```bash
curl -X POST "http://your-server:8080/api/contacts/{contact_id}/messages" \
  -H "X-API-Key: soosh_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "interactive",
    "interactive": {
      "type": "cta_url",
      "body": "Click below to view your order details",
      "button_text": "View Order",
      "url": "https://example.com/orders/12345"
    }
  }'
```

```json
{
  "type": "interactive",
  "interactive": {
    "type": "cta_url",
    "body": "Click below to view your order details",
    "button_text": "View Order",
    "url": "https://example.com/orders/12345"
  }
}
```

### WhatsApp Flow

```json
{
  "type": "interactive",
  "interactive": {
    "type": "flow",
    "flow_id": "<meta_flow_id>",
    "header": "Order form",
    "body": "Tap below to place your order",
    "button_text": "Open form",
    "first_screen": "WELCOME"
  }
}
```

The flow must belong to your organization, otherwise `400 Flow not found for this organization`.
Omitting `flow_id` returns `400 flow_id is required to send a flow`.

### Voice Call Button

```json
{
  "type": "interactive",
  "interactive": {
    "type": "voice_call",
    "body": "Need help? Call us.",
    "display_text": "Call support",
    "ttl_minutes": 60
  }
}
```

Requires the sending account to have Business Calling enabled, otherwise the request is rejected
with `400`. When the recipient taps the button, the resulting inbound call is sticky-routed back to
the agent who sent it — the routing payload is stamped server-side from the authenticated user and
is never read from the request body.

### Response

Returns the created message object. `interactive_data` carries the rendered interactive payload.

```json
{
  "status": "success",
  "data": {
    "id": "uuid",
    "contact_id": "uuid",
    "direction": "outgoing",
    "message_type": "interactive",
    "content": { "body": "How would you like to proceed?" },
    "interactive_data": { },
    "status": "sent",
    "is_reply": false,
    "whatsapp_account": "Main Account",
    "created_at": "2024-01-01T12:00:00Z",
    "updated_at": "2024-01-01T12:00:00Z"
  }
}
```

> **Note:** Button titles have a maximum length of 20 characters. Button IDs are returned when the user clicks a button.

## Send Reaction

Add or remove an emoji reaction on a specific message.

`POST /api/contacts/{id}/messages/{message_id}/reaction`

### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `emoji` | string | Yes | The emoji to react with. Send an empty string (`""`) to remove your existing reaction. |

```json
{
  "emoji": "👍"
}
```

Each user may have only one reaction per message; sending a new emoji replaces
the previous one from the same user.

### Response

```json
{
  "status": "success",
  "data": {
    "message_id": "uuid",
    "reactions": [
      {
        "emoji": "👍",
        "from_user": "user-uuid"
      }
    ]
  }
}
```

## Mark Conversation as Read

Mark all incoming messages from a contact as read. When the account has
auto read receipts enabled, read receipts are also sent to WhatsApp.

`POST /api/contacts/{id}/mark-read`

### Response

```json
{
  "status": "success",
  "data": {
    "status": "ok"
  }
}
```

## Mark Message as Read

`PUT /api/messages/{id}/read`

> **Careful:** **Not implemented.** This route is registered but currently returns
> `501 Not implemented yet`. Use [Mark Conversation as Read](#mark-conversation-as-read)
> (`POST /api/contacts/{id}/mark-read`) instead.

## Translate a Message

Translate a message's text (or a media caption) into one of the app's languages with AI, for the
agent to read. Nothing is sent to the customer.

```bash
POST /api/messages/{id}/translate
```

```json
{ "to": "en" }
```

`to` is one of `en`, `hi`, `ta`, `ar`, `es`, `pt`. Requires `chat:read` and access to the message's
contact (agents only their own chats; an agent limited to the current conversation only its
messages). The workspace needs the **AI translation** component and AI replies and knowledge
allocated. Each translation is metered as AI usage (`translate`), at most 300 per agent per hour by
default. A translation is kept for 30 days: the same text in the same language comes back from that
cache (`"cached": true`) without a new AI call or charge. Card, account and ID numbers, and codes,
PINs and passwords named in any of the app's languages, are removed before the text is sent;
one-time code messages are never translated.

```json
{
  "status": "success",
  "data": { "text": "Hello, where is my order?" }
}
```

| Status | When |
|--------|------|
| `400` | `to` isn't a supported language, the message has no text, or it is over 2,000 characters |
| `402` | The wallet has no credit left for AI, or the monthly AI limit is reached |
| `403` | AI translation (or AI) is off for the workspace |
| `404` | Message not found, or not one the agent may read |
| `429` | Too many AI requests; try again later |
| `502` | The AI's answer was cut off or empty (not charged); try again |
| `503` | AI isn't available right now |

`GET /api/ai/status` says `"translate": true` when the member can translate: the workspace has it
and AI on, and the member has `chat:read`.

## Rewrite a Draft

Rewrite the agent's draft reply with AI before they send it. Nothing is sent: the agent reads the
new text and sends it as usual.

```bash
POST /api/ai/rewrite
```

```json
{ "text": "ur order ships tmrw", "style": "friendly" }
```

`style` is `fix` (spelling and grammar only), `friendly`, `formal` or `shorter`. The text is 1 to
2,000 characters; the rewrite keeps its language, meaning, names, numbers, links and WhatsApp
formatting. Requires `chat:write`; the workspace needs the **AI rewrite** component and AI replies
and knowledge allocated. Each rewrite is metered as AI usage (`rewrite`), at most 120 per agent per
hour by default. Returns `{ "text": "..." }`, with the same errors as translation (`400` for an
unknown style, or a text that is empty or too long).

`GET /api/ai/status` says `"rewrite": true` when the member can rewrite: the workspace has it and AI
on, and the member has `chat:write`.

## Download Media

Stream the media attached to a message. Returns the raw file bytes with the stored MIME type — not
a JSON envelope.

`GET /api/media/{message_id}`

Returns `404 No media found` when the message has no attachment. Callers without `contacts:read`
may only fetch media for contacts assigned to them, or for contacts with an active transfer to a
team they belong to — otherwise `403 Access denied`.

## Message Status

Messages go through the following status flow:

| Status | Description |
|--------|-------------|
| `pending` | Message queued for sending |
| `sent` | Message sent to WhatsApp servers |
| `delivered` | Message delivered to recipient's device |
| `read` | Message read by recipient |
| `failed` | Message failed to send |
| `received` | Set on inbound messages |

Valid `message_type` values are `text`, `image`, `video`, `audio`, `document`, `template`,
`interactive`, `flow`, `reaction`, `location`, and `contact`.

> **Note:** Status updates are delivered via webhooks in real-time. Configure your webhook endpoint to receive these updates.

## Message Types

  **Text**

    Plain text messages
  
  **Image**

    JPEG, PNG images with optional caption
  
  **Video**

    MP4 videos with optional caption
  
  **Document**

    PDF, Word, Excel, and other documents
  
  **Audio**

    Voice messages and audio files
  
  **Template**

    Pre-approved message templates
  
  **Interactive**

    Buttons, lists, and reply buttons
  
  **Flow**

    WhatsApp Flows
