Pipes.bot

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

FieldTypeDescription
conversationIdstringConversation ID from the incoming message
poolNumberIdstring?Ignored for routing. The server always routes via the conversation's stored pool number. Safe to omit
toNumberstringRecipient's WhatsApp number
textstringReply 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:

CodeMeaning
SERVICE_WINDOW_EXPIREDLast inbound is older than 24 hours (non-BYON only)
CONVERSATION_SCOPE_MISMATCHReply targets a conversation outside a conversation-scoped connection
POOL_NUMBER_SCOPE_MISMATCHReply 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!" }
  }
}
FieldTypeDescription
typestringAlways "send"
requestIdstringYour correlation ID. Echoed back in send_result
poolNumberIdstringThe BYON number to send from
messageobjectMeta /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.codeMeaning
INVALID_REQUESTrequestId, poolNumberId or message is missing or malformed
UNSUPPORTED_IN_LEGACYSent on a legacy connection. send is v2 only
NOT_FOUNDBYON number not found
FORBIDDENThe number belongs to another App, or your connection is scoped to a different number
REGISTRATION_PENDINGThe number's registration isn't finished yet
NO_CREDENTIALSThe number has no access token configured
META_UNREACHABLEThe request to Meta failed
META_INVALID_RESPONSEMeta returned a non-JSON or unexpected response
META_ERRORMeta rejected the message without a response body (see below)
INTERNAL_ERRORUnexpected 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_status with code: "SERVICE_WINDOW_EXPIRED"
  • REST API replies receive HTTP 403 with code: "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}`);
    }
  }
});

On this page