Start typing to search the documentation.

to navigate·to open

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_KEY bearer token against https://app.wrnexus.com, not a session cookie. Sending, template mutations, media uploads and consent writes need the whatsapp:send scope; status and the read-only ledgers need whatsapp:read. A key with no explicit scopes is unrestricted. Only the Meta-facing webhook is unauthenticated by API key (it uses the X-Hub-Signature-256 HMAC — 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:

  1. GET verification — echo hub.challenge only when hub.mode is subscribe and hub.verify_token matches your configured VERIFY_TOKEN.
  2. POST receiver — verify the X-Hub-Signature-256 HMAC-SHA256 of the raw request body keyed by your Meta APP_SECRET (constant-time compare), then parse the envelope into typed messages and statuses.

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 constructEvent the 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.sms namespace.
  • WhatsApp Webhooks — receive inbound messages and delivery statuses from Meta.
  • Authentication — create and scope an API key.