API
WhatsApp API
Send WhatsApp template, text, media and interactive messages over HTTP with an API key — manage templates and consent, look up delivery status, read the message log, and check your balance.
The public WhatsApp API sends WhatsApp Business messages from any external client over plain HTTP, authenticated with a WRNexus API key. It is a thin, billed wrapper over the Meta WhatsApp Cloud API (Graph v21.0).
In v1 every organization sends through the single shared WRNexus WABA (WRNexus owns the number), exactly like the shared SMS gateway — sends consume prepaid, workspace-scoped WhatsApp credits. The data model is Embedded-Signup-ready, so per-customer WABAs can be added later without an API change.
Feature flag. Outbound sending is gated behind
WHATSAPP_ENABLED. Until the WABA is live in production and the flag is flipped on, every send returns503 WHATSAPP_DISABLED. The inbound webhook is always live so Meta can reach it during App Review.
Base URL & authentication
https://app.wrnexus.com
WhatsApp endpoints live under /whatsapp/* (and are aliased under
/api/whatsapp/* for REST-convention callers — both resolve to the same
handlers).
Authenticate every request with your API key in either header:
Authorization: Bearer wrn_live_<your_api_key>
X-API-Key: wrn_live_<your_api_key>
Scopes: whatsapp:send is required to send messages, upload media, manage
templates and write consent; whatsapp:read is required for status, the message
log and the discovery endpoints. A key with no explicit scopes is unrestricted.
The Meta webhook endpoints use no API key.
The WRNexus WhatsApp model. A WhatsApp message is always sent to a recipient (
to) and takes one of several shapes selected bytype. To start a conversation outside the recipient’s open 24-hour service window you must send a pre-approved template; free-formtext,mediaandinteractivemessages are only permitted inside an open window. A template’s positional{{1}},{{2}}… placeholders are filled at send time from the Graphcomponentsarray you pass — the same merge-variable model as an SMS template’s{{token}}placeholders.
Billing model
Billing follows Meta’s 2025 per-template-message model (not the older
per-24h-conversation model). Every billable send is metered by recipient
country × category, the price is computed as base_cost + markup, and the
amount is deducted from the workspace’s prepaid WhatsApp credit balance
before the message is sent — so a send can never front unrecoverable cost.
| Category | When it is free |
|---|---|
marketing |
Never free — always billed. |
authentication |
Never free — always billed. |
utility |
Free when sent inside an open 24-hour service window. |
service |
Free inside the window and for the first 1,000 service messages per calendar month. |
The 24-hour service window opens when the recipient sends you an inbound
message; it stays open for 24 hours. Free-form text, interactive and media
messages are service-category and are only permitted inside an open window.
Send a message
POST /whatsapp/send — REST JSON
Send a message (scope whatsapp:send). The type field selects the message
shape; the billing category is derived from the type (and, for templates, from
the approved template’s category).
| Field | Type | Notes |
|---|---|---|
to |
string | Required. Recipient in E.164 format, e.g. +919812345678. Aliases (GET/form): recipient, number, msisdn, mobile, phone. |
type |
string | text | template | image | document | audio | video | sticker | interactive. Defaults to text (or template when a template name is present in a GET/form call). |
text |
string | Message body for type: text. Aliases (GET/form): message, body, msg, content. |
template |
object | { name, language, components } for type: template. language defaults to en_US. components is the Graph components array used to fill the template’s {{n}} placeholders. |
media |
object | { id } (a media id from POST /whatsapp/media) or { link }, plus an optional caption, for the media types. JSON only. |
interactive |
object | A Graph interactive object for type: interactive. JSON only. |
category |
string | Optional billing-category override, used when a template is not yet mirrored locally. |
Template send (opens a conversation outside the 24-hour window):
curl https://app.wrnexus.com/whatsapp/send \
-H "Authorization: Bearer wrn_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"to": "+919812345678",
"type": "template",
"template": {
"name": "order_update",
"language": "en_US",
"components": [
{ "type": "body", "parameters": [{ "type": "text", "text": "ORD-4821" }] }
]
}
}'
Free-form text reply (inside an open 24-hour window):
curl https://app.wrnexus.com/whatsapp/send \
-H "Authorization: Bearer wrn_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"to": "+919812345678",
"type": "text",
"text": "Thanks for getting in touch — how can we help?"
}'
Response 200 OK:
{
"ok": true,
"id": "b3f1c0e2-2c4b-4f0a-9b7e-1f2a3b4c5d6e",
"waMessageId": "wamid.HBgM...",
"to": "+919812345678",
"type": "template",
"category": "utility",
"free": false,
"price": 89,
"currency": "INR",
"charged": true
}
id is the WRNexus message id (use it with GET /whatsapp/status);
waMessageId is Meta’s wamid. price is in the credit currency’s minor units
(INR paise), and free is true when a free allowance applied and nothing was
charged.
GET /whatsapp/send — query parameters
The text and template send styles can also be driven entirely from the query
string — the simplest call style. Nested components, media and interactive
payloads are JSON-only, so use POST for those.
curl "https://app.wrnexus.com/whatsapp/send?to=%2B919812345678&type=text&text=Hello" \
-H "Authorization: Bearer wrn_live_xxx"
POST /whatsapp/send — form-encoded
Content-Type: application/x-www-form-urlencoded, using the same field names and
aliases as the GET call:
curl https://app.wrnexus.com/whatsapp/send \
-H "Authorization: Bearer wrn_live_xxx" \
-d "to=+919812345678" -d "template=order_update" -d "language=en_US"
POST /whatsapp/mark-read
Mark an inbound message as read — shows the blue ticks to the customer (scope
whatsapp:send).
curl https://app.wrnexus.com/whatsapp/mark-read \
-H "Authorization: Bearer wrn_live_xxx" \
-H "Content-Type: application/json" \
-d '{ "messageId": "wamid.HBgM..." }'
{ "ok": true }
Media
POST /whatsapp/media
Upload a media file (≤ 90 MB) as multipart/form-data under the file field and
receive a Graph media id to reuse in a media send (scope whatsapp:send).
curl https://app.wrnexus.com/whatsapp/media \
-H "Authorization: Bearer wrn_live_xxx" \
-F "file=@invoice.pdf"
{ "id": "1234567890123456" }
GET /whatsapp/media/{id}
Resolve a media id to its temporary download URL, MIME type and size (scope
whatsapp:read). The response is the Graph media object.
curl https://app.wrnexus.com/whatsapp/media/1234567890123456 \
-H "Authorization: Bearer wrn_live_xxx"
{
"url": "https://lookaside.fbsbx.com/whatsapp_business/attachments/...",
"mime_type": "application/pdf",
"sha256": "9f86d0818...",
"file_size": 81234,
"id": "1234567890123456",
"messaging_product": "whatsapp"
}
Templates
WhatsApp templates use positional {{1}}, {{2}} … placeholders that you fill
at send time via template.components. When you create a template, WRNexus
validates that the placeholders are numbered sequentially from {{1}} and
auto-synthesizes the sample example values Meta requires for each placeholder.
The authentication category is normalized to Meta’s canonical one-time-passcode
template schema.
GET /whatsapp/templates
List the shared WABA’s template catalogue (scope whatsapp:read). Filter with
?approved=true (only APPROVED templates) or ?status=PENDING (any Meta
status).
curl "https://app.wrnexus.com/whatsapp/templates?approved=true" \
-H "Authorization: Bearer wrn_live_xxx"
{
"templates": [
{
"id": "f1e2d3c4-...",
"wabaId": "123456789012345",
"name": "order_update",
"language": "en_US",
"category": "utility",
"status": "APPROVED",
"components": [
{ "type": "BODY", "text": "Your order {{1}} has shipped." }
]
}
]
}
Each entry includes the name, language, category, Meta approval status
(PENDING / APPROVED / REJECTED) and the template components.
POST /whatsapp/templates
Submit a new template to Meta for approval (scope whatsapp:send). Requires the
WABA to be configured (WABA_ID).
curl https://app.wrnexus.com/whatsapp/templates \
-H "Authorization: Bearer wrn_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"name": "order_update",
"language": "en_US",
"category": "utility",
"components": [{ "type": "BODY", "text": "Your order {{1}} has shipped." }]
}'
| Field | Type | Notes |
|---|---|---|
name |
string | Required. Lowercase [a-z0-9_], ≤ 512 chars. |
language |
string | Optional. BCP-47 / Meta locale; defaults to en_US. |
category |
string | Required. One of marketing | utility | authentication. |
components |
array | Required. Graph component objects (BODY, HEADER, …). Placeholders must be numbered sequentially from {{1}}. |
{
"ok": true,
"id": "f1e2d3c4-...",
"wabaId": "123456789012345",
"name": "order_update",
"language": "en_US",
"category": "utility",
"status": "PENDING",
"components": [{ "type": "BODY", "text": "Your order {{1}} has shipped." }],
"metaTemplateId": "987654321098765"
}
The template is mirrored locally with the approval status returned by Meta.
POST /whatsapp/templates/sync
Pull Meta’s current template catalogue into the local mirror (scope
whatsapp:send). Useful after a template’s approval status changes on Meta’s
side.
curl -X POST https://app.wrnexus.com/whatsapp/templates/sync \
-H "Authorization: Bearer wrn_live_xxx"
{ "ok": true, "fetched": 12, "inserted": 2, "updated": 9, "skipped": 1 }
DELETE /whatsapp/templates/{name}
Delete a template from Meta and the local mirror by name (scope
whatsapp:send).
curl -X DELETE https://app.wrnexus.com/whatsapp/templates/order_update \
-H "Authorization: Bearer wrn_live_xxx"
{ "ok": true, "name": "order_update" }
Delivery status
GET /whatsapp/status?id=<messageId>
Look up the delivery status of a message you sent (scope whatsapp:read). The
id accepts either the WRNexus message id or Meta’s waMessageId; the alias
messageId is also accepted.
curl "https://app.wrnexus.com/whatsapp/status?id=b3f1c0e2-2c4b-4f0a-9b7e-1f2a3b4c5d6e" \
-H "Authorization: Bearer wrn_live_xxx"
{
"id": "b3f1c0e2-2c4b-4f0a-9b7e-1f2a3b4c5d6e",
"waMessageId": "wamid.HBgM...",
"direction": "outbound",
"to": "+919812345678",
"from": "+14155550000",
"type": "template",
"templateName": "order_update",
"category": "utility",
"status": "delivered",
"error": null,
"price": 89,
"currency": "INR",
"creditsCharged": true,
"createdAt": "2026-06-18T14:30:00.000000",
"sentAt": "2026-06-18T14:30:01.000000",
"deliveredAt": "2026-06-18T14:30:03.000000",
"readAt": null,
"failedAt": null
}
An unknown id returns 404 NOT_FOUND.
Message log, account & balance
GET /whatsapp/messages
The 200 most recent WhatsApp messages (inbound + outbound) for the organization,
with direction, type, category, delivery status, the metered price and
timestamps (scope whatsapp:read).
{
"messages": [
{
"id": "b3f1c0e2-...",
"waMessageId": "wamid.HBgM...",
"direction": "outbound",
"to": "+919812345678",
"from": "+14155550000",
"type": "template",
"templateName": "order_update",
"category": "utility",
"status": "delivered",
"price": 89,
"currency": "INR",
"createdAt": "2026-06-18T14:30:00.000000"
}
]
}
GET /whatsapp/account
WhatsApp service/account status for the key’s active workspace (scope
whatsapp:read).
{
"enabled": true,
"configured": true,
"ownerType": "platform",
"creditType": "whatsapp",
"workspaceId": "ws_01HVZ...",
"balance": 124500
}
GET /whatsapp/balance
Remaining WhatsApp credits for the key’s active workspace (scope
whatsapp:read).
{
"workspaceId": "ws_01HVZ...",
"creditType": "whatsapp",
"balance": 124500
}
Consent (opt-in / STOP)
WhatsApp requires demonstrable opt-in before you message a customer, and you must
honour opt-outs. A recipient who replies STOP is recorded as opted-out and
further sends to them are blocked with 409 RECIPIENT_OPTED_OUT. A successful
send also records a best-effort opt-in for the recipient.
GET /whatsapp/consents
The 500 most recent opt-in / opt-out consent records for the organization (scope
whatsapp:read).
{
"consents": [
{
"id": "c0n5ent1-...",
"number": "+919812345678",
"status": "opted_in",
"source": "web_form",
"note": null,
"updatedAt": "2026-06-18T14:29:00.000000"
}
]
}
POST /whatsapp/consents
Record or override consent for a number (scope whatsapp:send). Keep an
auditable source for every opt-in.
curl https://app.wrnexus.com/whatsapp/consents \
-H "Authorization: Bearer wrn_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"number": "+919812345678",
"status": "opted_in",
"source": "web_form"
}'
| Field | Type | Notes |
|---|---|---|
number |
string | Required. Recipient phone number. |
status |
string | One of opted_in | opted_out | pending. Defaults to opted_in. |
source |
string | Free-text provenance for the audit trail. Defaults to public_api. |
{
"id": "c0n5ent1-...",
"number": "+919812345678",
"status": "opted_in",
"source": "web_form",
"note": null,
"updatedAt": "2026-06-18T14:30:00.000000"
}
Rate limits
WRNexus does not impose its own request throttling on the WhatsApp API — sending
is governed by your prepaid credit balance and by consent. Meta’s upstream Graph
API rate limits are surfaced verbatim: a Graph 429 is returned to you as
429 WHATSAPP_RATE_LIMITED. Back off and retry when you receive it.
Error responses
Errors use a flat, stable contract:
{
"code": "INSUFFICIENT_CREDITS",
"message": "This WhatsApp marketing message costs 89 credit(s) but the workspace has 0. Top up WhatsApp credits to send."
}
| Code | HTTP | Description |
|---|---|---|
UNAUTHENTICATED |
401 | Missing, invalid, expired, or revoked API key. |
FORBIDDEN |
403 | Key lacks the required scope or is not bound to an org. |
IP_NOT_ALLOWED |
403 | Caller IP is not on the organization’s allowlist. |
INVALID_RECIPIENT |
400 | to is missing or empty. |
INVALID_REQUEST |
400 | Body is not valid JSON/form, or a required query param (e.g. id) is missing. |
MISSING_TEMPLATE |
400 | type: template without a template object. |
MISSING_TEXT |
400 | type: text without text. |
MISSING_INTERACTIVE |
400 | type: interactive without an interactive object. |
MISSING_MEDIA |
400 | A media type without a media object. |
UNSUPPORTED_TYPE |
400 | Unknown message type. |
INVALID_CATEGORY |
400 | Template category is not marketing / utility / authentication. |
MISSING_FILE |
400 | Media upload has no file part. |
MEDIA_TOO_LARGE |
400 | Uploaded media exceeds the 90 MB WhatsApp limit. |
INVALID_NUMBER |
400 | Consent number is missing or invalid. |
INVALID_STATUS |
400 | Consent status is not a known value. |
WHATSAPP_REQUEST_INVALID |
400 | Meta rejected the request (e.g. unapproved template). |
INSUFFICIENT_CREDITS |
402 | Prepaid WhatsApp balance is below the message price. |
NOT_FOUND |
404 | Message (status lookup) or media not found. |
RECIPIENT_OPTED_OUT |
409 | The recipient has opted out — sending is blocked. |
NO_WORKSPACE |
409 | The organization has no active workspace to send from. |
WHATSAPP_RATE_LIMITED |
429 | Meta’s Graph API rate limit was hit — back off and retry. |
WHATSAPP_UPSTREAM |
502 | The Meta Graph API returned an error or was unreachable. |
WHATSAPP_DISABLED |
503 | Sending is off (WHATSAPP_ENABLED not set). |
WHATSAPP_NOT_CONFIGURED |
503 | WHATSAPP_TOKEN / PHONE_NUMBER_ID / WABA_ID missing. |
INTERNAL_ERROR |
500 | An unexpected server error. |
See also
- WhatsApp Webhooks — receive inbound messages and delivery statuses from Meta.
- WhatsApp SDK — typed clients that wrap these endpoints.
- Authentication — create and secure an API key.
- SMS API and Email API — the other API-key-authenticated messaging APIs.