Send messages with a normal Meta Cloud API request body, but pointed at the HookMyApp gateway. The gateway swaps your token for the real Meta credential server-side, so your Meta token never leaves HookMyApp.
The basic call
Every send is a POST to ${WHATSAPP_API_URL}/${WHATSAPP_PHONE_NUMBER_ID}/messages with Authorization: Bearer ${WHATSAPP_ACCESS_TOKEN} and a JSON body whose first key is messaging_product: "whatsapp".
Both the sandbox and your own number go through the HookMyApp gateway. The base-URL variable is the only difference: the sandbox env sets WHATSAPP_API_URL, your own number sets META_GRAPH_API_URL, and both point at the gateway (<gateway>/<graph-version>). The token is a scoped HookMyApp gateway token (hmat_…), not your raw Meta token. The gateway authenticates the hmat_ token, swaps in the real Meta token, and forwards to Meta. The Node example reads whichever base-URL variable is present (WHATSAPP_API_URL ?? META_GRAPH_API_URL). For the full message-object reference see Meta’s Cloud API docs.
curl example
curl -X POST "${WHATSAPP_API_URL}/${WHATSAPP_PHONE_NUMBER_ID}/messages" \
-H "Authorization: Bearer ${WHATSAPP_ACCESS_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"messaging_product": "whatsapp",
"to": "+15551234567",
"type": "text",
"text": { "body": "Hello from my app" }
}'
Node/Express example
Verbatim from the webhook starter kit (src/providers/whatsapp.js). It reads whichever base-URL variable your env has, so the same code runs against the sandbox and your own number.
export async function send(to, text) {
const base = process.env.WHATSAPP_API_URL ?? process.env.META_GRAPH_API_URL;
const url = `${base}/${process.env.WHATSAPP_PHONE_NUMBER_ID}/messages`;
const res = await fetch(url, {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.WHATSAPP_ACCESS_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ messaging_product: 'whatsapp', to, type: 'text', text: { body: text } }),
});
if (!res.ok) {
const err = await res.json().catch(() => ({}));
throw new Error(`WhatsApp API error ${res.status}: ${JSON.stringify(err)}`);
}
return res.json();
}
Templates need your own number
Sandbox blocks template sends. The sandbox proxy rejects type: "template" messages. Test templates against a connected WhatsApp number, not the sandbox.
Rate limits and retries
Meta enforces per-phone messaging tiers and per-account business-initiated-conversation quotas. HookMyApp passes rate-limit error responses through untouched so your retry logic sees them directly.
For current tier thresholds, pair rates, and the 429 retry contract, see Meta’s rate-limit docs.
Next steps