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_apiscope (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 exampleapi_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-RemainingandX-RateLimit-Reset, and a 429 carriesRetry-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, andPOST /api/webhook/outbound/testto send a signed test event. - Events can arrive more than once and out of order.
Where to go next
- The full API reference, the OpenAPI document and the whole reference as Markdown.
- Webhooks in both directions, including starting flows from your own systems.
- All integrations and the cost guide.
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.