Start typing to search the documentation.

to navigate·to open

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 returns 503 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 by type. To start a conversation outside the recipient’s open 24-hour service window you must send a pre-approved template; free-form text, media and interactive messages are only permitted inside an open window. A template’s positional {{1}}, {{2}} … placeholders are filled at send time from the Graph components array 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
}

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