Passthrough Messages
Send template messages, interactive buttons, lists, CTA URL buttons, and other advanced WhatsApp message types via the passthrough endpoint.
The passthrough endpoint forwards your message payload directly to Meta's Cloud API, giving you access to every WhatsApp message type — templates, interactive buttons, list messages, locations, contacts, and reactions.
POST /v1/messages/passthrough
Authorization: Bearer YOUR_API_KEY
Content-Type: application/jsonBoth pool (pk_) and BYON (ak_) keys are accepted, but template messages require a BYON ak_ key.
Simple text messages
For sending plain text messages on BYON numbers, use the dedicated send endpoint:
POST /v1/messages/app/send
Authorization: Bearer ak_your_app_key
Content-Type: application/json| Field | Type | Description |
|---|---|---|
poolNumberId | string | Required. Your BYON number ID |
toNumber | string | Required on legacy. Recipient phone number, or a BSUID. On API v2 you can use to instead |
to | string? | API v2 only. Phone number or BSUID. Alias of toNumber; wins if both are sent |
text | string? | Message text. Either text or media must be provided |
media | string? | Media ID, HTTP(S) URL, or base64 data (auto-detected) |
mimeType | string? | MIME type. Required for base64 media |
fileName | string? | Optional file name for media |
caption | string? | Media caption (max 1024 chars) |
curl -X POST https://api.pipes.bot/v1/messages/app/send \
-H "Authorization: Bearer ak_your_app_key" \
-H "Content-Type: application/json" \
-d '{
"poolNumberId": "pn_abc123",
"toNumber": "5511999999999",
"text": "Hello! Your order has been shipped."
}'On the legacy API the response is the flat Pipes.bot send shape:
{
"success": true,
"messageId": "wamid.abc123...",
"conversationId": "conv_xyz789"
}On API v2 nothing is stored and the response is Meta's, trimmed to two fields:
{
"messageId": "wamid.abc123...",
"contacts": [{ "input": "US.13491208655302741918", "user_id": "US.13491208655302741918" }]
}contacts is Meta's contacts array. When you message a BSUID, Meta answers with input and user_id and, in our tests, no wa_id: don't expect the send response to reveal a phone number. When you message a phone number, you get input and wa_id. If Meta rejects the message (for example with error 131062), Meta's error body is returned as-is with Meta's HTTP status. v2 adds these errors: 500 NO_CREDENTIALS (the number has no access token), 502 META_UNREACHABLE and 502 META_INVALID_RESPONSE.
For templates, interactive messages, location, contacts, and reactions, use the passthrough endpoint below.
Routing fields
The routing fields depend on your API key type and, for BYON, on your App's API version.
BYON (ak_ key), legacy:
| Field | Type | Description |
|---|---|---|
messaging_product | string | Required. Always "whatsapp" |
poolNumberId | string | Required. Your BYON number ID |
to | string | Required. Recipient phone number or BSUID |
type | string | Required. Message type |
poolNumberId and to are stripped, and Pipes.bot sets the recipient for Meta: to for a phone number, recipient for a BSUID.
BYON (ak_ key), API v2:
| Field | Type | Description |
|---|---|---|
messaging_product | string | Required. Always "whatsapp" |
poolNumberId | string | Required. Your BYON number ID |
to / recipient | string | Required. to for a phone number, recipient for a BSUID |
type | string | Required. Message type |
Only poolNumberId is stripped. The rest of the body is forwarded to Meta untouched.
Pool (pk_ key):
| Field | Type | Description |
|---|---|---|
conversationId | string | Required. Target conversation |
type | string | Required. Message type |
Sending template messages
Send a pre-approved template with dynamic parameters. Templates let you initiate conversations outside the 24-hour service window.
curl -X POST https://api.pipes.bot/v1/messages/passthrough \
-H "Authorization: Bearer ak_your_app_key" \
-H "Content-Type: application/json" \
-d '{
"messaging_product": "whatsapp",
"type": "template",
"poolNumberId": "pn_abc123",
"to": "5511999999999",
"template": {
"name": "order_confirmation",
"language": { "code": "en_US" },
"components": [
{
"type": "body",
"parameters": [
{ "type": "text", "text": "Maria" },
{ "type": "text", "text": "ORD-4521" },
{ "type": "text", "text": "January 20" }
]
}
]
}
}'Template with media header
{
"messaging_product": "whatsapp",
"type": "template",
"poolNumberId": "pn_abc123",
"to": "5511999999999",
"template": {
"name": "shipping_update",
"language": { "code": "en_US" },
"components": [
{
"type": "header",
"parameters": [
{
"type": "image",
"image": { "link": "https://example.com/tracking-map.jpg" }
}
]
},
{
"type": "body",
"parameters": [
{ "type": "text", "text": "ORD-4521" },
{ "type": "text", "text": "Jan 20, 2025" }
]
}
]
}
}Template with button parameters
For templates with dynamic URL buttons, pass the variable portion:
{
"messaging_product": "whatsapp",
"type": "template",
"poolNumberId": "pn_abc123",
"to": "5511999999999",
"template": {
"name": "order_tracking",
"language": { "code": "en_US" },
"components": [
{
"type": "body",
"parameters": [
{ "type": "text", "text": "ORD-4521" }
]
},
{
"type": "button",
"sub_type": "url",
"index": "0",
"parameters": [
{ "type": "text", "text": "ORD-4521" }
]
}
]
}
}Interactive reply buttons
Send a message with up to 3 reply buttons. When the user taps a button, you receive a webhook with the button ID.
curl -X POST https://api.pipes.bot/v1/messages/passthrough \
-H "Authorization: Bearer ak_your_app_key" \
-H "Content-Type: application/json" \
-d '{
"messaging_product": "whatsapp",
"type": "interactive",
"poolNumberId": "pn_abc123",
"to": "5511999999999",
"interactive": {
"type": "button",
"header": {
"type": "text",
"text": "Confirm your appointment"
},
"body": {
"text": "You have an appointment on Jan 20 at 3:00 PM. Would you like to confirm?"
},
"footer": {
"text": "Reply within 24 hours"
},
"action": {
"buttons": [
{
"type": "reply",
"reply": { "id": "confirm", "title": "Confirm" }
},
{
"type": "reply",
"reply": { "id": "reschedule", "title": "Reschedule" }
},
{
"type": "reply",
"reply": { "id": "cancel", "title": "Cancel" }
}
]
}
}
}'Button constraints
- Max 3 buttons per message
- Button
titlemax 20 characters - Button
idmax 256 characters
Interactive list messages
Send a message with a selectable list of up to 10 items grouped into sections.
{
"messaging_product": "whatsapp",
"type": "interactive",
"poolNumberId": "pn_abc123",
"to": "5511999999999",
"interactive": {
"type": "list",
"header": {
"type": "text",
"text": "Our Menu"
},
"body": {
"text": "Browse our menu and pick your items:"
},
"footer": {
"text": "Prices may vary"
},
"action": {
"button": "View Menu",
"sections": [
{
"title": "Pizzas",
"rows": [
{ "id": "margherita", "title": "Margherita", "description": "$12.00" },
{ "id": "pepperoni", "title": "Pepperoni", "description": "$14.00" }
]
},
{
"title": "Drinks",
"rows": [
{ "id": "cola", "title": "Cola", "description": "$3.00" },
{ "id": "water", "title": "Water", "description": "$2.00" }
]
}
]
}
}
}List constraints
- Max 10 rows total across all sections
- Max 10 sections
- Row
titlemax 24 characters - Row
descriptionmax 72 characters - Action
buttontext max 20 characters
Interactive CTA URL buttons
Send a message with a call-to-action button that opens a URL when tapped.
curl -X POST https://api.pipes.bot/v1/messages/passthrough \
-H "Authorization: Bearer ak_your_app_key" \
-H "Content-Type: application/json" \
-d '{
"messaging_product": "whatsapp",
"type": "interactive",
"poolNumberId": "pn_abc123",
"to": "5511999999999",
"interactive": {
"type": "cta_url",
"header": {
"type": "text",
"text": "Visit our store"
},
"body": {
"text": "Check out our latest products and promotions."
},
"footer": {
"text": "Free shipping on orders over $50"
},
"action": {
"name": "cta_url",
"parameters": {
"display_text": "Shop Now",
"url": "https://example.com/store"
}
}
}
}'CTA URL fields
| Field | Type | Description |
|---|---|---|
interactive.type | string | Must be "cta_url" |
interactive.header | object | Optional. Text header |
interactive.body | object | Required. Message body text |
interactive.footer | object | Optional. Footer text |
interactive.action.name | string | Required. Must be "cta_url" |
action.parameters.display_text | string | Required. Button label (max 20 characters) |
action.parameters.url | string | Required. HTTPS URL to open |
CTA URL constraints
display_textmax 20 charactersurlmust use HTTPS
Sending location messages
{
"messaging_product": "whatsapp",
"type": "location",
"poolNumberId": "pn_abc123",
"to": "5511999999999",
"location": {
"latitude": "-23.5505",
"longitude": "-46.6333",
"name": "Pipes.bot HQ",
"address": "Av. Paulista, 1000 - São Paulo"
}
}Sending contact cards
{
"messaging_product": "whatsapp",
"type": "contacts",
"poolNumberId": "pn_abc123",
"to": "5511999999999",
"contacts": [
{
"name": { "formatted_name": "Support Team", "first_name": "Support" },
"phones": [
{ "phone": "+15551234567", "type": "WORK" }
],
"emails": [
{ "email": "support@example.com", "type": "WORK" }
]
}
]
}Sending reactions
React to a received message with an emoji:
{
"messaging_product": "whatsapp",
"type": "reaction",
"poolNumberId": "pn_abc123",
"to": "5511999999999",
"reaction": {
"message_id": "wamid.abc123...",
"emoji": "👍"
}
}To remove a reaction, send an empty emoji string.
Response
All passthrough responses are returned directly from Meta:
{
"messaging_product": "whatsapp",
"contacts": [
{ "input": "5511999999999", "wa_id": "5511999999999" }
],
"messages": [
{ "id": "wamid.abc123..." }
]
}Error handling
Meta errors are forwarded as-is. Common errors:
| Meta error code | Meaning |
|---|---|
131047 | Re-engagement message outside window |
131026 | Message undeliverable (user not on WA) |
131053 | Media upload failed |
132000 | Template parameter count mismatch |
132015 | Template is paused |
132012 | Template parameter format mismatch |
Pipes.bot-level errors:
| Status | Code | Description |
|---|---|---|
400 | INVALID_REQUEST | Missing type, routing fields, or invalid JSON |
401 | UNAUTHORIZED | Missing or invalid API key |
403 | BYON_ONLY | Template messages require a BYON ak_ key |
403 | WINDOW_CLOSED | Service window expired (pool users only) |
404 | NOT_FOUND | Conversation or BYON number not found |
403 | FORBIDDEN | Number not owned by your App, or conversation not yours |
429 | RATE_LIMITED | 60 requests/minute exceeded |
500 | NO_CREDENTIALS | BYON number has no Meta access token configured |
502 | META_UNREACHABLE | Could not reach Meta's API |
502 | META_INVALID_RESPONSE | Meta returned an unparseable/invalid response |
BSUIDs and usernames
WhatsApp users can adopt a username and hide their phone number. When they message you, WhatsApp identifies them with a BSUID (business-scoped user ID) instead:
- Format:
CC.<id>, for exampleUS.13491208655302741918. Parent BSUIDs (opt-in, for multiple business portfolios) look likeCC.ENT.<id>. - A BSUID is unique per business portfolio and user. It changes if the user changes their phone number, but not when they change their username.
- The phone number is omitted when the user has a username, your number hasn't messaged them in the last 30 days, and they aren't in your Meta-hosted contact book. Missing fields are omitted, never empty strings.
You don't need to do anything special to reply: pass the BSUID wherever you would pass a phone number.
| Endpoint | Field |
|---|---|
POST /v1/messages/app/send | toNumber (or to on v2) |
POST /v1/messages/passthrough (legacy) | to |
POST /v1/messages/passthrough (v2) | to or recipient |
{
"poolNumberId": "pn_abc123",
"toNumber": "US.13491208655302741918",
"text": "Thanks for reaching out!"
}Pipes.bot sends BSUIDs to Meta in the recipient field and phone numbers in to. On the v2 passthrough the body is forwarded untouched, so set messaging_product and to or recipient yourself.
On the legacy API a user's identity is fromNumber, which is the phone number or, when there isn't one, the BSUID. If a phone number appears or disappears for the same user, their fromNumber changes and you get a new conversationId. API v2 stores no conversations, so you key your own records on the BSUID or wa_id in the event. For identity-change events, see BYON API v2 migration.
Asking for a phone number
If you need a phone number (for example to place a call or to send an authentication template), ask for it with REQUEST_CONTACT_INFO. The user chooses whether to share it.
As a template button. Add a REQUEST_CONTACT_INFO button when you create the template:
{
"name": "ask_contact",
"language": "en_US",
"category": "UTILITY",
"components": [
{ "type": "BODY", "text": "Share your phone number so we can follow up." },
{ "type": "BUTTONS", "buttons": [{ "type": "REQUEST_CONTACT_INFO" }] }
]
}As an interactive message, inside the 24-hour service window:
{
"poolNumberId": "pn_abc123",
"messaging_product": "whatsapp",
"recipient": "US.13491208655302741918",
"type": "interactive",
"interactive": {
"type": "request_contact_info",
"body": { "text": "Share your phone number so we can follow up." },
"action": { "name": "request_contact_info" }
}
}Send it through POST /v1/messages/passthrough (or a v2 send frame). On API v2 Pipes.bot forwards the body unchanged. On legacy, put the BSUID in to instead of recipient.
The reply arrives as a normal inbound message with type: "contacts". When the user shared the info because you asked, contacts[].origin is contact_request (otherwise other):
{
"type": "contacts",
"contacts": [
{
"origin": "contact_request",
"name": { "formatted_name": "Jane Doe" },
"phones": [{ "phone": "+1 555-987-6543", "wa_id": "15559876543", "type": "CELL" }],
"vcard": "QkVHSU46VkNBUkQKVkVSU0lPTjozLjAK..."
}
]
}Store the mapping from the BSUID (the message's from/from_user_id) to phones[].wa_id. phones[].phone is formatted for display; use wa_id as the number. vcard is a base64-encoded vCard of the same contact. See the full API v2 example payload.
Error 131062
Meta returns error 131062 when you send a one-tap, zero-tap or copy-code authentication template to a user whose phone number isn't known. These templates need a phone number. Ask for it with REQUEST_CONTACT_INFO first, then send the template to the shared number.
Complete example
Send a template with buttons, then handle the button reply:
const API_KEY = "ak_your_app_key";
const BASE = "https://api.pipes.bot";
// Send a template message
const res = await fetch(`${BASE}/v1/messages/passthrough`, {
method: "POST",
headers: {
Authorization: `Bearer ${API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
messaging_product: "whatsapp",
type: "template",
poolNumberId: "pn_abc123",
to: "5511999999999",
template: {
name: "appointment_reminder",
language: { code: "en_US" },
components: [
{
type: "body",
parameters: [
{ type: "text", text: "Dr. Smith" },
{ type: "text", text: "January 20 at 3:00 PM" },
],
},
],
},
}),
});
const result = await res.json();
console.log("Sent template:", result.messages?.[0]?.id);When the user replies to a button, you receive a webhook event:
{
"type": "whatsapp_message",
"data": {
"type": "interactive",
"messageId": "wamid.xyz...",
"fromNumber": "5511999999999",
"interactive": {
"type": "button_reply",
"button_reply": {
"id": "confirm",
"title": "Confirm"
}
}
}
}Then send a follow-up interactive message:
await fetch(`${BASE}/v1/messages/passthrough`, {
method: "POST",
headers: {
Authorization: `Bearer ${API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
messaging_product: "whatsapp",
type: "interactive",
poolNumberId: "pn_abc123",
to: "5511999999999",
interactive: {
type: "button",
body: {
text: "Your appointment is confirmed! Would you like a reminder?",
},
action: {
buttons: [
{ type: "reply", reply: { id: "remind_1h", title: "1 hour before" } },
{ type: "reply", reply: { id: "remind_1d", title: "1 day before" } },
{ type: "reply", reply: { id: "no_remind", title: "No thanks" } },
],
},
},
}),
});