Send WhatsApp messages with AvenPing API
All
/v2/messagesroutes send through your linked WhatsApp line. ProvidephoneNumberIdplusto(phone) orrecipient(BSUID). Successful sends also upsert the contact in AvenPing.
Base: /v2/messages
All routes require auth, plan, Premium/Enterprise, and phoneNumberId ownership — see overview and Account.
Also read: HTTP API v2 Overview
Prerequisites
- An API token and Premium/Enterprise plan (Authentication)
- A
phoneNumberIdfrom GET /v2/me - A destination phone (
to) or Business-Scoped User ID (recipient)
Step-by-step
Authenticate
SendAuthorization: Bearer YOUR_TOKENorX-API-Token: YOUR_TOKEN.Set phoneNumberId
Use a Meta Phone number ID that belongs to a linked WABA. Missing:400 VALIDATION_ERROR. Not owned:403 FORBIDDEN.Choose to or recipient
Provideto(E.164-like phone; non-digits stripped) orrecipient(BSUID,CC.alphanumericorCC.ENT.alphanumeric). When both are sent, Meta prefers the phone. Authentication (OTP) templates requireto(Meta error 131062 for BSUID-only auth sends).POST the message body
Use a dedicated path (/send/text,/send/media, …) or smartPOST /v2/messages/send. Zod failures return400 VALIDATION_ERRORwithdetails. A blocked contact returns403 FORBIDDENand does not call Meta. Graph 4xx (other than 401/403/404) map toVALIDATION_ERRORwith Meta’s payload indetails.
Common POST fields
| Field | Type | Required | Notes |
|---|---|---|---|
phoneNumberId | string | yes | Meta phone number ID |
to | string | one of | E.164 digits (phone). Use when the contact’s phone is known |
recipient | string | one of | Business-Scoped User ID (BSUID), format CC.alphanumeric or CC.ENT.alphanumeric |
contextMessageWamid | string | no | Reply context |
Provide to or recipient. When both are sent, Meta prefers the phone (to). Authentication (OTP) templates require a phone and cannot be sent to BSUID-only contacts (Meta error 131062).
BSUID (Business-Scoped User ID)
Meta assigns a portfolio-scoped BSUID to each WhatsApp user. It appears in webhooks as user_id, from_user_id, or recipient_user_id. When a user adopts a username, their phone may be omitted from webhooks; use recipient for outbound sends in that case.
Webhook regression matrix (see scripts/fixtures/bsuid-webhooks/README.md in the main app repo):
- Inbound BSUID-only message creates a thread
- Status webhook with
recipient_user_idresolves the recipient user_id_updatemerges identity without duplicate contacts- Outbound BSUID-only send uses
recipient - Phone + BSUID merge on one contact row
- Auth template blocked for BSUID-only contacts
Typical success data (Meta send message)
All POST .../messages/... JSON endpoints wrap Meta’s response in { success: true, data }. A typical successful data value:
json
{
"messaging_product": "whatsapp",
"contacts": [
{
"input": "15551234567",
"wa_id": "15551234567"
}
],
"messages": [
{
"id": "wamid.HBgL..."
}
]
}On failure, Meta Graph errors are mapped to the same envelope as other routes: 4xx usually VALIDATION_ERROR (or 401/403/404), with Meta’s payload in details when present. Transport failures use 500/502 INTERNAL_ERROR.
Successful sends upsert the contact and inbox thread in AvenPing so the message appears in the dashboard. If that WhatsApp user is blocked on the line, the API returns 403 FORBIDDEN (Contact is blocked on this WhatsApp line) and does not call Meta.
POST /v2/messages/send/text
Body
| Field | Type | Required |
|---|---|---|
text | string | yes |
Example request:
bash
curl -sS -X POST "https://api.avenping.com/v2/messages/send/text" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"phoneNumberId": "109876543210987",
"to": "15551234567",
"text": "Hello from the API"
}'Response 200: envelope with data as in Typical success data (Meta send message).
POST /v2/messages/send/media
Body — media
| Field | Type | Required | Notes |
|---|---|---|---|
type | enum | yes | image, video, audio, document |
mediaId | string | yes | Meta media id |
caption | string | no | Not used for audio in payload |
filename | string | no | For document |
Example request:
bash
curl -sS -X POST "https://api.avenping.com/v2/messages/send/media" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"phoneNumberId": "109876543210987",
"to": "15551234567",
"media": {
"type": "image",
"mediaId": "1234567890",
"caption": "Check this out"
}
}'Response 200: same Meta send shape as above.
POST /v2/messages/send/location
Body — location
| Field | Type | Required |
|---|---|---|
latitude | string | number | yes |
longitude | string | number | yes |
name | string | no |
address | string | no |
Example request:
bash
curl -sS -X POST "https://api.avenping.com/v2/messages/send/location" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"phoneNumberId": "109876543210987",
"to": "15551234567",
"location": {
"latitude": 37.422,
"longitude": -122.084,
"name": "HQ",
"address": "1600 Amphitheatre Parkway"
}
}'Response 200: same Meta send shape.
POST /v2/messages/send/interactive/catalog-message
| Field | Type | Required |
|---|---|---|
bodyText | string | yes |
footerText | string | no |
thumbnailProductRetailerId | string | no |
Example request:
bash
curl -sS -X POST "https://api.avenping.com/v2/messages/send/interactive/catalog-message" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"phoneNumberId": "109876543210987",
"to": "15551234567",
"bodyText": "Browse our catalog",
"footerText": "Tap below"
}'Response 200: same Meta send shape.
POST /v2/messages/send/interactive/product
| Field | Type | Required |
|---|---|---|
catalogId | string | yes |
productRetailerId | string | yes |
bodyText | string | no |
footerText | string | no |
Example request:
bash
curl -sS -X POST "https://api.avenping.com/v2/messages/send/interactive/product" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"phoneNumberId": "109876543210987",
"to": "15551234567",
"catalogId": "CATALOG_ID",
"productRetailerId": "SKU-001",
"bodyText": "Featured item"
}'Response 200: same Meta send shape.
POST /v2/messages/send/interactive/product-list
| Field | Type | Required |
|---|---|---|
catalogId | string | yes |
headerText | string | yes |
bodyText | string | yes |
footerText | string | no |
sections | array | yes (min 1) |
Each section: { "title": string, "product_items": [ { "product_retailer_id": string } ] } (at least one product per section).
Example request:
bash
curl -sS -X POST "https://api.avenping.com/v2/messages/send/interactive/product-list" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"phoneNumberId": "109876543210987",
"to": "15551234567",
"catalogId": "CATALOG_ID",
"headerText": "Shop",
"bodyText": "Pick a product",
"sections": [
{
"title": "Featured",
"product_items": [{ "product_retailer_id": "SKU-001" }]
}
]
}'Response 200: same Meta send shape.
POST /v2/messages/send/interactive/product-carousel
| Field | Type | Required |
|---|---|---|
bodyText | string | yes |
cards | array | yes (min 2 cards) |
Each card: { "catalogId": string, "productRetailerId": string }.
Example request:
bash
curl -sS -X POST "https://api.avenping.com/v2/messages/send/interactive/product-carousel" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"phoneNumberId": "109876543210987",
"to": "15551234567",
"bodyText": "Swipe to browse",
"cards": [
{ "catalogId": "CATALOG_ID", "productRetailerId": "SKU-001" },
{ "catalogId": "CATALOG_ID", "productRetailerId": "SKU-002" }
]
}'Response 200: same Meta send shape.
POST /v2/messages/send/interactive/button
| Field | Type | Required |
|---|---|---|
bodyText | string | yes |
buttons | array | yes, 1–3 items |
Each button: { "id": string, "title": string } → sent as reply buttons.
Example request:
bash
curl -sS -X POST "https://api.avenping.com/v2/messages/send/interactive/button" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"phoneNumberId": "109876543210987",
"to": "15551234567",
"bodyText": "Choose one",
"buttons": [
{ "id": "yes", "title": "Yes" },
{ "id": "no", "title": "No" }
]
}'Response 200: same Meta send shape.
POST /v2/messages/send/interactive/list
| Field | Type | Required |
|---|---|---|
bodyText | string | yes |
buttonText | string | yes |
headerText | string | no |
footerText | string | no |
sections | array | yes (min 1) |
Each section: title + rows (min 1). Each row: id, title, optional description.
Example request:
bash
curl -sS -X POST "https://api.avenping.com/v2/messages/send/interactive/list" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"phoneNumberId": "109876543210987",
"to": "15551234567",
"bodyText": "Select an option",
"buttonText": "Open menu",
"headerText": "Menu",
"sections": [
{
"title": "Options",
"rows": [
{ "id": "1", "title": "Option A", "description": "Details" }
]
}
]
}'Response 200: same Meta send shape.
POST /v2/messages/send (smart dispatcher)
Single endpoint that infers message type from the JSON body (same base fields + optional contextMessageWamid):
| Condition | Dispatches to |
|---|---|
text is a string | Text send |
media is an object | Media send |
location is an object | Location send |
bodyText + catalogId + productRetailerId | Single product (note: bodyText must be present as string for this branch) |
bodyText + cards array | Product carousel |
bodyText + catalogId + sections array | Product list |
bodyText + (thumbnailProductRetailerId or footerText) | Catalog message |
bodyText + buttons array | Reply buttons |
bodyText + buttonText + sections array | List message |
Example request (text via smart send):
bash
curl -sS -X POST "https://api.avenping.com/v2/messages/send" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"phoneNumberId": "109876543210987",
"to": "15551234567",
"text": "Hello via smart send"
}'Response 200: same Meta send shape as dedicated text route.
Response 400 when no branch matches:
json
{
"success": false,
"error": "VALIDATION_ERROR",
"message": "Unsupported message payload."
}For interactive types, prefer the explicit path URLs above when documenting examples; use /send when you want one integration point.
FAQ
Should I use to or recipient?
Use to (phone) when you have the number. Use recipient (BSUID) when webhooks omit the phone (for example after a username). Authentication (OTP) templates require to. When both are sent, Meta prefers the phone.
What happens if the contact is blocked?
The API returns 403 FORBIDDEN (Contact is blocked on this WhatsApp line) and does not call Meta. Successful sends upsert the contact and inbox thread in AvenPing.
How are validation and Graph errors returned?
Zod schema failures return 400 VALIDATION_ERROR with details set to the issue list. Smart send returns 400 with message Unsupported message payload. when no branch matches. Meta Graph 401/403/404 keep those statuses; other Graph 4xx map to VALIDATION_ERROR with the Graph payload in details. Transport failures use 500/502 INTERNAL_ERROR.
Does a successful send appear in the AvenPing inbox?
Yes. Successful sends upsert the contact and inbox thread so the message appears in the dashboard.