Sending Replies
Reply to WhatsApp messages through the WebSocket connection and REST API.
You can send replies through the WebSocket connection (text only) or the REST API (text and media).
Replying via WebSocket
This section applies to legacy Apps and pool numbers. On BYON API v2, see Sending via WebSocket on API v2.
Send a whatsapp_reply message through the open connection:
{
"type": "whatsapp_reply",
"data": {
"conversationId": "conv_xyz789",
"poolNumberId": "pool_123",
"toNumber": "+15559876543",
"text": "Thanks for reaching out!"
}
}Reply fields
| Field | Type | Description |
|---|---|---|
conversationId | string | Conversation ID from the incoming message |
poolNumberId | string? | Ignored for routing. The server always routes via the conversation's stored pool number. Safe to omit |
toNumber | string | Recipient's WhatsApp number |
text | string | Reply text (max 4096 characters) |
Reply confirmation
After sending a reply, the server responds with a reply_status event:
Success:
{
"type": "reply_status",
"data": {
"conversationId": "conv_xyz789",
"success": true,
"messageId": "wamid.abc123..."
}
}Failure:
{
"type": "reply_status",
"data": {
"conversationId": "conv_xyz789",
"success": false,
"error": "Service window expired — last inbound message is older than 24 hours.",
"code": "SERVICE_WINDOW_EXPIRED"
}
}reply_status failures can carry these codes:
| Code | Meaning |
|---|---|
SERVICE_WINDOW_EXPIRED | Last inbound is older than 24 hours (non-BYON only) |
CONVERSATION_SCOPE_MISMATCH | Reply targets a conversation outside a conversation-scoped connection |
POOL_NUMBER_SCOPE_MISMATCH | Reply targets a conversation on a different pool number than the connection's scope |
A Meta-side send failure returns a reply_status with an error message but no code.
Reply scoping
Reply behavior depends on your connection scope:
- User-scoped connections can reply to any conversation you own
- Pool-number-scoped connections can only reply to conversations belonging to the scoped pool number — replies targeting a conversation on a different pool number are rejected
- Conversation-scoped connections can only reply to the conversation they are scoped to — replies targeting a different conversationId are rejected
Replying via REST API
For text and media replies, use the Messages API:
POST /v1/messages/send
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json{
"conversationId": "conv_xyz789",
"text": "Here's the info you requested"
}The REST API is not affected by connection scoping — you can send to any conversation you own regardless of your WebSocket connections.
See Media Handling for sending images, audio, video, and documents.
Sending via WebSocket on API v2
Apps on BYON API v2 have no conversations, so whatsapp_reply is replaced by a send frame. The message is the body you would send to Meta's /messages endpoint, forwarded as-is, so include messaging_product and the recipient (to for a phone number, recipient for a BSUID):
{
"type": "send",
"requestId": "req_001",
"poolNumberId": "pn_abc123",
"message": {
"messaging_product": "whatsapp",
"recipient_type": "individual",
"to": "15559876543",
"type": "text",
"text": { "body": "Thanks for reaching out!" }
}
}| Field | Type | Description |
|---|---|---|
type | string | Always "send" |
requestId | string | Your correlation ID. Echoed back in send_result |
poolNumberId | string | The BYON number to send from |
message | object | Meta /messages request body (text, media, template, interactive) |
The server answers every send with a send_result carrying the same requestId.
Success:
{
"type": "send_result",
"requestId": "req_001",
"success": true,
"messageId": "wamid.HBgL...",
"contacts": [{ "input": "15559876543", "wa_id": "15559876543" }]
}contacts is Meta's contacts array. Sending to a phone number returns input and wa_id; sending to a BSUID (recipient) returns input and user_id.
Failure:
{
"type": "send_result",
"requestId": "req_001",
"success": false,
"error": { "code": "NOT_FOUND", "message": "BYON number not found" }
}error.code | Meaning |
|---|---|
INVALID_REQUEST | requestId, poolNumberId or message is missing or malformed |
UNSUPPORTED_IN_LEGACY | Sent on a legacy connection. send is v2 only |
NOT_FOUND | BYON number not found |
FORBIDDEN | The number belongs to another App, or your connection is scoped to a different number |
REGISTRATION_PENDING | The number's registration isn't finished yet |
NO_CREDENTIALS | The number has no access token configured |
META_UNREACHABLE | The request to Meta failed |
META_INVALID_RESPONSE | Meta returned a non-JSON or unexpected response |
META_ERROR | Meta rejected the message without a response body (see below) |
INTERNAL_ERROR | Unexpected server error |
When Meta rejects a message with a JSON body, error is Meta's own error object (for example { "message": "...", "code": 131062, ... }), so you can act on Meta's numeric code. In that case error.code is a number, not one of the strings above. You get the string META_ERROR only when Meta's rejection has no body. Handle both: switch on typeof error.code.
Pipes.bot does not check the 24-hour service window on v2; Meta enforces it and returns its own error.
On a v2 connection, a legacy whatsapp_reply frame is refused with a reply_status event carrying code: "UNSUPPORTED_IN_V2".
24-hour service window
WhatsApp enforces a 24-hour messaging window. You can only send outbound messages within 24 hours of the customer's last inbound message. After the window expires:
- WebSocket replies receive a
reply_statuswithcode: "SERVICE_WINDOW_EXPIRED" - REST API replies receive HTTP
403withcode: "SERVICE_WINDOW_EXPIRED"
The window resets each time the customer sends a new message.
BYON numbers are exempt from the platform-side 24-hour window check — replies on BYON conversations always pass it.
Conversation ownership
Replies are validated against conversation ownership. You can only reply to conversations that belong to your account. Attempting to reply to another tenant's conversation is silently rejected.
Example: echo bot
ws.on("message", (raw) => {
const event = JSON.parse(raw.toString());
if (event.type === "whatsapp_message") {
const msg = event.data;
// Echo text messages back
if (msg.type === "text") {
ws.send(JSON.stringify({
type: "whatsapp_reply",
data: {
conversationId: msg.conversationId,
poolNumberId: msg.poolNumberId,
toNumber: msg.fromNumber,
text: `You said: ${msg.body}`,
},
}));
}
}
if (event.type === "reply_status") {
if (!event.data.success) {
console.error(`Reply failed: ${event.data.error}`);
}
}
});