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 Cloud API throughput docs.

Next steps

  • Receive webhooks: Handle the delivery and read receipts.
  • Sandbox: Try the send flow end-to-end with a test number.