## Send message

`POST https://whatsapp.paaltech.org/api/v1/businesses/{external_id}/messages`

Queues a text, template or media (image, video, audio, document) message - on WhatsApp, or
with `channel` sms or email (text; set up with [Set up SMS or email](/docs/api/enable-channel)).
SMS and email are always business-initiated, so suppression, consent and quiet hours apply. `202` = a new
message, queued; `200` = this `reference` was already submitted and the original message
is returned (nothing is sent again). Progress arrives as `message.status` callbacks.
Free-form text and media need an open 24-hour customer service window; otherwise send an
APPROVED template. For media, [upload the file](/docs/api/upload-media) first and pass its
`media.id` (businesses with media enabled).

Ability: `messages.send`

### Path parameters

| Name | Type | Required | Description |
|---|---|---|---|
| `external_id` | string | yes | Your own ID for the business (Multi-SKUUL - the tenant ID). Max 191 characters. Pattern `^[A-Za-z0-9._:-]+$` |

### Body

| Name | Type | Required | Description |
|---|---|---|---|
| `channel` | string | no | SMS and email send `type` text (email also needs `subject`); the business must have the channel set up. One of: `whatsapp`, `sms`, `email`. Default `"whatsapp"` |
| `to` | string | yes | International digits, optional leading + (an email address for email). |
| `subject` | string | null | no | Email only. Max 200 characters |
| `type` | string | yes | One of: `text`, `template`, `image`, `video`, `audio`, `document` |
| `text` | object | no | Required when type is text; not allowed otherwise. |
| `text.body` | string | yes | Max 4096 characters |
| `text.preview_url` | boolean | no | Default `false` |
| `template` | object | no | Required when type is template; not allowed otherwise. |
| `template.name` | string | yes | Max 512 characters |
| `template.language` | string | yes | Max 20 characters |
| `template.components` | array of objects | no | Meta's send-time components (header/body/button parameters). Max 20 items |
| `media` | object | no | Required when type is image, video, audio or document; not allowed otherwise. The file must suit the type. |
| `media.id` | integer | yes | PaalChat's media ID, from Upload media (or an incoming message's media_id). |
| `media.caption` | string | null | no | Images, videos and documents. Max 1024 characters |
| `media.filename` | string | null | no | Documents; defaults to the uploaded name. Max 240 characters |
| `category` | string | null | no | Templates only - the consent category checked. Defaults from the template (MARKETING -> marketing, AUTHENTICATION -> transactional, else notifications). One of: `marketing`, `notifications`, `transactional`, `` |
| `topic` | string | null | no | Templates only - refused if the contact turned this topic off. One of: `fees`, `results`, `attendance`, `pta`, `marketing`, `system`, `` |
| `send_at` | string | null | no | Templates only - send at this time (up to 90 days ahead); held until then and cancellable with Cancel scheduled message. Quiet hours still apply at that time. Format date-time |
| `urgent` | boolean | no | Templates only - skip the business's quiet hours. Needs the messages.urgent ability. Default `false` |
| `reference` | string | null | no | Your ID for the message - the idempotency key. Strongly recommended. Max 191 characters. Pattern `^[\x21-\x7E]+$` |
| `phone_number_id` | string | null | no | Required only when the business has several connected numbers. |

### Request

```bash
curl --request POST \
  --url 'https://whatsapp.paaltech.org/api/v1/businesses/presec/messages' \
  --header "Authorization: Bearer $PAALCHAT_TOKEN" \
  --header 'Accept: application/json' \
  --header 'Content-Type: application/json' \
  --data '{
  "to": "+233241234567",
  "type": "template",
  "reference": "skuul-msg-8812",
  "template": {
    "name": "fees_reminder",
    "language": "en_US",
    "components": [
      {
        "type": "body",
        "parameters": [
          {"type": "text", "text": "Mrs Mensah"},
          {"type": "text", "text": "Kofi"},
          {"type": "text", "text": "450.00"},
          {"type": "text", "text": "30 October"}
        ]
      }
    ]
  }
}'
```

### Responses

- **200** - Duplicate reference - the original message, with its current status.
- **202** - Queued.

```json
{
  "data": {
    "id": 2,
    "reference": "skuul-msg-8812",
    "wamid": null,
    "direction": "outgoing",
    "contact": "233241234567",
    "phone_number_id": "106540352242922",
    "type": "template",
    "template_name": "fees_reminder",
    "status": "queued",
    "error": null,
    "pricing": null,
    "created_at": "2026-09-30T18:54:28+00:00",
    "sent_at": null,
    "delivered_at": null,
    "read_at": null,
    "failed_at": null
  }
}
```

- **401** - unauthenticated - missing, invalid, revoked or expired token

```json
{
  "error": {"code": "unauthenticated", "message": "A valid product token is required."}
}
```

- **403** - missing_ability, product_suspended or product_token_required

```json
{
  "error": {
    "code": "missing_ability",
    "message": "This token does not have the ability this request needs."
  }
}
```

- **404** - not_found (business), unknown_phone_number or unknown_media.

```json
{
  "error": {
    "code": "unknown_phone_number",
    "message": "That phone number is not connected for this business."
  }
}
```

- **409** - not_connected, connection_paused, business_suspended or media_not_ready.

```json
{
  "error": {
    "code": "not_connected",
    "message": "This business has no connected WhatsApp number."
  }
}
```

- **422** - validation_failed, outside_service_window, unknown_template, template_not_approved, phone_number_required, media_type_mismatch, recipient_suppressed, recipient_opted_out , recipient_preference_off or invalid_template_parameters.

```json
{
  "error": {
    "code": "outside_service_window",
    "message": "The customer has not messaged in the last 24 hours. Send an approved template instead."
  }
}
```

- **429** - rate_limited - over 300 requests/minute for this product

```json
{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Slow down and retry after the Retry-After header."
  }
}
```

