WhatsApp Business API
Send WhatsApp templates, OTPs, chat replies, buttons, lists and bulk campaigns from your own software, and receive delivery reports and customer replies in real time. Built on the official WhatsApp Cloud API (Meta) - SMSGatewayHub is a Meta Tech Provider.
Clean JSON: to, template, variables. We build the WhatsApp payload, check opt-outs, billing and the 24-hour rule for you.
Already have code for Meta's Cloud API (or n8n / Make / Pabbly modules)? Send the same JSON to us - get Meta's own reply with the real wamid.
Signed (HMAC-SHA256) events for every status, every customer message, form submissions, payments and AI hand-overs - retried automatically.
Campaigns can fall back to RCS or SMS and track link clicks per recipient - set up in the panel, reported through the same API.
| Base URL | https://www.smsgatewayhub.com/cpaas/whatsapp/api.ashx |
|---|---|
| Format | JSON in, JSON out (UTF-8). Every action is selected with ?action=. |
| Success shape | {"ok": true, ...} · errors: {"ok": false, "error": "code", "message": "..."} with a matching HTTP status |
| Time zone | All times are India Standard Time (IST, UTC+05:30), format yyyy-MM-ddTHH:mm:ss |
Quick start
- Connect WhatsApp. In the panel go to CPaaS › WhatsApp and connect your number with Facebook (about 5 minutes).
- Get an approved template. Templates › New template. Meta usually approves within minutes. Business-initiated messages must use a template.
- Create an API key. WhatsApp › API Documentation › Create key. It starts with
sghwa_and is shown once. - Send your first message with the request below, then check its status or set a webhook.
Authentication
Send your API key in the Authorization header as a Bearer token (or in X-API-Key). Keys belong to your panel account; you can have up to 5 active keys and revoke any of them in the panel.
Authorization: Bearer sghwa_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx Content-Type: application/json
403 unauthorized.Numbers, limits & idempotency
Phone numbers
Use the full international number, digits only: 919876543210. Spaces, + and dashes are removed; a 10-digit Indian mobile (starting 6-9) gets 91 added automatically.
Rate limit
Up to 50 requests per second per API key. Above that you get 429 rate_limited with Retry-After: 1. For large sends use bulk (1,000 recipients per request). WhatsApp's own throughput and daily limits (by number tier) also apply.
Your reference (ref)
Add "ref": "order-1043" (max 100 characters) to any send. It is returned in every status, in webhooks, and you can look a message up by it.
Idempotency (safe retries)
If a network error leaves you unsure whether a request worked, retry it with the same Idempotency-Key header. Within 24 hours the same key returns the original result ("duplicate": true) instead of sending again.
Idempotency-Key: order-1043-shipped
The 24-hour rule & pricing
WhatsApp has two kinds of outgoing messages:
| Kind | When | How to send | Meta category |
|---|---|---|---|
| Template | Any time - starting a conversation, alerts, offers, OTPs | send (type template), bulk | Marketing, Utility or Authentication |
| Session (free-form) | Only within 24 hours after the customer's last message to you | text / media, buttons, lists, links | Service |
Check whether the window is open with GET contact (windowOpen, windowClosesAt). Sending a session message after the window closed returns 409 window_closed.
Each accepted message is charged from your WhatsApp wallet at your price for its category (or covered by your monthly plan). If WhatsApp does not deliver a message, or Meta does not bill it, the amount is returned automatically. See balance.
Send a template
| Field | Description |
|---|---|
torequiredstring | Customer number with country code. |
templaterequiredstring | Approved template name (see templates). |
languagedefault enstring | Template language code, e.g. en, hi, en_US. |
variablesstring[] | Values for body {{1}}, {{2}}... in order. New lines and tabs are flattened (WhatsApp rule). |
headerobject | {"text": "..."} for a text header with {{1}}, or {"media": "https://...", "filename": "bill.pdf"} for image / video / document headers. |
buttonsstring[] | Values for dynamic link buttons (the part after the fixed URL), in order. |
refstring | Your reference, max 100. |
The response is immediate: the message is queued and normally reaches WhatsApp within 1-2 seconds. Opted-out numbers are refused (409 opted_out).
{"ok": true, "messageId": 1045, "status": "queued", "ref": "order-1043"}Send an OTP
Create an Authentication template in the panel (Templates › New template › Authentication). Meta fixes the wording ("123456 is your verification code") and adds a Copy code button. Send the code as the only variable:
Coupon, limited-time offer & carousel templates
Create these in the panel (Templates › Rich templates). Each has default values saved with it - the coupon code, how long the offer runs, and every card's image / video and link value - so a normal send or bulk request needs only the usual variables. To change them for one message, add:
| Field | For | Description |
|---|---|---|
couponstring | Coupon / offer | The code the customer copies (1-15 letters / numbers). |
offerExpiresAtstring | Limited-time offer | When the countdown ends (IST), e.g. 2026-10-31T23:59:00. Default: N hours after sending, as set in the panel. |
cardsobject[] | Carousel | One object per card, in order: media (https image / video), variables (card text values), button (value for the card link's {{1}}). Missing values use the defaults. |
button.Send text & media (session messages)
Within the 24-hour window you can reply freely. Set type and the matching object:
| type | Body | Limits |
|---|---|---|
text | "text": "Hello" (or {"body": "Hello"}) | 4096 characters, links get a preview |
image | "media": {"link": "https://...jpg", "caption": "..."} | JPG / PNG, 5 MB |
video | "media": {"link": "https://...mp4", "caption": "..."} | MP4 / 3GP, 16 MB |
audio | "media": {"link": "https://...mp3"} | 16 MB, no caption |
document | "media": {"link": "https://...pdf", "filename": "invoice.pdf", "caption": "..."} | 100 MB |
Media links must be public https URLs - or upload the file first with POST media.
Buttons, lists and link buttons
Set "type": "interactive" and an interactive object. When the customer taps a button or list row, you receive a message.inbound webhook with replyId = the id you set.
| interactive.type | What the customer sees | Limits |
|---|---|---|
buttons | Up to 3 reply buttons under the message | title 20 chars, id 200 chars |
list | A menu button opening up to 10 rows in sections | button 20, row title 24, description 72 |
cta_url | One button that opens a website | button text 20 |
Optional for all: header (text, 60), footer (60). body is required (1024). Ids must be unique and must not look like p12 or b1.x.0 (reserved for our chatbot and catalog).
Bulk send / campaigns
Send one template to up to 1,000 recipients per request, each with their own variables. The batch becomes a campaign you can watch live in the panel (delivered, read, clicked, replied, failure reasons, retarget). Invalid, duplicate and opted-out numbers are reported in rejected and the rest are sent.
| Field | Description |
|---|---|
template, languagerequired | As in send. |
recipientsrequiredobject[] | Each: to, optional variables, header, buttons, ref. |
namestring | Campaign name shown in the panel. |
scheduleAtstring | IST date-time within 30 days, e.g. 2026-10-07T10:30:00. Omit to send now. |
The wallet must cover all accepted messages up front (402 insufficient_balance otherwise). Larger lists: send several requests, or upload Excel / CSV in the panel (no limit).
{
"ok": true, "campaignId": 88, "status": "queued", "scheduledAt": null,
"accepted": 2, "rejectedCount": 1,
"messages": [
{"index": 0, "to": "919876543210", "messageId": 2101, "ref": "inv-1"},
{"index": 1, "to": "919812345678", "messageId": 2102, "ref": "inv-2"}
],
"rejected": [{"index": 2, "to": "12345", "error": "bad_number"}]
}Meta-compatible API
Send exactly the JSON of Meta's Cloud API POST /{phone-number-id}/messages - any message type Meta supports (template with any components, text, image, video, audio, document, sticker, location, contacts, interactive including flows and catalogs). We check billing, opt-outs (templates) and the 24-hour window (session messages), send it to WhatsApp straight away and return Meta's own response with the real wamid - plus sgh_message_id for our status API.
- Your reference: Meta's
biz_opaque_callback_datais stored asrefand comes back in our webhooks. - Errors use Meta's format (
{"error": {"message", "code", ...}}) so existing error handling keeps working. - Several numbers? Pass
?phone_number_id=(from the panel); default is your default number.
{
"messaging_product": "whatsapp",
"contacts": [{"input": "919876543210", "wa_id": "919876543210"}],
"messages": [{"id": "wamid.HBgMOTE5ODc2NTQzMjEwFQIAERgS..."}],
"sgh_message_id": 1046
}sghwa_ key as Bearer token, and paste the Meta JSON body.Message status
Look up by id (our messageId), by ref (your reference - latest match), or up to 100 at once with ids=1045,1046,1047 (returns messages[]). For real-time updates prefer webhooks.
{
"ok": true, "messageId": 1045, "toNumber": "919876543210", "messageType": "template",
"templateName": "order_update", "campaignId": null, "status": "read",
"errorCode": null, "errorText": null, "clientRef": "order-1043",
"createdAt": "2026-10-06T12:01:41", "sentAt": "2026-10-06T12:01:43",
"deliveredAt": "2026-10-06T12:01:44", "readAt": "2026-10-06T12:05:10",
"repliedAt": null, "clickedAt": null, "failedAt": null,
"pricingCategory": "utility", "fallbackStatus": null
}List messages
| Query | Description |
|---|---|
from, to | Dates (yyyy-MM-dd), max 31 days. Default: last 7 days. |
status | queued, sending, sent, delivered, read, failed, cancelled |
number, campaignId | Filter by recipient or campaign. |
page | 100 per page, newest first; hasMore tells you if there is a next page. |
Status lifecycle
| Status | Meaning |
|---|---|
queued | Accepted by us, waiting to go to WhatsApp (normally < 2 s; scheduled campaigns wait for their time). |
sending | Being handed to WhatsApp right now. |
sent | WhatsApp accepted it (one grey tick). sentAt is set. |
delivered | On the customer's phone (two grey ticks). |
read | Opened (two blue ticks) - only if the customer has read receipts on. |
failed | Not delivered - see errorCode / errorText and WhatsApp errors. Charges are refunded. |
cancelled | Campaign cancelled before this message was sent. |
Extra timestamps: repliedAt (customer replied), clickedAt (tracked link clicked). Statuses never go backwards.
Templates
Your approved templates with what each needs: variables (body values), headerType (NONE, TEXT, IMAGE, VIDEO, DOCUMENT), headerVariable, buttonVariables and Meta's raw components. Create and edit templates in the panel (Meta reviews them).
Upload media
Returns a public https link you can use in templates (header.media) and session messages (media.link). JPG / PNG up to 5 MB, MP4 / 3GP 16 MB, PDF / Office / TXT 100 MB. The file content is checked against its type.
{"ok": true, "link": "https://www.smsgatewayhub.com/cpaas/media/202610/7f3c...e1.pdf", "fileName": "invoice.pdf", "size": 48211}Contact & 24-hour window
Everything we know about a customer: name, tags, attributes (e.g. form answers), assigned agent and department, opt-out and block state - and windowOpen / windowClosesAt to decide between a session message and a template.
{
"ok": true, "number": "919876543210", "known": true, "name": "Ravi Kumar",
"email": null, "conversationStatus": "open", "assignedTo": "Priya",
"department": "Sales", "tags": ["lead", "interested"], "attributes": {"city": "Indore"},
"blocked": false, "optedOut": false,
"windowOpen": true, "windowClosesAt": "2026-10-07T09:42:23",
"lastInboundAt": "2026-10-06T09:42:23", "firstSeenAt": "2026-10-05T21:25:36"
}Opt-outs
Customers who reply STOP / UNSUBSCRIBE are added automatically. Templates to opted-out numbers are refused (409 opted_out) and skipped in bulk. Sync your own unsubscribe list with op: add. 500 per page.
Balance
{"ok": true, "billingActive": true, "mode": "per_message", "currency": "INR", "balance": 2450.75, "planName": null, "planValidTill": null}mode: per_message (charged per message from balance), plan (monthly plan, valid till planValidTill) or free.
Webhooks: setup & security
In the panel open WhatsApp › Automation › Webhooks, enter your https URL, choose the events and save. You get a signing secret once. We POST JSON to your URL; reply with any 2xx within 10 seconds. Failed deliveries are retried 6 times with growing gaps (2, 4, 8, 16, 32, 64 minutes). Use Send test to receive a test.ping.
| Header | Value |
|---|---|
X-SGH-Event | Event name, e.g. message.status |
X-SGH-Signature | sha256= + hex HMAC-SHA256 of the raw request body using your signing secret |
Always verify the signature before trusting the data, compare in constant time, and process events idempotently (the same event can arrive twice after a retry).
Events & payloads
| Event | When |
|---|---|
message.status | A message you sent changes status (sent, delivered, read, failed). |
message.inbound | A customer sends you a message, taps a button or picks a list row (replyId). |
flow.submitted | A customer submits a WhatsApp form (Flow) - all answers in fields. |
payment.paid | A payment request sent from WhatsApp is paid. |
ai.handoff | The AI assistant hands a chat to your team. |
test.ping | You pressed "Send test" in the panel. |
{
"event": "message.status", "timestamp": "2026-10-06T12:01:44",
"data": {
"messageId": 1045, "to": "919876543210", "ref": "order-1043",
"campaignId": null, "status": "delivered", "at": "2026-10-06T12:01:44",
"errorCode": null, "errorText": null,
"wamid": "wamid.HBgMOTE5ODc2NTQzMjEwFQIAERgS..."
}
}{
"event": "flow.submitted", "timestamp": "2026-10-06T12:20:05",
"data": {"contactId": 77, "from": "919876543210", "flowId": 4,
"fields": {"name": "Ravi", "email": "ravi@example.com", "interested_in": "WhatsApp API"}}
}{
"event": "message.inbound", "timestamp": "2026-10-06T12:10:02",
"data": {
"from": "919876543210", "name": "Ravi", "type": "interactive",
"text": "Looks good", "replyId": "yes", "mediaId": null,
"wamid": "wamid.HBgMOTE5...", "contactId": 77,
"receivedAt": "2026-10-06T12:10:01", "adReferral": null
}
}{"event": "payment.paid", "timestamp": "...",
"data": {"payId": 31, "number": "919876543210", "amount": 999.00, "note": "Order #1043"}}
{"event": "ai.handoff", "timestamp": "...",
"data": {"contactId": 77, "from": "919876543210", "question": "I want a refund"}}type in message.inbound: text, image, video, audio, document, sticker, location, contacts, button, interactive. Media can be viewed in the panel inbox.
Error codes
| HTTP | error | Meaning / what to do |
|---|---|---|
| 400 | bad_json | Body is not valid JSON or too large (64 KB; bulk 4 MB). |
| 400 | bad_number | Number is not 8-15 digits with country code. |
| 400 | variables / header / buttons | The template needs more (non-empty) values - the message says how many. |
| 400 | text / media / interactive / bad_type | Session message fields missing or over the limits. |
| 400 | recipients / schedule / no_valid_recipients | Bulk request problems. |
| 400 | bad_ref / bad_idempotency_key | ref over 100 characters / Idempotency-Key format. |
| 402 | insufficient_balance | Recharge your WhatsApp wallet. |
| 402 | plan_expired / no_rate | Renew your plan / your price is not set - contact your account manager. |
| 403 | unauthorized | Missing, wrong or revoked key, or WhatsApp not enabled for the account. |
| 404 | template_not_found / not_found | Template name + language not found (sync in the panel) / unknown message or action. |
| 409 | template_not_approved | Template is pending, rejected or paused. |
| 409 | window_closed | No customer message in the last 24 hours - send a template. |
| 409 | opted_out / blocked | Customer unsubscribed / you blocked them. |
| 409 | not_connected / no_number | Connect WhatsApp / register a number in the panel. |
| 413 | too_large | Media file over its size limit. |
| 429 | rate_limited | More than 50 requests per second - slow down (Retry-After: 1). |
| 500 | server_error | Temporary problem - retry with the same Idempotency-Key. |
WhatsApp (Meta) errors
When WhatsApp itself refuses or fails a message, its code appears in errorCode / errorText (status API and webhooks). The common ones:
| Code | Meaning | What to do |
|---|---|---|
131026 | Message undeliverable | The number is not on WhatsApp, or uses an old app version, or has not accepted the new terms. Use SMS fallback. |
131047 | Re-engagement message | More than 24 hours since the customer's last message - send a template. |
131049 | Not delivered to keep the ecosystem healthy | Meta limits how many marketing messages one person gets. Retry later, improve relevance. |
131050 | User stopped marketing messages | The customer turned off your marketing messages in WhatsApp. |
131051 | Unsupported message type | Check the message type / object. |
131056 | Pair rate limit | Too many messages to the same number in a short time - slow down. |
130429 | Throughput limit | We retry automatically. |
131042 | Payment issue | The WhatsApp Business Account has a billing problem in Meta (card / credit line). |
132000 / 132001 | Template variable count / template does not exist | Check templates and language. |
132015 / 132016 | Template paused / disabled | Quality was low - edit or create a new template. |
368 | Temporarily blocked for policy violations | Check Account Quality in WhatsApp Manager. |
Best practices & go-live checklist
- Use webhooks, not polling, for statuses and replies. Verify the signature; answer 2xx fast and process in the background.
- Store our
messageIdand yourreftogether; useIdempotency-Keyon every send you might retry. - Only message people who opted in, honour STOP immediately (we do it for you), and keep marketing relevant - quality rating decides your daily limit.
- Check
windowOpenbefore session messages; outside the window always use a template. - OTPs: Authentication template, code as the only variable, short expiry.
- Large sends: bulk in batches of 1,000 or a scheduled campaign; watch the campaign page for failure reasons and retarget non-readers.
- Media: host on fast https or use upload; keep images under 1 MB for quick delivery.
- Security: keys only on servers, one key per system, revoke unused keys.
Postman & OpenAPI
Changelog
| 2026-10-06 | v1.1: coupon code, limited-time offer and carousel templates (coupon, offerExpiresAt, cards). |
|---|---|
| 2026-10-06 | v1: session messages (text, media, buttons, lists, link buttons), bulk / scheduled campaigns, Meta-compatible endpoint, status for many ids, message list, contact & 24-hour window, opt-outs, balance, media upload, Idempotency-Key, webhook events flow.submitted / payment.paid / ai.handoff and replyId. |
| 2026-10-05 | Initial release: send template, status, templates; signed webhooks. |