# WhatsApp plain text messages

Plain text is the simplest free-form content arm: a body with no attachment, and an optional preview for the first link inside it.

## Send a text message

Set `text.body`:

<!-- bird:tabs typescript,python,go,php,cli,curl -->

```typescript
const msg = await bird.whatsapp.send({
  to: "+16505551234",
  from: "+13124495648",
  text: { body: "Your driver is 2 minutes away." },
});
console.log(msg.id, msg.status);
```

```python
msg = client.whatsapp.send(
    to="+16505551234",
    from_="+13124495648",
    text={"body": "Your driver is 2 minutes away."},
)
print(msg.id, msg.status)
```

```go
package main

import (
	"context"
	"fmt"
	"log"
	"os"

	bird "github.com/messagebird/bird-sdk-go"
	"github.com/messagebird/bird-sdk-go/option"
)

func main() {
	client, err := bird.NewClient(option.WithAPIKey(os.Getenv("BIRD_API_KEY")))
	if err != nil {
		log.Fatal(err)
	}
	msg, err := client.Whatsapp.Send(context.Background(), bird.WhatsappSendParams{
		To:   "+16505551234",
		From: "+13124495648",
		Text: &bird.WhatsAppTextSend{Body: "Your driver is 2 minutes away."},
	})
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(msg.Id, *msg.Status)
}
```

```php
$text = (new WhatsAppMessageSendRequestText())
    ->setBody('Your driver is 2 minutes away.');
$message = $bird->whatsapp->send(
    to: '+16505551234',
    from: '+13124495648',
    text: $text,
);
echo $message->getId(), ' ', $message->getStatus();
```

```cli
bird whatsapp send \
  --to +16505551234 \
  --from +13124495648 \
  --text 'Your driver is 2 minutes away.'
```

```curl
curl -X POST "https://us1.platform.bird.com/v1/whatsapp/messages" \
  -H "Authorization: Bearer $BIRD_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
      "to": "+16505551234",
      "from": "+13124495648",
      "text": {
        "body": "Your driver is 2 minutes away."
      }
    }'
```

<!-- /bird:tabs -->

The full shape adds `preview_url` plus the fields any free-form send can carry:

```json
{
  "to": "+16505551234",
  "from": "+13124495648",
  "text": {
    "body": "Your order shipped: https://example.com/track/A1B2C3",
    "preview_url": true
  },
  "in_reply_to_message_id": "wam_01kya19eknftrs2s6p82asmvnh",
  "tags": [{ "name": "category", "value": "shipping" }],
  "metadata": { "order_id": "A1B2C3" }
}
```

`from` is required on every service message: a number your workspace owns, not a Bird-managed one. `in_reply_to_message_id` quotes an earlier message in the same conversation; see [Quoting a message](/docs/guides/whatsapp/sending-whatsapp#quoting-a-message) for what it resolves against and what it can miss.

## Limits

| Field         | Bound                        | Enforced by             |
| ------------- | ---------------------------- | ----------------------- |
| `body`        | 1 to 4096 characters         | Bird, at accept (`422`) |
| `preview_url` | boolean, defaults to `false` | N/A, informational      |

A whitespace-only `body` passes the schema's own `minLength: 1`, but Bird still catches it: `body` that is empty after trimming is refused with `422` [`E15015`](/docs/api/errors/E15015) `WhatsAppContentRequired`. A body over 4096 characters is refused with a plain `422` and no dedicated catalog code.

## Reading an inbound text message

An inbound text message carries the same `text.body` field:

```json
{
  "id": "wam_01kya19eknftrs2s6p82asmvnh",
  "direction": "inbound",
  "from": { "phone_number": "+14155550100" },
  "to": { "phone_number": "+13124495569" },
  "text": { "body": "Does it come in another color?" },
  "status": "received"
}
```

See [Receiving WhatsApp messages](/docs/guides/whatsapp/receiving-whatsapp) for the message list, the API, and the webhook path.

## Limits and edge cases

- **The customer service window has to be open.** Plain text is a service message, deliverable only inside an open window; see the hub's [customer service window](/docs/guides/whatsapp/message-types#the-customer-service-window).
- **`preview_url` only affects the first link, and only what the recipient's client renders.** It defaults to `false`. Set it to preview the first URL in `body`; a later URL in the same body never gets one. If the recipient's client can't fetch a preview for that link, it falls back silently to a plain clickable link. Nothing on read tells you whether a preview actually rendered.
- **WhatsApp markdown is the recipient's client rendering `body`, not part of the API contract.** Bird passes `body` through untouched; it does not validate, strip, or encode `*bold*`, `_italic_`, `~strikethrough~`, or triple-backtick monospace. Whether those markers render is entirely up to the client that opens the message.
- **An inbound `body` is not guaranteed to be non-empty, despite what the read schema says.** Meta can report an inbound message as `"text": {}` or with an empty `body`, and Bird stores it verbatim rather than synthesizing a placeholder. This is a known, open gap: don't write a consumer that trusts the schema's `required: body` here.

## Next steps

- [WhatsApp service messages](/docs/guides/whatsapp/message-types): the customer service window and the model every service message shares
- [How sending works](/docs/guides/whatsapp/sending-whatsapp): the request envelope, the `202` model, and safe retries
- [Interactive messages](/docs/guides/whatsapp/message-types/interactive): when you want a tap instead of a typed reply