InboxWAA QUIET INTELLIGENCE
Developers · API integration

WhatsApp Business API Integration: Send a Message and Verify Webhooks in PHP, Python, Node.js and curl

Last updated: October 2026

Three steps from nothing to a sent message, then the part most integrations get wrong: verifying webhooks. The samples below are taken from the InboxWA API reference, so they match the real endpoints and field names.

Start free trial 7-day free trial, no card required

What you need first

  • A plan with API access. API keys are on Growth and Scale. Starter has no API and no webhooks. Growth has the core API (send messages and broadcasts, contacts, templates) and five webhook events; Scale has every call, the Reporting API and all 11 events.
  • A WhatsApp Cloud API connection on the workspace, and an approved message template for business-initiated messages.
  • A key with the right scope. To send messages the key needs the whatsapp_api scope (or full access).

The base URL is https://inboxwa.online. Requests and responses are JSON. Send phone numbers with the country code, digits only, for example 919876543210.

Step 1: create a key and send it as a bearer token

A workspace admin creates keys on the API Keys page. The plaintext is shown once and only its hash is stored. Send it on every request as Authorization: Bearer iwk_.... Keep keys on your server, never in a browser page or a mobile app, and give each integration its own scoped key so one can be revoked alone.

Step 2: check the key, then send a template message

GET /api/reporting/v1/whoami returns the workspace, the scopes and what your plan includes. POST /api/send-message sends a message: a template, or free text inside the 24-hour window.

curl

# 1. Check the key
curl https://inboxwa.online/api/reporting/v1/whoami \
  -H "Authorization: Bearer iwk_yourkeyhere"

# 2. Send a template message
curl -X POST https://inboxwa.online/api/send-message \
  -H "Authorization: Bearer iwk_yourkeyhere" \
  -H "Content-Type: application/json" \
  -d '{"to":"919876543210","type":"template","template":{"name":"order_confirmation","language":"en_US","parameters":["ORD-4271","2026-10-05"]}}'

Node.js

const headers = {
  Authorization: "Bearer " + process.env.INBOXWA_KEY,
  "Content-Type": "application/json",
};

const who = await fetch("https://inboxwa.online/api/reporting/v1/whoami", { headers });
console.log(await who.json());

const res = await fetch("https://inboxwa.online/api/send-message", {
  method: "POST",
  headers,
  body: JSON.stringify({
    to: "919876543210",
    type: "template",
    template: { name: "order_confirmation", language: "en_US", parameters: ["ORD-4271", "2026-10-05"] },
  }),
});
const out = await res.json();
if (!res.ok) throw new Error(out.error);
console.log(out.messageId);

Python

import os, requests

headers = {"Authorization": "Bearer " + os.environ["INBOXWA_KEY"]}
print(requests.get("https://inboxwa.online/api/reporting/v1/whoami", headers=headers, timeout=30).json())

r = requests.post(
    "https://inboxwa.online/api/send-message",
    headers=headers,
    json={
        "to": "919876543210",
        "type": "template",
        "template": {"name": "order_confirmation", "language": "en_US", "parameters": ["ORD-4271", "2026-10-05"]},
    },
    timeout=30,
)
r.raise_for_status()
print(r.json()["messageId"])

PHP

function inboxwa(string $method, string $path, ?array $body = null): array {
  $ch = curl_init("https://inboxwa.online" . $path);
  curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST => $method,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => ["Authorization: Bearer " . getenv("INBOXWA_KEY"), "Content-Type: application/json"],
    CURLOPT_POSTFIELDS => $body ? json_encode($body) : null,
  ]);
  return json_decode(curl_exec($ch), true);
}

print_r(inboxwa("GET", "/api/reporting/v1/whoami"));
$out = inboxwa("POST", "/api/send-message", [
  "to" => "919876543210",
  "type" => "template",
  "template" => ["name" => "order_confirmation", "language" => "en_US", "parameters" => ["ORD-4271", "2026-10-05"]],
]);
echo $out["messageId"];

Errors and rate limits you will meet

  • 401 means no credential was sent. 403 means the key does not exist, was revoked or expired (key_expired), or the call is outside your plan (for example api_not_in_plan). A 403 is not a reason to retry.
  • The default limit is 600 requests per minute per key. Responses carry X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset, and a 429 carries Retry-After.
  • Sending is metered by your credits and WhatsApp's own limits, not by the request-rate limit.

Webhooks: verify the signature before you trust anything

Register one HTTPS endpoint and choose events. Each event is a POST with a JSON envelope (event, sent_at, correlation_id, data) and an X-Webhook-Signature header: sha256= followed by the hex HMAC-SHA256 of the raw request body, keyed with your signing secret. Compute it over the exact bytes you received, before any JSON parsing, and compare in constant time.

Node.js

const crypto = require("crypto");

// rawBody must be the exact bytes received (express.raw), not re-serialised JSON
function verify(rawBody, header, secret) {
  const expected = "sha256=" +
    crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
  const a = Buffer.from(header || "");
  const b = Buffer.from(expected);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

Python

import hmac, hashlib

def verify(raw_body: bytes, header: str, secret: str) -> bool:
    expected = "sha256=" + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(header or "", expected)

PHP

function verify(string $raw, ?string $header, string $secret): bool {
    $expected = "sha256=" . hash_hmac("sha256", $raw, $secret);
    return hash_equals($expected, $header ?? "");
}

Reject replays and repeats

The signature covers the body only, and sent_at is inside the body. After verifying, reject events more than five minutes old and de-duplicate on correlation_id.

const event = JSON.parse(rawBody);
const age = Date.now() - Date.parse(event.sent_at);
if (!(age > -60_000 && age < 5 * 60_000)) return res.status(400).end();   // too old, or from the future
if (await alreadyProcessed(event.correlation_id)) return res.status(200).end(); // a repeat
await markProcessed(event.correlation_id);                                      // a database or Redis in production

Delivery behaviour to design for

  • Answer with any 2xx quickly and do slow work afterwards.
  • Up to three attempts (immediately, then after 1 and 4 seconds), on network errors and 5xx only. Each attempt times out after 8 seconds.
  • An event that fails every attempt is not delivered again. Use the delivery log (GET /api/webhook/outbound/deliveries) to catch up, and POST /api/webhook/outbound/test to send a signed test event.
  • Events can arrive more than once and out of order.

Where to go next

Frequently asked questions

How do I integrate the WhatsApp Business API in PHP, Python or Node.js?

Create an API key, send it as a bearer token, and call POST /api/send-message with a template or text. The developer guide gives working examples in curl, Node.js, Python and PHP, taken from the API reference.

Which plans include the InboxWA API?

Growth and Scale. Starter has no API keys and no webhooks. Growth has the core API and five webhook events, and Scale has every call and all events.

How do I verify an InboxWA webhook?

Compute an HMAC-SHA256 of the raw request body using your signing secret, prefix it with sha256= and compare it to the X-Webhook-Signature header in constant time, before parsing the JSON.

What is the API rate limit?

The default is 600 requests per minute per key, adjustable per key. Sending messages is limited by your credits and WhatsApp's own limits rather than by this request limit.

Try it on your own account

Plans from ₹999/month, zero markup on Meta's message fees. 7-day free trial, no card required.

Start free trial