# Send on WhatsApp

> Reply to customers on WhatsApp from your code, inside WhatsApp's 24-hour reply window.

Source: https://docs.vlayers.ai/developers/send/whatsapp

Kind: How-to guide

## What you'll build

Code that answers a person who wrote to your WhatsApp business number, checks the reply window first, and handles a closed window.

## How WhatsApp differs

WhatsApp only lets a business message a person freely for **24 hours after that person's last message**. This is the reply window. Outside it, WhatsApp requires a pre-approved message template.

> **Warning:** Sending templates is not available yet. Today you can reply to people while their window is open; you cannot start a conversation on WhatsApp or message someone after their window closes. A template send is refused with `422 operation_not_supported` (`details.reason: "template_sends_not_available"`).

## Prerequisites

- A secret API key with `messages:send` and `messages:read`. See [Authentication and API keys](https://docs.vlayers.ai/get-started/authentication).
- A WhatsApp business number connected in the dashboard under **Channels**. It is a [Binding](https://docs.vlayers.ai/get-started/concepts#binding), your sender on WhatsApp.
- A conversation someone started by writing to that number. Its id comes from a `message.received` event ([webhooks](https://docs.vlayers.ai/developers/receive/webhooks) or the [event stream](https://docs.vlayers.ai/developers/receive/event-stream)) or from `GET /v1/conversations`.
- curl, Node.js 18 or later with `@voicelayer/api`, or Python 3.9 or later with `voicelayer`.

Save the conversation id first:

```bash
export CONVERSATION_ID=<the conversation's id>
```

## Step 1: Check the reply window

`GET /v1/conversations/{id}` returns the conversation. On WhatsApp it has a `window`: `open` says whether you may reply, and `closesAt` says until when.

cURL:

```bash
curl "https://api.vlayers.ai/v1/conversations/$CONVERSATION_ID" \
  -H "Authorization: Bearer $VOICELAYER_API_KEY"
```

TypeScript:

```ts
import { createVoiceLayerClient, getConversation } from '@voicelayer/api';

const client = createVoiceLayerClient({ apiKey: process.env.VOICELAYER_API_KEY!, throwOnError: true });

const { data } = await getConversation({
  client,
  path: {
    id: process.env.CONVERSATION_ID!
  }
});
console.log(data.conversation);
```

Python:

```python
import os
import voicelayer

config = voicelayer.Configuration(access_token=os.environ["VOICELAYER_API_KEY"])
with voicelayer.ApiClient(config) as client:
    api = voicelayer.ConversationsApi(client)
    result = api.get_conversation(
        id=os.environ["CONVERSATION_ID"],
    )
    print(result.conversation)
```

```json
{
  "conversation": {
    "id": "0192f5b0-7c3e-7a51-9b2d-4e8f1a6c3d21",
    "channel": "whatsapp",
    "controller": "api",
    "window": { "open": true, "closesAt": "2026-10-21T15:04:05.000Z" }
  }
}
```

## Step 2: Reply in the conversation

`POST /v1/conversations/{id}/messages` sends into the conversation. The channel, your WhatsApp number and the person come from the conversation.

cURL:

```bash
curl -X POST "https://api.vlayers.ai/v1/conversations/$CONVERSATION_ID/messages" \
  -H "Authorization: Bearer $VOICELAYER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "content": {
      "type": "text",
      "text": "Yes, we have a table at 7pm. Shall I book it?"
    }
  }'
```

TypeScript:

```ts
import { createVoiceLayerClient, replyToConversation } from '@voicelayer/api';

const client = createVoiceLayerClient({ apiKey: process.env.VOICELAYER_API_KEY!, throwOnError: true });

const { data } = await replyToConversation({
  client,
  path: {
    id: process.env.CONVERSATION_ID!
  },
  body: {
    content: {
      type: 'text',
      text: 'Yes, we have a table at 7pm. Shall I book it?'
    }
  }
});
console.log(data.message.id);
```

Python:

```python
import os
import voicelayer

config = voicelayer.Configuration(access_token=os.environ["VOICELAYER_API_KEY"])
with voicelayer.ApiClient(config) as client:
    api = voicelayer.ConversationsApi(client)
    result = api.reply_to_conversation(
        id=os.environ["CONVERSATION_ID"],
        reply_to_conversation_request=voicelayer.ReplyToConversationRequest.from_dict({
            "content": {
                "type": "text",
                "text": "Yes, we have a table at 7pm. Shall I book it?",
            },
        }),
    )
    print(result.message.id)
```

Response `202`:

```json
{
  "message": {
    "id": "0192f5b0-8d1f-7b62-8c3e-5f9a2b7d4e32",
    "conversationId": "0192f5b0-7c3e-7a51-9b2d-4e8f1a6c3d21",
    "channel": "sms",
    "direction": "outbound",
    "status": "queued",
    "text": "Yes, we have a table at 7pm. Shall I book it?",
    "media": [],
    "from": "+14155550100",
    "to": "+15550123",
    "bindingId": null,
    "metadata": {},
    "error": null,
    "createdAt": "2026-10-20T15:04:05.000Z",
    "parts": [
      {
        "id": "0192f5b0-8d1f-7b62-8c3e-5f9a2b7d4e32",
        "status": "queued"
      }
    ]
  }
}
```

A long text is sent as several messages (up to 4,096 characters each).

## Step 3: Handle a closed window

If the window has closed, the reply is refused with `409 window_closed` and nothing is sent. Wait for the person to write again: their next message reopens the window for another 24 hours. Tell your users or your agent to ask for what they need while the window is open.

## Test it

In test mode, a reserved number plays the customer. Without a connected WhatsApp number, your free [test sender](https://docs.vlayers.ai/developers/test-mode#your-free-test-sender) receives it, but it has no reply window, so Step 1 shows no `window`. To test the 24-hour window itself, connect your WhatsApp number and name it in `to`:

cURL:

```bash
curl -X POST https://api.vlayers.ai/v1/test/inbound \
  -H "Authorization: Bearer $VOICELAYER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "channel": "whatsapp",
    "from": "+15550124",
    "text": "Hi, can I change my delivery address?"
  }'
```

TypeScript:

```ts
import { createVoiceLayerClient, createTestInbound } from '@voicelayer/api';

const client = createVoiceLayerClient({ apiKey: process.env.VOICELAYER_API_KEY!, throwOnError: true });

const { data } = await createTestInbound({
  client,
  body: {
    channel: 'whatsapp',
    from: '+15550124',
    text: 'Hi, can I change my delivery address?'
  }
});
console.log(data.conversationId);
```

Python:

```python
import os
import voicelayer

config = voicelayer.Configuration(access_token=os.environ["VOICELAYER_API_KEY"])
with voicelayer.ApiClient(config) as client:
    api = voicelayer.TestApi(client)
    result = api.create_test_inbound(
        create_test_inbound_request=voicelayer.CreateTestInboundRequest.from_dict({
            "channel": "whatsapp",
            "from": "+15550124",
            "text": "Hi, can I change my delivery address?",
        }),
    )
    print(result.conversation_id)
```

Save the returned `conversationId` as `CONVERSATION_ID` and run Steps 1 and 2. The reply goes to a simulated carrier and costs nothing. See [Test mode](https://docs.vlayers.ai/developers/test-mode).

## Templates you have

`GET /v1/channels/whatsapp/templates` lists the templates Meta approved for a WhatsApp number, with their variables. It is read only for now; use it to prepare for template sends.

cURL:

```bash
curl "https://api.vlayers.ai/v1/channels/whatsapp/templates?bindingId=$BINDING_ID" \
  -H "Authorization: Bearer $VOICELAYER_API_KEY"
```

TypeScript:

```ts
import { createVoiceLayerClient, listWhatsAppTemplates } from '@voicelayer/api';

const client = createVoiceLayerClient({ apiKey: process.env.VOICELAYER_API_KEY!, throwOnError: true });

const { data } = await listWhatsAppTemplates({
  client,
  query: {
    bindingId: process.env.BINDING_ID!
  }
});
console.log(data.templates);
```

Python:

```python
import os
import voicelayer

config = voicelayer.Configuration(access_token=os.environ["VOICELAYER_API_KEY"])
with voicelayer.ApiClient(config) as client:
    api = voicelayer.ChannelsApi(client)
    result = api.list_whats_app_templates(
        binding_id=os.environ["BINDING_ID"],
    )
    print(result.templates)
```

## Common errors

| Error | Status | How to fix |
|---|---|---|
| [`window_closed`](https://docs.vlayers.ai/errors/window_closed) | 409 | Wait for the person to write again. Template sends, the way to reach someone outside the window, are not available yet (`operation_not_supported`). |
| [`operation_not_supported`](https://docs.vlayers.ai/errors/operation_not_supported) | 422 | Send text instead (inside the conversation's window, on windowed channels). |
| [`capability_not_ready`](https://docs.vlayers.ai/errors/capability_not_ready) | 409 | Fix the connection in the dashboard (Channels) using `details.remediation` when present, then send again. |
| [`no_sender`](https://docs.vlayers.ai/errors/no_sender) | 409 | Connect a sender for the channel in the dashboard (for email, create a mailbox), or name one in `from`. |
| [`not_found`](https://docs.vlayers.ai/errors/not_found) | 404 | Check the id, and that the key belongs to the workspace (and covers the channel) that owns it. |
| [`recipient_not_allowed`](https://docs.vlayers.ai/errors/recipient_not_allowed) | 403 | Reply in an existing conversation instead, or wait for the person to write first. |

## Next steps

- [Conversations and handoff](https://docs.vlayers.ai/developers/handoff): let a person take over a WhatsApp conversation.
- [Receive with webhooks](https://docs.vlayers.ai/developers/receive/webhooks): get the `message.received` event that starts each conversation.
- [Send on Telegram](https://docs.vlayers.ai/developers/send/telegram).
