SDK
WhatsApp SDK
Official WRNexus WhatsApp SDK — install and configure the API-key client, send text / template / media / interactive messages, check delivery status, manage templates, media and consent, read the message log and balance, and verify inbound Meta webhooks, with typed responses and a structured error hierarchy.
The WhatsApp product is exposed as the client.whatsapp namespace on the
same WrNexusClient used for SMS — a typed wrapper over the
public WhatsApp API. Like every product namespace it is created
lazily over the client’s single shared transport (base URL + auth header +
timeout + JSON codec), so it shares the SMS client’s configuration and the same
structured error hierarchy. Send template, text, media and
interactive messages, check delivery status, manage templates, media and opt-in
consent, read the message log and balance, and verify inbound Meta webhooks.
Same API key as SMS and Email. The public WhatsApp API is the API-key sibling of the SMS API: it authenticates with your
WRNEXUS_API_KEYbearer token againsthttps://app.wrnexus.com, not a session cookie. Sending, template mutations, media uploads and consent writes need thewhatsapp:sendscope; status and the read-only ledgers needwhatsapp:read. A key with no explicit scopes is unrestricted. Only the Meta-facing webhook is unauthenticated by API key (it uses theX-Hub-Signature-256HMAC — see §7).
1. Environment & prerequisites
Install the SDK for your language (the WhatsApp namespace ships in the same package as SMS/Email):
npm install @wrnexus/sdk # JavaScript / TypeScript (Node 18+)
pip install wrnexus # Python 3.8+
Configure the client from the environment — the same three variables the SMS SDK reads, so one client serves every product:
| Setting | Env var | Default |
|---|---|---|
| API key | WRNEXUS_API_KEY |
(required) |
| Base URL | WRNEXUS_API_BASE_URL |
https://app.wrnexus.com |
| Timeout | WRNEXUS_TIMEOUT_MS |
30000 |
export WRNEXUS_API_KEY="wrn_live_xxx"
Create your key under Account → API Configuration (see
Authentication) and grant it the
whatsapp:send / whatsapp:read scopes. Outbound sending is gated behind the
WHATSAPP_ENABLED feature flag — until the shared WABA is live, sends return
503 WHATSAPP_DISABLED, while read-only calls keep working.
2. Initialization
Construct the client once and reuse it; explicit options win over the
environment. The WhatsApp namespace is client.whatsapp, exactly as SMS is
client.sms.
import { WrNexusClient } from '@wrnexus/sdk';
// Reads WRNEXUS_API_KEY (and optional base URL / timeout) from the environment.
const client = new WrNexusClient();
// Or configure explicitly:
const explicit = new WrNexusClient({
apiKey: 'wrn_live_xxx',
baseUrl: 'https://app.wrnexus.com',
timeoutMs: 15_000,
});
client.whatsapp; // the WhatsApp namespace over the shared transport
from wrnexus import WrNexusClient
client = WrNexusClient() # reads WRNEXUS_API_KEY
explicit = WrNexusClient(
api_key="wrn_live_xxx",
base_url="https://app.wrnexus.com",
timeout=15.0, # seconds
)
A missing API key fails fast at construction (WrNexusConfigError in Python,
WrNexusError in JS/TS) before any request is made.
3. Methods
client.whatsapp exposes the same surface in every language. Each method maps to
one WhatsApp API endpoint:
| Method | Scope | Description |
|---|---|---|
send(...) |
whatsapp:send |
Send a message. The type field selects the shape (text/template/media/interactive) and the billing category. Returns id, waMessageId, category, price, charged. |
status(id) |
whatsapp:read |
Look up a sent message’s delivery status by WRNexus id or Meta waMessageId. |
markRead(messageId) |
whatsapp:send |
Mark an inbound message as read (shows blue ticks to the customer). |
uploadMedia(file) |
whatsapp:send |
Upload a media file (≤ 90 MB, multipart/form-data) and get a Graph media id. |
getMedia(id) |
whatsapp:read |
Resolve a media id to its temporary download URL, MIME type and size. |
templates(options?) |
whatsapp:read |
List the WABA template catalogue (name, language, category, approval status, components). Filter with { approved: true } or { status: 'PENDING' }. |
submitTemplate(...) |
whatsapp:send |
Submit a new template to Meta for approval; mirrored locally with the returned status. |
syncTemplates() |
whatsapp:send |
Pull Meta’s current catalogue into the local mirror. Returns { fetched, inserted, updated, skipped }. |
deleteTemplate(name) |
whatsapp:send |
Delete a template from Meta and the local mirror by name. |
consents() |
whatsapp:read |
List the 500 most recent opt-in / opt-out consent records for the workspace. |
recordConsent(...) |
whatsapp:send |
Record or update consent for a number (keep an auditable source). |
messages() |
whatsapp:read |
The 200 most recent messages (inbound + outbound) with direction, type, category, status, price. |
account() |
whatsapp:read |
WhatsApp account status for the active workspace (enabled, configured, balance). |
balance() |
whatsapp:read |
Remaining WhatsApp credits for the workspace. |
The package also exports a WhatsAppWebhook helper for
verifying and parsing inbound Meta webhook deliveries.
4. Parameters
send(...) parameters (names shown in JS/TS form; Python uses snake_case):
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
to |
string |
Yes | — | Recipient in E.164 format, e.g. +919812345678. |
type |
string |
No | 'text' |
text | template | image | document | audio | video | sticker | interactive. |
text |
string |
Cond.* | — | Body for type: text. |
template |
object |
Cond.* | — | { name, language, components } for type: template. language defaults to en_US. |
media |
object |
Cond.* | — | { id } or { link, caption? } for image/document/audio/video/sticker. |
interactive |
object |
Cond.* | — | Graph interactive object for type: interactive. |
category |
string |
No | derived | Billing-category override (marketing | utility | authentication | service), used when a template isn’t mirrored locally yet. |
* Provide the field that matches type: text for text, template for
template, media for media types, interactive for interactive.
submitTemplate(...) parameters:
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
name |
string |
Yes | — | Lowercase [a-z0-9_], ≤ 512 chars. |
language |
string |
No | en_US |
BCP-47 / Meta locale tag, e.g. en_US. |
category |
string |
Yes | — | marketing | utility | authentication. |
components |
object[] |
Yes | — | Meta component objects, e.g. [{ type: 'BODY', text: '…{{1}}…' }]. Placeholders must be numbered sequentially from {{1}}. |
recordConsent(...) parameters:
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
number |
string |
Yes | — | Recipient number in E.164 format. |
status |
string |
No | opted_in |
opted_in | opted_out | pending. |
source |
string |
No | public_api |
Auditable opt-in source, e.g. web_form. |
note |
string |
No | null |
Free-text note recorded with the consent change. |
Other methods: status(id) takes the WRNexus id or the wamid… string;
markRead(messageId) takes the inbound wamid… string; getMedia(id) and
deleteTemplate(name) take a single id/name; templates(options?) takes an
optional { approved?, status? } filter; uploadMedia(file) takes a file/blob;
and syncTemplates(), consents(), messages(), account() and balance()
take no parameters.
5. Function-calling examples
JavaScript / TypeScript
import { WrNexusClient } from '@wrnexus/sdk';
const client = new WrNexusClient();
// Template send — opens a conversation outside the 24h service window:
const sent = await client.whatsapp.send({
to: '+919812345678',
type: 'template',
template: {
name: 'order_update',
language: 'en_US',
components: [
{ type: 'body', parameters: [{ type: 'text', text: 'ORD-4821' }] },
],
},
});
console.log(sent.id, sent.waMessageId, sent.charged);
// Free-form text reply (only inside an open 24h window):
await client.whatsapp.send({
to: '+919812345678',
type: 'text',
text: 'Thanks for getting in touch — how can we help?',
});
// Read-only calls (whatsapp:read):
const status = await client.whatsapp.status(sent.id);
const templates = await client.whatsapp.templates({ approved: true });
const balance = await client.whatsapp.balance();
const account = await client.whatsapp.account();
console.log(status.status, balance.balance);
Sample send response:
{
"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
}
Sample status response:
{
"id": "b3f1c0e2-2c4b-4f0a-9b7e-1f2a3b4c5d6e",
"waMessageId": "wamid.HBgM...",
"direction": "outbound",
"to": "+919812345678",
"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
}
Python
The client is a context manager, so the underlying HTTP pool is closed on exit.
from wrnexus import WrNexusClient
with WrNexusClient() as client:
sent = client.whatsapp.send(
to="+919812345678",
type="text",
text="Thanks for getting in touch — how can we help?",
)
print(sent.id, sent.wa_message_id)
# Check delivery status and record consent:
status = client.whatsapp.status(sent.id)
print(status.status)
client.whatsapp.record_consent(
number="+919812345678",
status="opted_in",
source="web_form",
)
account = client.whatsapp.account()
print(account.balance)
Sample account response:
{
"enabled": true,
"configured": true,
"ownerType": "platform",
"creditType": "whatsapp",
"workspaceId": "ws_01HVZ...",
"balance": 124500
}
6. Error handling
The WhatsApp API returns the same flat error contract as SMS —
{ "code": "...", "message": "..." } paired with the HTTP status. The SDK parses
it onto the shared typed error hierarchy, so you branch on the error class,
statusCode and code:
import {
WrNexusAuthError, // 401 / 403 — bad key, missing scope, or IP not allowed
WrNexusServerError, // 5xx — retryable (e.g. 502 WHATSAPP_UPSTREAM)
WrNexusApiError, // other 4xx — .statusCode, .code, .message
WrNexusTransportError, // no HTTP response (DNS, timeout) — retryable
} from '@wrnexus/sdk';
try {
await client.whatsapp.send({ to: '+919812345678', type: 'text', text: 'Hi' });
} catch (err) {
if (err instanceof WrNexusServerError && err.isBadGateway) {
// 502 WHATSAPP_UPSTREAM — Meta's Graph API errored; retry with backoff.
} else if (err instanceof WrNexusApiError) {
console.error(err.statusCode, err.code, err.message);
}
}
from wrnexus import WrNexusApiError, WrNexusServerError
try:
client.whatsapp.send(to="+919812345678", type="text", text="Hi")
except WrNexusServerError as err:
pass # 5xx — retry with backoff
except WrNexusApiError as err:
print(err.status_code, err.code, err.message)
| Code | HTTP | Error class | Retry? |
|---|---|---|---|
UNAUTHENTICATED |
401 | WrNexusAuthError |
No — fix the API key. |
FORBIDDEN |
403 | WrNexusAuthError |
No — key missing a scope or not bound to an org. |
IP_NOT_ALLOWED |
403 | WrNexusAuthError |
No — caller IP off the org allowlist. |
INVALID_RECIPIENT |
400 | WrNexusApiError |
No — provide a valid to. |
MISSING_TEMPLATE |
400 | WrNexusApiError |
No — add a template for type:template. |
MISSING_TEXT |
400 | WrNexusApiError |
No — add text for type:text. |
MISSING_MEDIA |
400 | WrNexusApiError |
No — add media for a media type. |
UNSUPPORTED_TYPE |
400 | WrNexusApiError |
No — unknown message type. |
INSUFFICIENT_CREDITS |
402 | WrNexusApiError |
No — top up WhatsApp credits. |
NOT_FOUND |
404 | WrNexusApiError |
No — unknown message or media id. |
RECIPIENT_OPTED_OUT |
409 | WrNexusApiError |
No — recipient sent STOP; wait for opt-in. |
NO_WORKSPACE |
409 | WrNexusApiError |
No — no active workspace to send from. |
WHATSAPP_RATE_LIMITED |
429 | WrNexusApiError |
Yes — Meta rate-limited; back off. |
WHATSAPP_UPSTREAM |
502 | WrNexusServerError |
Yes — Meta Graph errored; back off. |
WHATSAPP_DISABLED |
503 | WrNexusServerError |
No — sending off until the flag is on. |
WHATSAPP_NOT_CONFIGURED |
503 | WrNexusServerError |
No — WABA credentials missing. |
| (none — no response) | — | WrNexusTransportError |
Yes — DNS/timeout; back off. |
Retry the 5xx server errors (WrNexusServerError, e.g. 502 WHATSAPP_UPSTREAM), the 429 WHATSAPP_RATE_LIMITED throttle, and transport
errors with exponential backoff. The 503 disabled/not-configured states and the
other 4xx codes are caller errors — fix the request or wait for the WABA to go
live rather than retrying. See the
WhatsApp API error table for the full list.
7. Webhook verification
Meta delivers inbound messages and delivery statuses to a single WRNexus
webhook. In v1 you usually read the resulting events
through client.whatsapp.messages() rather than hosting the receiver yourself —
but when you do terminate Meta’s callback (proxying, or once per-customer WABAs
land), the WhatsAppWebhook helper handles both halves of the handshake:
GETverification — echohub.challengeonly whenhub.modeissubscribeandhub.verify_tokenmatches your configuredVERIFY_TOKEN.POSTreceiver — verify theX-Hub-Signature-256HMAC-SHA256 of the raw request body keyed by your MetaAPP_SECRET(constant-time compare), then parse the envelope into typedmessagesandstatuses.
JavaScript / TypeScript
import { WhatsAppWebhook, WrNexusWebhookSignatureError } from '@wrnexus/sdk';
// GET /webhook — verification handshake. Returns the challenge to echo, or null.
const challenge = WhatsAppWebhook.verifyChallenge(req.query, process.env.VERIFY_TOKEN);
if (challenge === null) {
res.status(403).json({ code: 'FORBIDDEN', message: 'Webhook verification failed' });
} else {
res.status(200).type('text/plain').send(challenge);
}
// POST /webhook — verify the signature over the RAW body, then parse.
try {
const event = WhatsAppWebhook.constructEvent(
rawBody, // exact bytes Meta sent (Buffer or string)
req.headers['x-hub-signature-256'],
process.env.APP_SECRET,
);
for (const message of event.messages) {
console.log('inbound', message.from, message.type);
}
for (const status of event.statuses) {
console.log('status', status.id, status.status); // sent | delivered | read | failed
}
res.status(200).json({ ok: true });
} catch (err) {
if (err instanceof WrNexusWebhookSignatureError) {
res.status(400).json({ code: 'SIGNATURE_INVALID', message: err.message });
} else {
res.status(400).json({ code: 'INVALID_PAYLOAD', message: 'Webhook body was not valid JSON' });
}
}
Python
from wrnexus import WhatsAppWebhook, WrNexusWebhookSignatureError
# GET /webhook — verification handshake.
challenge = WhatsAppWebhook.verify_challenge(request.args, os.environ["VERIFY_TOKEN"])
if challenge is None:
return ("Webhook verification failed", 403)
return (challenge, 200, {"Content-Type": "text/plain"})
# POST /webhook — verify the signature over the RAW body, then parse.
try:
event = WhatsAppWebhook.construct_event(
request.get_data(), # exact bytes
request.headers.get("X-Hub-Signature-256"),
os.environ["APP_SECRET"],
)
for message in event.messages:
print("inbound", message.from_, message.type)
for status in event.statuses:
print("status", status.id, status.status)
return ({"ok": True}, 200)
except WrNexusWebhookSignatureError:
return ({"code": "SIGNATURE_INVALID", "message": "Webhook signature verification failed"}, 400)
Sign over the raw bytes. Pass
constructEventthe exact bytes Meta transmitted. If a proxy or framework re-serialises the JSON (reordering keys or changing whitespace) the recomputed HMAC won’t match and verification fails — the same rule the webhook receiver enforces.
See also
- WhatsApp API — every endpoint, field, and error code.
- SMS SDK — the same API-key client and the
client.smsnamespace. - WhatsApp Webhooks — receive inbound messages and delivery statuses from Meta.
- Authentication — create and scope an API key.