> ## Documentation Index
> Fetch the complete documentation index at: https://docs.pilotstatus.com.br/llms.txt
> Use this file to discover all available pages before exploring further.

# Send WhatsApp Messages via API | Pilot Status

> How to send template, free-form text, and media WhatsApp messages with POST /v1/messages/send using x-api-key authentication.

# Send Messages Guide

This guide shows how to send WhatsApp messages via the Pilot Status API. All sends go through a **single endpoint**:

```text theme={null}
POST https://pilotstatus.com.br/v1/messages/send
```

Authentication uses the `x-api-key: ps_...` header (or `x-api-key-id`) with a **number-scoped** key — see [API Authentication](/api/authentication). There is no Bearer-token auth.

The endpoint has three mutually exclusive modes: **template send** (`templateId`), **free-form text** (`text`), and **direct media** (`media` + `mediaType`). Full reference: [POST /v1/messages/send](/api/messages/send).

## Prerequisites

1. A connected WhatsApp number (dashboard `/numbers`).
2. An API key for that number (dashboard `/api-keys`).
3. For template sends: a template created in `/templates` (Meta numbers require Meta approval).

## 1. Send a template message

The most reliable mode — works at any time, including outside the 24h window on Meta numbers.

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST "https://pilotstatus.com.br/v1/messages/send" \
    -H "Content-Type: application/json" \
    -H "x-api-key: ps_your_key_here" \
    -d '{
      "templateId": "onboarding-test",
      "destinationNumber": "+5511999999999",
      "variables": { "name": "John", "order_id": "123" }
    }'
  ```

  ```javascript Node.js (fetch) theme={null}
  const res = await fetch("https://pilotstatus.com.br/v1/messages/send", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "x-api-key": process.env.PILOT_STATUS_API_KEY, // ps_...
    },
    body: JSON.stringify({
      templateId: "onboarding-test",
      destinationNumber: "+5511999999999",
      variables: { name: "John", order_id: "123" },
    }),
  });

  const data = await res.json(); // 202 => { id, correlationId, status: "QUEUED", ... }
  console.log(data.id); // persist this to track delivery
  ```
</CodeGroup>

## 2. Send free-form text

Send plain text with the `text` field (no `templateId`).

<Warning>
  On **Meta** numbers, free-form messages only work within the WhatsApp **24h conversation window** (i.e. after the recipient messaged you in the last 24 hours). Outside the window, the send fails asynchronously with `META_OUTSIDE_24H_WINDOW` in the `message.failed` webhook — use an approved template instead.
</Warning>

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST "https://pilotstatus.com.br/v1/messages/send" \
    -H "Content-Type: application/json" \
    -H "x-api-key: ps_your_key_here" \
    -d '{
      "text": "Hi! Your delivery has been confirmed for tomorrow.",
      "destinationNumber": "+5511999999999"
    }'
  ```

  ```javascript Node.js (fetch) theme={null}
  await fetch("https://pilotstatus.com.br/v1/messages/send", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "x-api-key": process.env.PILOT_STATUS_API_KEY,
    },
    body: JSON.stringify({
      text: "Hi! Your delivery has been confirmed for tomorrow.",
      destinationNumber: "+5511999999999",
    }),
  });
  ```
</CodeGroup>

You can add up to 3 `buttons` (and, with buttons, a `header`/`footer`) to free-form messages — see the [send reference](/api/messages/send).

## 3. Send direct media

Send an image, video, document, or audio on its own — provide `media` + `mediaType`, with no `templateId` and no `text`. An optional `caption` is allowed for image/video/document (not audio). `mediaType: "audio"` is delivered as a WhatsApp voice note (PTT).

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST "https://pilotstatus.com.br/v1/messages/send" \
    -H "Content-Type: application/json" \
    -H "x-api-key: ps_your_key_here" \
    -d '{
      "destinationNumber": "+5511999999999",
      "media": "https://cdn.example.com/voice.ogg",
      "mediaType": "audio"
    }'
  ```

  ```javascript Node.js (fetch) theme={null}
  await fetch("https://pilotstatus.com.br/v1/messages/send", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "x-api-key": process.env.PILOT_STATUS_API_KEY,
    },
    body: JSON.stringify({
      destinationNumber: "+5511999999999",
      media: "https://cdn.example.com/report.pdf",
      mediaType: "document",
      caption: "Your monthly report",
    }),
  });
  ```
</CodeGroup>

<Note>
  Media sends require an active paid subscription (otherwise **402** `SUBSCRIPTION_REQUIRED_FOR_MEDIA`). Base64 data URIs are accepted on Meta Cloud API and Evolution v2; **Evolution GO** numbers require a public http(s) URL.
</Note>

## 4. Track delivery

The `202` response returns an `id`. Persist it and:

* Poll [`GET /v1/messages/{id}`](/api/messages/status) for `QUEUED` → `SENT` → `DELIVERED` → `READ` (or `FAILED` / `CANCELED`), or
* Consume webhooks — the response `id` matches `internalMessageId` in `message.sent`, `message.delivered`, `message.read`, and `message.failed`.

## Next steps

* Full field reference and error codes: [POST /v1/messages/send](/api/messages/send)
* Scheduling with `deliverAt` and canceling: [DELETE /v1/messages/cancel](/api/messages/cancel)
* Failure diagnostics: [Log Error Codes](/api/messages/log-error-codes)
