Unirsal
Back to Home

Unirsal Developer Platform

WhatsApp Official API

Complete reference for the WhatsApp Official (Meta Cloud API) integration: Embedded Signup, phone-number management, message templates, sending messages, analytics, manager on-behalf routes, and the Meta inbound webhook. The Unirsal backend acts as a secure proxy in front of the Meta WhatsApp Business Platform — this covers only the official integration (the unofficial WAHA and Wasender session APIs are documented separately).

38 endpoints7 sections{BASE_URL} = your Unirsal API originLast updated July 11, 2026

Embedded Signup

Connect a WhatsApp Business Account through Meta's Embedded Signup — the backend exchanges the one-time code, stores the account link, and subscribes the WABA to webhooks.

Server-held Meta tokens

The long-lived Meta access token and internal verification tokens live server-side only and are never returned to any client — every response is a strict allow-list.

Template lifecycle

Create custom or Template-Library templates, submit them to Meta review, poll approval status, browse the library, and delete — all proxied to the Graph API.

Messaging & analytics

Send text, media, and template messages, then read sent/delivered, conversation-cost, and per-template analytics straight from Meta.

Manager on-behalf

Managers operate WhatsApp Official features for accounts they created, with role and ownership checks enforced before every request.

Three endpoint surfaces

SurfaceBase pathConsumers
Self-service API/v3/whatsapp-officialAccount holders (JWT or account api_key)
Manager on-behalf/v3/managers/users/:userId/whatsapp-officialManagers acting for users they created (manager JWT or api_key + ownership check)
Webhook handler/v2/webhook/whatsapp/official/handlerCalled by Meta (WhatsApp Cloud API) — verification handshake + event ingress, fanned out to your configured forward URLs
On this page

Read this first

Conventions

These rules apply to every /v3/whatsapp-official endpoint unless an endpoint states otherwise. The webhook handler is the one exception — it replies with its own minimal JSON (and a plain-text challenge on the verification handshake) instead of the standard envelopes.

Base URL & mount

All endpoints are mounted under {BASE_URL}/v3/whatsapp-official, where {BASE_URL} is your API server origin (e.g. https://api.yourserver.com). The whole router sits behind authentication that accepts either a JWT or your account API key — the one exception is POST /v3/whatsapp-official/setup-link, which is deliberately unauthenticated.

Authentication ("JWT or api_key")

The server resolves your identity from the request in this order:

SourceBehavior
body.api_keyIf the JSON body contains an api_key field it is used as your account key — this wins even if an Authorization header is also present. On endpoints with a Joi body schema, an api_key body field is then rejected as unknown, so prefer the header.
Authorization: <key>An Authorization header whose value does not start with Bearer is treated as the raw account API key. No prefix, no quotes — just the key.
Authorization: BearerA header starting with Bearer is parsed as a JWT (obtained from the login endpoint).

If no Authorization header and no body.api_key is present, the request fails with 400 Authorization header not provided. An unknown API key fails with 403; an invalid/expired JWT fails with 401.

With an API key (raw value)
curl -X POST "{BASE_URL}/v3/whatsapp-official/message" \
  -H "Authorization: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "phoneNumberId": "106540352242922", "recipientPhone": "9627XXXXXXXX", "type": "text", "text": { "body": "Hello" } }'
With a JWT
curl -X POST "{BASE_URL}/v3/whatsapp-official/message" \
  -H "Authorization: Bearer YOUR_JWT" \
  -H "Content-Type: application/json" \
  -d '{ "phoneNumberId": "106540352242922", "recipientPhone": "9627XXXXXXXX", "type": "text", "text": { "body": "Hello" } }'
  • JWT auth — every successful request returns a refreshed JWT in the token response header; you may adopt it.
  • API-key auth — the key you sent is echoed back in the api-key response header.

Where the API key comes from

The API key belongs to your user account. It is created/rotated with POST /v2/users/generate-api-key (JWT auth only). Rotating generates a new key and immediately invalidates the previous one; a notification email is sent to your account address. Treat the key like a password — anyone holding it can act as your account.

Success envelope

Every success response has exactly this shape (produced by res.customSuccess). data is endpoint-specific — an object, an array, or {} when there is nothing to return.

Success
{
  "message": "Human-readable success message",
  "data": { }
}

Error envelope

All errors — auth, validation, business logic, and upstream Meta failures — share one JSON shape. Unexpected failures return 500 with errorType: "Internal".

Error
{
  "httpStatusCode": 400,
  "errorType": "Validation",
  "errorMessage": "\"body.recipientPhone\" is required",
  "errors": [],
  "errorRaw": null,
  "errorsValidation": null
}

Error envelope fields

FieldMeaning
httpStatusCodeMirrors the HTTP status of the response.
errorTypeError category — Validation, General, WhatsappOfficialAPI, Billable, Internal, Raw, Unauthorized.
errorMessageHuman-readable summary.
errorsArray of extra detail strings, or null.
errorRawRaw underlying error when available (for Meta errors: the Meta error response as a JSON string), or null.
errorsValidationUsually null.

Request validation (Joi)

Endpoints that take input validate it with Joi (some simple endpoints have no schema). Each schema targets one segment of the request — usually the body, sometimes the query string — and runs with abortEarly: true and allowUnknown: false. In practice:

  • Only the first violation is reported — fix it and re-send to see the next one.
  • Unknown fields are rejected — a field not in the schema is a 400, not silently ignored.
  • Failures return the error envelope with errorType: "Validation" and Joi's message (e.g. "body.name" is required); HTTP status 400.

How Meta Graph API errors are surfaced

Endpoints that proxy the Meta WhatsApp Business (Graph) API normalize provider failures into the standard error envelope:

AspectBehavior
400Almost always 400 with errorType: "WhatsappOfficialAPI".
402Special case: Meta billing-block error code 131042 (payment problem on the WABA) is returned as 402 Payment Required with an actionable message telling you to fix the payment method in Meta Business Manager.
errorMessageMeta's error.error_data.details verbatim when present; otherwise Whatsapp Official: <Meta error.message>.
errorsMeta's error_user_title and error_user_msg on the generic/billing branch; empty [] when the error_data.details message was used.
errorRawThe full Meta error response, JSON-stringified — inspect it for Meta's code, type, and fbtrace_id.

Every call to Meta (success or failure) is also logged server-side for diagnostics.

Rate limiting

The backend applies no rate limiting of its own on /v3/whatsapp-official/* routes. Meta's own Graph API rate and messaging limits still apply and surface as normalized WhatsappOfficialAPI errors as described above.

Vocabularies

Phone number status

CONNECTEDPENDINGFLAGGEDRESTRICTED

Quality rating

GREENYELLOWRED

Template status

PENDINGAPPROVEDREJECTEDPAUSEDDISABLED

Template category

MARKETINGUTILITYAUTHENTICATION

Getting started

The typical integration flow:

  1. 1Connect a WhatsApp Business Account via Meta Embedded Signup and complete the setup endpoint so the backend stores your account link.
  2. 2List the phone numbers attached to the account and pick a phone_number_id.
  3. 3Register that phone number for Cloud API messaging.
  4. 4Create a message template and wait for Meta to approve it (status moves from PENDING to APPROVED — poll the template endpoints).
  5. 5Start sending messages — template messages to open new conversations, free-form messages inside the 24-hour customer service window.

Quick reference

Endpoint index

MethodPathPurpose
GET/v3/whatsapp-officialGet connected account
POST/v3/whatsapp-official/setupComplete Embedded Signup
POST/v3/whatsapp-official/setup-linkEmbedded Signup via manager invite
GET/v3/whatsapp-official/activitiesAccount activity log
POST/v3/whatsapp-official/register-phone-number/:phoneNumberIdRegister phone number (deprecated)
POST/v3/whatsapp-official/subscribeRe-subscribe to webhooks
GET/v3/whatsapp-official/phone-numbersList phone numbers
POST/v3/whatsapp-official/phone-numbersAdd a phone number
GET/v3/whatsapp-official/phone-numbers/:phoneNumberIdGet one phone number
POST/v3/whatsapp-official/phone-numbers/:phoneNumberId/registerRegister the number (Cloud API)
POST/v3/whatsapp-official/phone-numbers/:phoneNumberId/deregisterDeregister the number
POST/v3/whatsapp-official/phone-numbers/:phoneNumberId/request-codeRequest a verification code
POST/v3/whatsapp-official/phone-numbers/:phoneNumberId/verify-codeSubmit the verification code
GET/v3/whatsapp-official/phone-numbers/:phoneNumberId/commerce-settingsRead commerce settings
POST/v3/whatsapp-official/phone-numbers/:phoneNumberId/commerce-settingsUpdate commerce settings
POST/v3/whatsapp-official/phone-numbers/:phoneNumberId/start-contact-syncStart a contact sync
POST/v3/whatsapp-official/phone-numbers/:phoneNumberId/start-message-history-syncStart a message-history sync
GET/v3/whatsapp-official/phone-numbers/:phoneNumberId/message-historyRead synced message history
GET/v3/whatsapp-official/phone-numbers/:phoneNumberId/whatsapp-profileRead the WhatsApp Business profile
GET/v3/whatsapp-official/message-templatesList templates
GET/v3/whatsapp-official/message-templates/:messageTemplateIdGet one template
POST/v3/whatsapp-official/message-templatesCreate a template
DELETE/v3/whatsapp-official/message-templates/:messageTemplateIdDelete a template
GET/v3/whatsapp-official/message-templates/supported-languagesSupported languages
GET/v3/whatsapp-official/message-templates/libraryBrowse the template library
GET/v3/whatsapp-official/message-templates/library/metadataTemplate library filters/metadata
POST/v3/whatsapp-official/messageSend a message
POST/v3/whatsapp-official/analytics/messagingSent/delivered analytics
POST/v3/whatsapp-official/analytics/conversationConversation & cost analytics
POST/v3/whatsapp-official/analytics/templatePer-template analytics
GET/v3/managers/users/:userId/whatsapp-official/phone-numbersList a managed user's phone numbers
GET/v3/managers/users/:userId/whatsapp-official/message-templatesList a managed user's templates
POST/v3/managers/users/:userId/whatsapp-official/message-templatesCreate a template for a managed user
POST/v3/managers/users/:userId/whatsapp-official/register-phone-number/:phoneNumberIdRegister a managed user's number
POST/v3/managers/users/:userId/whatsapp-official/subscribeSubscribe a managed user's WABA
POST/v3/managers/users/:userId/whatsapp-official/messageSend a message as a managed user
GET/v2/webhook/whatsapp/official/handlerWebhook verification handshake
POST/v2/webhook/whatsapp/official/handlerEvent ingress + forwarding

6 endpoints

Account & Setup

Connect a WhatsApp Business Account (WABA) to a user via Meta Embedded Signup, inspect the stored account, and manage the webhook subscription. All routes are mounted under /v3/whatsapp-official with JWT-or-api_key auth, except POST /v3/whatsapp-official/setup-link, which is deliberately unauthenticated (linkage-checked instead).

Get connected account

GET/v3/whatsapp-official

Returns the authenticated user's stored WhatsApp Business Account record. The accessToken and internal verification tokens are deliberately excluded — only non-sensitive fields are returned.

Auth — JWT or api_key. Authorization: Bearer <jwt> is treated as a JWT; any other non-empty Authorization value is treated as the account api_key; an api_key field in the request body is checked first and wins if present.

Request

None — no parameters, query, or body.

Response

200 OK — success envelope { message, data }. data.account contains exactly the fields below; secrets are never included.

data.account

FieldTypeDescription
idintegerInternal account row id.
businessIdstringThe connected WABA ID.
ownerBusinessIdstringID of the Meta business that owns the WABA.
ownerBusinessNamestringDisplay name of the owning Meta business.
webhookSubscribedbooleanWhether the WABA is currently subscribed to the app's webhooks.
unilinkWebhookUrlstring | nullForward URL for inbound events (Unilink), or null.
unibotWebhookUrlstring | nullForward URL for inbound events (Unibot), or null.
createdAtstringISO 8601 creation timestamp.
updatedAtstringISO 8601 last-update timestamp.
Response example
{
  "message": "WhatsApp account details fetched successfully",
  "data": {
    "account": {
      "id": 42,
      "businessId": "112233445566778",
      "ownerBusinessId": "998877665544332",
      "ownerBusinessName": "Acme Corp",
      "webhookSubscribed": true,
      "unilinkWebhookUrl": "https://example.com/webhooks/unilink",
      "unibotWebhookUrl": null,
      "createdAt": "2026-05-01T09:30:00.000Z",
      "updatedAt": "2026-06-20T14:12:45.000Z"
    }
  }
}

Errors

StatusWhen
401Missing or invalid credentials.
404No WhatsApp Official account has been set up for this user (errorMessage: "WhatsApp account not found").

Notes

  • Read-only — no side effects.

Complete Embedded Signup

POST/v3/whatsapp-official/setup

Completes Meta's Embedded Signup flow. The one-time authCode from the signup popup is exchanged with Meta's /oauth/access_token for a long-lived token; the WABA business info is fetched, the account row is created/updated, and the WABA is subscribed to the app's webhooks (POST /{wabaId}/subscribed_apps).

Auth — JWT or api_key.

Request

FieldTypeRequiredDescription
authCodestringrequiredOne-time authorization code from Meta Embedded Signup.
businessIdstringrequiredThe WABA ID selected during Embedded Signup.
Request example
{
  "authCode": "AQBx7...redacted-one-time-code",
  "businessId": "112233445566778"
}

Response

200 OKdata is the WABA business info fetched from Meta (passed through verbatim; fields requested: id,name,owner_business_info).

Response example
{
  "message": "Whatsapp official setup successful",
  "data": {
    "id": "112233445566778",
    "name": "Acme WhatsApp",
    "owner_business_info": {
      "id": "998877665544332",
      "name": "Acme Corp"
    }
  }
}

Errors

StatusWhen
400Validation failed (missing/unknown fields), or Meta rejected the code exchange / Graph call (mapped to a WhatsappOfficialAPI error).
401Missing or invalid credentials.
402Meta billing-block error codes (e.g. 131042, payment issue on the WABA).
404Authenticated user id does not resolve to a user (User not found).

Notes

  • Calls Meta twice (token exchange + business-info fetch) plus a webhook-subscribe call.
  • Creates or updates the user's WhatsApp Official account row and stores the access token server-side only — it is never returned to the client. Sets webhookSubscribed = true.

Account activity logPaginated

GET/v3/whatsapp-official/activities

Returns the WABA's audit/activity log, proxied verbatim from Meta (GET /{wabaId}/activities). Supports Meta cursor pagination and time-window filters.

Auth — JWT or api_key.

Query parameters

FieldTypeRequiredDescription
fieldsstringoptionalComma-separated field list. Default: id,activity_type,timestamp,actor_type,actor_id,actor_name,description,details,ip_address,user_agent.
limitintegeroptional1–100 (also clamped server-side). Schema default 25.
afterstringoptionalPagination cursor (next page).
beforestringoptionalPagination cursor (previous page).
sincestringoptionalUnix timestamp or ISO 8601 date.
untilstringoptionalUnix timestamp or ISO 8601 date.
activity_typestringoptionalFilter by activity type (e.g. TEMPLATE_CREATED, PHONE_NUMBER_ADDED, SECURITY_EVENT).

Request

Request
GET /v3/whatsapp-official/activities?limit=2&activity_type=TEMPLATE_CREATED

Response

200 OKdata is the Meta activity list passed through verbatim ({ data: Activity[], paging? }). Each row has id, activity_type, timestamp, actor_type (USER | SYSTEM | API | ADMIN | AUTOMATED_PROCESS), and optionally actor_id, actor_name, description, details, ip_address, user_agent.

Response example
{
  "message": "Activities fetched successfully",
  "data": {
    "data": [
      {
        "id": "1054321987654321",
        "activity_type": "TEMPLATE_CREATED",
        "timestamp": "2026-07-01T10:15:00+0000",
        "actor_type": "USER",
        "actor_id": "10203040506070",
        "actor_name": "Jane Admin",
        "description": "Message template order_update created"
      }
    ],
    "paging": {
      "cursors": { "before": "QVFIU...", "after": "QVFIV..." },
      "next": "https://graph.facebook.com/v21.0/112233445566778/activities?after=QVFIV..."
    }
  }
}

Errors

StatusWhen
400Validation failed (e.g. limit out of range, unknown query field), or Meta Graph API error.
401Missing or invalid credentials.
404No WhatsApp Official account set up for this user.

Notes

  • Read-only call to Meta — no local writes.

Register phone number (deprecated)Deprecated

POST/v3/whatsapp-official/register-phone-number/:phoneNumberId

Deprecated — prefer POST /v3/whatsapp-official/phone-numbers/{phoneNumberId}/register. Registers a phone number with WhatsApp Cloud API by calling Meta's POST /{phoneNumberId}/register with a fixed two-step-verification PIN of 000000.

Auth — JWT or api_key.

Path parameters

FieldTypeRequiredDescription
phoneNumberIdstringrequiredMeta phone number ID belonging to the user's WABA.

Request

No request body. No Joi validation on this route.

Request
POST /v3/whatsapp-official/register-phone-number/109876543212345

Response

200 OKdata.phoneNumber is Meta's registration response passed through verbatim (typically { "success": true }).

Response example
{
  "message": "Phone number registered successfully",
  "data": {
    "phoneNumber": { "success": true }
  }
}

Errors

StatusWhen
400Meta rejected the registration (e.g. number not verified, wrong PIN state).
401Missing or invalid credentials.
404No WhatsApp Official account set up for this user.

Notes

  • Registration call to Meta; no local DB writes.

Re-subscribe to webhooks

POST/v3/whatsapp-official/subscribe

(Re-)subscribes the user's WABA to the application's webhooks by calling Meta's POST /{wabaId}/subscribed_apps. Setup already does this automatically; use this endpoint to re-subscribe if the subscription was lost.

Auth — JWT or api_key.

Request

None — no parameters, query, or body.

Response

200 OKdata.subscription is Meta's response passed through verbatim (typically { "success": true }).

Response example
{
  "message": "Whatsapp official subscribed successfully",
  "data": {
    "subscription": { "success": true }
  }
}

Errors

StatusWhen
400Meta Graph API error (e.g. expired/invalid stored access token).
401Missing or invalid credentials.
404No WhatsApp Official account set up for this user.

Notes

  • On success sets webhookSubscribed = true on the stored account row and saves it.

13 endpoints

Phone Numbers

Manage the business phone numbers attached to your WABA. Base path /v3/whatsapp-official/phone-numbers. Every endpoint resolves your WhatsApp Official account first (404 if none), proxies the Meta Graph API with the stored WABA token, and logs each Meta call to meta_api_logs. Meta Graph errors surface as 400 WhatsappOfficialAPI (or 402 for billing-block code 131042).

List phone numbers

GET/v3/whatsapp-official/phone-numbers

Lists all phone numbers registered under your WABA. Proxies GET /{wabaId}/phone_numbers requesting id, cc, country_dial_code, display_phone_number, verified_name, status, quality_rating, search_visibility, platform_type, code_verification_status, messaging_limit_tier, country_code, username, name_status.

Auth — JWT or api_key. Authorization: Bearer <jwt> for JWT; any other non-empty Authorization value is treated as your api_key; an api_key field in the request body is also accepted.

Request

None — no params, query, or body.

Response

200 OK "Phone numbers fetched successfully". data.phoneNumbers is the Meta data array passed through verbatim.

Response example
{
  "message": "Phone numbers fetched successfully",
  "data": {
    "phoneNumbers": [
      {
        "id": "106540352242922",
        "cc": "1",
        "country_dial_code": "1",
        "display_phone_number": "+1 631-555-1234",
        "verified_name": "Acme Support",
        "status": "CONNECTED",
        "quality_rating": "GREEN",
        "search_visibility": "VISIBLE",
        "platform_type": "CLOUD_API",
        "code_verification_status": "VERIFIED",
        "messaging_limit_tier": "TIER_1K",
        "country_code": "US",
        "name_status": "APPROVED"
      }
    ]
  }
}

Errors

StatusWhen
400Auth failed — missing Authorization header (also 401 for invalid/expired JWT, 403 for unknown api_key).
404No WhatsApp Official account is set up for this user.
400Meta Graph API rejected the request (passed through as WhatsappOfficialAPI).
402Meta billing block on the WABA (Meta error code 131042).

Add a phone number

POST/v3/whatsapp-official/phone-numbers

Adds a new phone number to your WABA. Proxies POST /{wabaId}/phone_numbers. The number will still need verification and registration afterwards.

Auth — JWT or api_key. Authorization: Bearer <jwt> for JWT; any other non-empty Authorization value is treated as your api_key; an api_key field in the request body is also accepted.

Request

No Joi validation on this route — fields are read directly and forwarded to Meta, which rejects missing ones.

FieldTypeRequiredDescription
phone_numberstringrequiredThe phone number without country code, e.g. "6315551234" (enforced by Meta).
verified_namestringrequiredDisplay name to verify for the number (enforced by Meta).
ccstringrequiredCountry dial code, e.g. "1" (enforced by Meta).
Request example
{
  "phone_number": "6315551234",
  "verified_name": "Acme Support",
  "cc": "1"
}

Response

200 OK "Phone number created successfully". Meta returns the new phone number's ID. Note: the service reads response.data; Meta's create response is { "id": "..." } at the top level, so treat data.phoneNumber defensively.

Response example
{
  "message": "Phone number created successfully",
  "data": { "phoneNumber": { "id": "106540352242922" } }
}

Errors

StatusWhen
400Auth failed — missing Authorization header (also 401 for invalid/expired JWT, 403 for unknown api_key).
404No WhatsApp Official account is set up for this user.
400Meta rejected the number (already registered elsewhere, invalid format, missing field, etc.).
402Meta billing block (code 131042).

Notes

  • Creates the phone number on Meta's side.

Get one phone number

GET/v3/whatsapp-official/phone-numbers/:phoneNumberId

Fetches a single phone number's details. Proxies GET /{phoneNumberId} with fields id, country_dial_code, display_phone_number, verified_name, status, quality_rating, search_visibility, platform_type, code_verification_status, messaging_limit_tier, country_code, name_status.

Auth — JWT or api_key. Authorization: Bearer <jwt> for JWT; any other non-empty Authorization value is treated as your api_key; an api_key field in the request body is also accepted.

Path parameters

FieldTypeRequiredDescription
phoneNumberIdstringrequiredMeta phone number ID (path parameter).

Response

200 OK "Phone number fetched successfully". The Meta object is passed through verbatim.

Response example
{
  "message": "Phone number fetched successfully",
  "data": {
    "phoneNumber": {
      "id": "106540352242922",
      "country_dial_code": "1",
      "display_phone_number": "+1 631-555-1234",
      "verified_name": "Acme Support",
      "status": "CONNECTED",
      "quality_rating": "GREEN",
      "search_visibility": "VISIBLE",
      "platform_type": "CLOUD_API",
      "code_verification_status": "VERIFIED",
      "messaging_limit_tier": "TIER_1K",
      "country_code": "US",
      "name_status": "APPROVED"
    }
  }
}

Errors

StatusWhen
400Auth failed — missing Authorization header (also 401 for invalid/expired JWT, 403 for unknown api_key).
404No WhatsApp Official account is set up for this user.
400Unknown/inaccessible phoneNumberId or other Meta error.

Register the number (Cloud API)

POST/v3/whatsapp-official/phone-numbers/:phoneNumberId/register

Registers the phone number with WhatsApp Cloud API so it can send and receive messages. Proxies POST /{phoneNumberId}/register with messaging_product: "whatsapp" and a fixed two-factor PIN of 000000 (set server-side, not client-configurable).

Auth — JWT or api_key. Authorization: Bearer <jwt> for JWT; any other non-empty Authorization value is treated as your api_key; an api_key field in the request body is also accepted.

Path parameters

FieldTypeRequiredDescription
phoneNumberIdstringrequiredMeta phone number ID (path parameter).

Request

No request body.

Response

200 OK "Phone number registered successfully". Meta's response is passed through verbatim.

Response example
{
  "message": "Phone number registered successfully",
  "data": { "phoneNumber": { "success": true } }
}

Errors

StatusWhen
400Auth failed — missing Authorization header (also 401 for invalid/expired JWT, 403 for unknown api_key).
404No WhatsApp Official account is set up for this user.
400Meta rejected registration (number not verified, PIN mismatch with an existing two-step PIN, etc.).

Notes

  • Registers the number on Meta's Cloud API.

Deregister the number

POST/v3/whatsapp-official/phone-numbers/:phoneNumberId/deregister

Deregisters the phone number from WhatsApp Cloud API messaging. Proxies POST /{phoneNumberId}/deregister. The number can no longer send/receive via Cloud API until re-registered.

Auth — JWT or api_key. Authorization: Bearer <jwt> for JWT; any other non-empty Authorization value is treated as your api_key; an api_key field in the request body is also accepted.

Path parameters

FieldTypeRequiredDescription
phoneNumberIdstringrequiredMeta phone number ID (path parameter).

Request

No request body.

Response

200 OK "Phone number deregistered successfully".

Response example
{
  "message": "Phone number deregistered successfully",
  "data": { "phoneNumber": { "success": true } }
}

Errors

StatusWhen
400Auth failed — missing Authorization header (also 401 for invalid/expired JWT, 403 for unknown api_key).
404No WhatsApp Official account is set up for this user.
400Meta error (e.g. number not currently registered).

Request a verification code

POST/v3/whatsapp-official/phone-numbers/:phoneNumberId/request-code

Requests a phone-ownership verification code, delivered by SMS or voice call. Proxies POST /{phoneNumberId}/request_code with code_method from the body and language fixed to en_US.

Auth — JWT or api_key. Authorization: Bearer <jwt> for JWT; any other non-empty Authorization value is treated as your api_key; an api_key field in the request body is also accepted.

Path parameters

FieldTypeRequiredDescription
phoneNumberIdstringrequiredMeta phone number ID (path parameter).

Request

No Joi validation on this route; codeMethod is forwarded to Meta, which rejects invalid values.

FieldTypeRequiredDescription
codeMethodstringrequiredSMS or VOICE (enforced by Meta).
Request example
{ "codeMethod": "SMS" }

Response

200 OK "Verification code requested successfully".

Response example
{
  "message": "Verification code requested successfully",
  "data": { "code": { "success": true } }
}

Errors

StatusWhen
400Auth failed — missing Authorization header (also 401 for invalid/expired JWT, 403 for unknown api_key).
404No WhatsApp Official account is set up for this user.
400Meta error (invalid codeMethod, number already verified, rate-limited code requests, etc.).

Notes

  • Meta sends a verification code to the phone.

Submit the verification code

POST/v3/whatsapp-official/phone-numbers/:phoneNumberId/verify-code

Submits the code received via request-code to complete phone verification. Proxies POST /{phoneNumberId}/verify_code. On success the number's code_verification_status becomes VERIFIED.

Auth — JWT or api_key. Authorization: Bearer <jwt> for JWT; any other non-empty Authorization value is treated as your api_key; an api_key field in the request body is also accepted.

Path parameters

FieldTypeRequiredDescription
phoneNumberIdstringrequiredMeta phone number ID (path parameter).

Request

No Joi validation on this route.

FieldTypeRequiredDescription
codestringrequiredThe 6-digit code received by SMS/voice (enforced by Meta).
Request example
{ "code": "123456" }

Response

200 OK "Verification code verified successfully".

Response example
{
  "message": "Verification code verified successfully",
  "data": { "verification": { "success": true } }
}

Errors

StatusWhen
400Auth failed — missing Authorization header (also 401 for invalid/expired JWT, 403 for unknown api_key).
404No WhatsApp Official account is set up for this user.
400Wrong/expired code or other Meta error.

Read commerce settings

GET/v3/whatsapp-official/phone-numbers/:phoneNumberId/commerce-settings

Fetches WhatsApp commerce settings (shopping cart and catalog visibility) for the phone number. Proxies GET /{phoneNumberId}/whatsapp_commerce_settings.

Auth — JWT or api_key. Authorization: Bearer <jwt> for JWT; any other non-empty Authorization value is treated as your api_key; an api_key field in the request body is also accepted.

Path parameters

FieldTypeRequiredDescription
phoneNumberIdstringrequiredMeta phone number ID (path parameter).

Response

200 OK "Commerce settings fetched successfully". The Meta response is passed through verbatim (note the nested data array — Meta's list shape).

Response example
{
  "message": "Commerce settings fetched successfully",
  "data": {
    "data": [
      {
        "id": "106540352242922",
        "is_cart_enabled": true,
        "is_catalog_visible": true
      }
    ]
  }
}

Errors

StatusWhen
400Auth failed — missing Authorization header (also 401 for invalid/expired JWT, 403 for unknown api_key).
404No WhatsApp Official account is set up for this user.
400Meta error (unknown phone number, no commerce features on the account, etc.).

Update commerce settings

POST/v3/whatsapp-official/phone-numbers/:phoneNumberId/commerce-settings

Updates cart and catalog visibility. Validated with commerceSettingsUpdateSchema (abortEarly: true, allowUnknown: false). The backend converts the JSON body into query parameters on POST /{phoneNumberId}/whatsapp_commerce_settings, as Meta requires.

Auth — JWT or api_key. Authorization: Bearer <jwt> for JWT; any other non-empty Authorization value is treated as your api_key; an api_key field in the request body is also accepted.

Path parameters

FieldTypeRequiredDescription
phoneNumberIdstringrequiredMeta phone number ID (path parameter).

Request

FieldTypeRequiredDescription
is_cart_enabledbooleanrequiredMust be a boolean.
is_catalog_visiblebooleanrequiredMust be a boolean.
Request example
{
  "is_cart_enabled": true,
  "is_catalog_visible": false
}

Response

200 OK "Commerce settings updated successfully". Meta's result is passed through verbatim.

Response example
{
  "message": "Commerce settings updated successfully",
  "data": { "success": true }
}

Errors

StatusWhen
400Validation failed (missing/non-boolean field, unknown extra field) — errorType: "Validation".
400Auth failed — missing Authorization header (also 401 for invalid/expired JWT, 403 for unknown api_key).
404No WhatsApp Official account is set up for this user.
400Meta error — errorType: "WhatsappOfficialAPI".

Notes

  • Changes commerce settings on Meta.

Start a contact sync

POST/v3/whatsapp-official/phone-numbers/:phoneNumberId/start-contact-sync

Triggers a contact/app-state sync for a business phone number connected to the WhatsApp Business app (SMB). Proxies POST /{phoneNumberId}/smb_app_data with { messaging_product: "whatsapp", sync_type: "smb_app_state_sync" }.

Auth — JWT or api_key. Authorization: Bearer <jwt> for JWT; any other non-empty Authorization value is treated as your api_key; an api_key field in the request body is also accepted.

Path parameters

FieldTypeRequiredDescription
phoneNumberIdstringrequiredMeta phone number ID (path parameter).

Request

No request body.

Response

200 OK "Contact sync started successfully". Meta's response is passed through verbatim.

Response example
{
  "message": "Contact sync started successfully",
  "data": {
    "messaging_product": "whatsapp",
    "request_id": "1234567890123456"
  }
}

Errors

StatusWhen
400Auth failed — missing Authorization header (also 401 for invalid/expired JWT, 403 for unknown api_key).
404No WhatsApp Official account is set up for this user.
400Meta error (number not eligible for SMB app data sync, etc.).

Notes

  • Starts an asynchronous sync on Meta; results arrive via webhooks.

Start a message-history sync

POST/v3/whatsapp-official/phone-numbers/:phoneNumberId/start-message-history-sync

Triggers a message-history sync for a business phone number connected to the WhatsApp Business app. Proxies POST /{phoneNumberId}/smb_app_data with { messaging_product: "whatsapp", sync_type: "history" }.

Auth — JWT or api_key. Authorization: Bearer <jwt> for JWT; any other non-empty Authorization value is treated as your api_key; an api_key field in the request body is also accepted.

Path parameters

FieldTypeRequiredDescription
phoneNumberIdstringrequiredMeta phone number ID (path parameter).

Request

No request body.

Response

200 OK "Message history sync started successfully".

Response example
{
  "message": "Message history sync started successfully",
  "data": {
    "messaging_product": "whatsapp",
    "request_id": "9876543210987654"
  }
}

Errors

StatusWhen
400Auth failed — missing Authorization header (also 401 for invalid/expired JWT, 403 for unknown api_key).
404No WhatsApp Official account is set up for this user.
400Meta error (number not eligible, sync already in progress, etc.).

Notes

  • Starts an asynchronous history sync; synced messages arrive via webhooks and become queryable via message-history.

Read synced message historyPaginated

GET/v3/whatsapp-official/phone-numbers/:phoneNumberId/message-history

Retrieves paginated message history for the phone number, including per-message delivery status events. Query validated with messageHistoryQuerySchema (abortEarly: true, allowUnknown: false). Proxies GET /{phoneNumberId}/message_history; limit is additionally clamped to 1–100 before calling Meta.

Auth — JWT or api_key. Authorization: Bearer <jwt> for JWT; any other non-empty Authorization value is treated as your api_key; an api_key field in the request body is also accepted.

Path parameters

FieldTypeRequiredDescription
phoneNumberIdstringrequiredMeta phone number ID (path parameter).

Query parameters

FieldTypeRequiredDescription
message_idstringoptionalFilter by a specific WhatsApp message ID (WAMID).
fieldsstringoptionalComma-separated field selector forwarded to Meta, e.g. id,message_id,events.
limitintegeroptionalMin 1, max 100. Default 25.
afterstringoptionalCursor for the next page.
beforestringoptionalCursor for the previous page.

Request

Request
GET /v3/whatsapp-official/phone-numbers/106540352242922/message-history?limit=25&after=MjQZD

Response

200 OK "Message history fetched successfully". The Meta response (data + paging) is passed through verbatim.

Response example
{
  "message": "Message history fetched successfully",
  "data": {
    "data": [
      {
        "id": "1057412...9968",
        "message_id": "wamid.HBgLMTYzMTU1NTEyMzQVAgARGBI5QTNDQTVCM0Q0Q0Q2RTY3RTcA",
        "events": {
          "data": [
            { "event_type": "SENT", "timestamp": "1720608000" },
            { "event_type": "DELIVERED", "timestamp": "1720608004" }
          ]
        }
      }
    ],
    "paging": {
      "cursors": { "before": "QVFIU...", "after": "QVFIU..." },
      "next": "https://graph.facebook.com/..."
    }
  }
}

Errors

StatusWhen
400Query validation failed (limit out of 1–100, unknown query param) — errorType: "Validation".
400Auth failed — missing Authorization header (also 401 for invalid/expired JWT, 403 for unknown api_key).
404No WhatsApp Official account is set up for this user.
400Meta error (invalid cursor, unknown WAMID, etc.) — errorType: "WhatsappOfficialAPI".

Read the WhatsApp Business profile

GET/v3/whatsapp-official/phone-numbers/:phoneNumberId/whatsapp-profile

Fetches the public WhatsApp Business profile for the phone number. Proxies GET /{phoneNumberId}/whatsapp_business_profile, always requesting all fields (messaging_product, about, address, description, email, profile_picture_url, websites, vertical) — not client-configurable.

Auth — JWT or api_key. Authorization: Bearer <jwt> for JWT; any other non-empty Authorization value is treated as your api_key; an api_key field in the request body is also accepted.

Path parameters

FieldTypeRequiredDescription
phoneNumberIdstringrequiredMeta phone number ID (path parameter).

Response

200 OK "Whatsapp business profile fetched successfully". The Meta response (a data array containing the profile) is passed through verbatim.

Response example
{
  "message": "Whatsapp business profile fetched successfully",
  "data": {
    "whatsappBusinessProfile": {
      "data": [
        {
          "business_profile": {
            "messaging_product": "whatsapp",
            "about": "We reply within 24 hours",
            "address": "1 Hacker Way, Menlo Park, CA",
            "description": "Acme customer support",
            "email": "support@acme.com",
            "profile_picture_url": "https://pps.whatsapp.net/...",
            "websites": ["https://acme.com"],
            "vertical": "RETAIL"
          }
        }
      ]
    }
  }
}

Errors

StatusWhen
400Auth failed — missing Authorization header (also 401 for invalid/expired JWT, 403 for unknown api_key).
404No WhatsApp Official account is set up for this user.
400Meta error (unknown phone number, insufficient permissions on the token, etc.).

7 endpoints

Message Templates

Manage WhatsApp message templates for your connected WABA, under /v3/whatsapp-official/message-templates. Except where noted, the backend proxies the Meta Graph API and template data passes through verbatim. Validation uses abortEarly: true (first error only) and allowUnknown: false (unknown fields rejected).

List templates

GET/v3/whatsapp-official/message-templates

Lists all message templates on your WABA. Data comes from Meta's GET /{waba-id}/message_templates with fields language,name,rejected_reason,status,category,sub_category,last_updated_time,components,quality_score.

Auth — JWT or api_key.

Request

None.

Response

200 OK. Each template is Meta's object: id, name, language, status (PENDING / APPROVED / REJECTED, plus PAUSED / DISABLED), category (MARKETING / UTILITY / AUTHENTICATION), rejected_reason when rejected, last_updated_time, components (uppercase HEADER / BODY / FOOTER / BUTTONS), and quality_score.

Response example
{
  "message": "Message templates fetched successfully",
  "data": {
    "messageTemplates": [
      {
        "id": "1093849275018274",
        "name": "summer_sale_2026",
        "language": "en_US",
        "status": "APPROVED",
        "category": "MARKETING",
        "last_updated_time": "2026-06-30T14:22:10+0000",
        "quality_score": { "score": "GREEN", "date": 1751292130 },
        "components": [
          { "type": "HEADER", "format": "TEXT", "text": "Summer Sale!" },
          {
            "type": "BODY",
            "text": "Hi {{1}}, get {{2}}% off everything until Sunday.",
            "example": { "body_text": [["Sara", "25"]] }
          },
          { "type": "FOOTER", "text": "Reply STOP to unsubscribe" },
          {
            "type": "BUTTONS",
            "buttons": [{ "type": "QUICK_REPLY", "text": "Show me deals" }]
          }
        ]
      }
    ]
  }
}

Errors

StatusWhen
400No Authorization header provided (errorMessage: "Authorization header not provided").
401Invalid or expired JWT.
403Invalid api_key.
404No WhatsApp Official account is connected for this user (errorType: "General", errorMessage: "Not found").
400Meta Graph API rejected the call — errorType: "WhatsappOfficialAPI", errorMessage: "Whatsapp Official: <Meta message>" (or Meta's error_data.details text), raw Meta error JSON in errorRaw.
402Meta billing block on the WABA (missing/expired payment method).

Get one template

GET/v3/whatsapp-official/message-templates/:messageTemplateId

Fetches a single message template by its Meta template ID (GET /{template-id}, returned verbatim).

Auth — JWT or api_key.

Path parameters

FieldTypeRequiredDescription
messageTemplateIdstringrequiredMeta template ID.

Response

200 OK.

Response example
{
  "message": "Message template fetched successfully",
  "data": {
    "messageTemplate": {
      "id": "1093849275018274",
      "name": "summer_sale_2026",
      "language": "en_US",
      "status": "APPROVED",
      "category": "MARKETING",
      "components": [
        { "type": "BODY", "text": "Hi {{1}}, get {{2}}% off everything until Sunday." }
      ]
    }
  }
}

Errors

StatusWhen
400No Authorization header provided (errorMessage: "Authorization header not provided").
401Invalid or expired JWT.
403Invalid api_key.
404No WhatsApp Official account is connected for this user (errorType: "General", errorMessage: "Not found").
400Meta Graph API rejected the call — errorType: "WhatsappOfficialAPI", errorMessage: "Whatsapp Official: <Meta message>" (or Meta's error_data.details text), raw Meta error JSON in errorRaw.
402Meta billing block on the WABA (missing/expired payment method).
400The ID does not exist or is not accessible by your WABA (Meta error).

Create a template

POST/v3/whatsapp-official/message-templates

Creates a template on your WABA — either fully custom (via components) or instantiated from Meta's Template Library (via library_template_name). Calls Meta's POST /{waba-id}/message_templates; the new template must then pass Meta review (initial status usually PENDING).

Auth — JWT or api_key.

Request

Top-level body

FieldTypeRequiredDescription
namestringrequiredLowercase alphanumeric and underscores only (^[a-z0-9_]+$), max 512 chars.
categorystringrequiredOne of authentication, marketing, utility (uppercase variants accepted). Must be utility/UTILITY when library_template_name is used.
languagestringrequiredA supported WhatsApp language code (e.g. en_US, ar, es_MX).
parameter_formatstringoptionalnamed or positional.
componentsarrayconditionalRequired for custom templates; forbidden when library_template_name is set. Item shape depends on category (see below).
library_template_namestringoptionalName of a Meta Template Library template to instantiate.
library_template_button_inputsarrayoptionalOnly allowed together with library_template_name.

components[] — marketing / utility

Unknown extra keys inside a component object are tolerated; unknown top-level body fields are rejected.

FieldTypeRequiredDescription
typestringrequiredheader, body, footer, or buttons.
formatstringoptionalOnly on header: text, image, video, or document.
textstringoptionalComponent text; use {{1}}, {{2}}, … for variables.
exampleobjectoptionalheader_text: string[]; header_handle: string[] (media handles); body_text: string[][].
buttonsarrayoptionalOnly on buttons. Each: type (quick_reply | url | phone_number), text, url (url type), phone_number (phone type), example (string[]).

components[] — authentication

FieldTypeRequiredDescription
typestringrequiredbody, footer, or buttons.
add_security_recommendationbooleanon bodyRequired when type is body; forbidden otherwise.
code_expiration_minutesnumberoptionalOnly on footer.
buttonsarrayoptionalOnly on buttons. Each button: type = OTP, otp_type = COPY_CODE or ONE_TAP.

library_template_button_inputs[]

FieldTypeRequiredDescription
typestringrequiredURL or PHONE_NUMBER.
urlobjectfor URL{ base_url, url_suffix_example } — both required, both valid URIs.
phone_numberstringfor PHONE_NUMBERE.164 format (^\+[1-9]\d{1,14}$, e.g. +16315551010).
Marketing template with body variables + quick-reply button
{
  "name": "summer_sale_2026",
  "category": "marketing",
  "language": "en_US",
  "components": [
    { "type": "header", "format": "text", "text": "Summer Sale!" },
    {
      "type": "body",
      "text": "Hi {{1}}, get {{2}}% off everything until Sunday. Use code {{3}} at checkout.",
      "example": { "body_text": [["Sara", "25", "SUMMER25"]] }
    },
    { "type": "footer", "text": "Reply STOP to unsubscribe" },
    {
      "type": "buttons",
      "buttons": [
        { "type": "quick_reply", "text": "Show me deals" },
        { "type": "url", "text": "Shop now", "url": "https://shop.example.com/sale" }
      ]
    }
  ]
}
Template from Meta's Template Library
{
  "name": "order_delivery_update_1",
  "category": "utility",
  "language": "en_US",
  "library_template_name": "order_delivery_update",
  "library_template_button_inputs": [
    {
      "type": "URL",
      "url": {
        "base_url": "https://track.example.com",
        "url_suffix_example": "https://track.example.com/orders/12345"
      }
    }
  ]
}

Response

200 OKdata.messageTemplate is Meta's create response, verbatim.

Response example
{
  "message": "Message template created successfully",
  "data": {
    "messageTemplate": {
      "id": "1093849275018274",
      "status": "PENDING",
      "category": "MARKETING"
    }
  }
}

Errors

StatusWhen
400Validation failed (bad name pattern, invalid category/language, components together with library_template_name, library_template_button_inputs without library_template_name, non-utility category with a library template, or any unknown top-level field). errorType: "Validation".
400Meta rejected the template (duplicate name for the same language, malformed components).
402See common errors.
404See common errors.

Notes

  • Creates the template on Meta (subject to Meta's review). Nothing is stored in the local database.

Delete a template

DELETE/v3/whatsapp-official/message-templates/:messageTemplateId

Deletes a message template. Calls Meta's DELETE /{waba-id}/message_templates with hsm_id (the path ID) and name (from the request body — Meta requires the template name for deletion by ID).

Auth — JWT or api_key.

Path parameters

FieldTypeRequiredDescription
messageTemplateIdstringrequiredMeta template ID (sent as hsm_id).

Request

FieldTypeRequiredDescription
namestringrecommendedThe template's name; passed to Meta alongside the ID. No Joi validation on this route.
Request example
DELETE /v3/whatsapp-official/message-templates/1093849275018274
{
  "name": "summer_sale_2026"
}

Response

200 OKdata.messageTemplate is Meta's response, typically { "success": true }.

Response example
{
  "message": "Message template deleted successfully",
  "data": {
    "messageTemplate": { "success": true }
  }
}

Errors

StatusWhen
400No Authorization header provided (errorMessage: "Authorization header not provided").
401Invalid or expired JWT.
403Invalid api_key.
404No WhatsApp Official account is connected for this user (errorType: "General", errorMessage: "Not found").
400Meta Graph API rejected the call — errorType: "WhatsappOfficialAPI", errorMessage: "Whatsapp Official: <Meta message>" (or Meta's error_data.details text), raw Meta error JSON in errorRaw.
402Meta billing block on the WABA (missing/expired payment method).
400The ID/name pair does not match an existing template (Meta error).

Notes

  • Deleting by name removes all language versions of that name.

Supported languages

GET/v3/whatsapp-official/message-templates/supported-languages

Returns the full list of WhatsApp-supported template language codes. Served from a static list in the backend — no Meta call and no DB access (auth still required). 111 languages are returned.

Auth — JWT or api_key.

Request

None.

Response

200 OK (list truncated below).

Response example
{
  "message": "Supported languages fetched successfully",
  "data": {
    "languages": [
      { "code": "af", "name": "Afrikaans" },
      { "code": "ar", "name": "Arabic" },
      { "code": "en_US", "name": "English (US)" },
      { "code": "es_MX", "name": "Spanish (Mexico)" }
    ]
  }
}

Errors

StatusWhen
400Missing Authorization header.
401Invalid JWT.
403Invalid api_key.

Browse the template library

GET/v3/whatsapp-official/message-templates/library

Browses Meta's pre-built Template Library (GET /message_template_library). Useful for finding utility templates to instantiate via the create endpoint's library_template_name.

Auth — JWT or api_key.

Query parameters

FieldTypeRequiredDescription
categorystringrequiredAUTHENTICATION, MARKETING, or UTILITY (uppercase only).
languagestringrequiredA supported WhatsApp language code (e.g. en_US).
limitintegeroptionalPositive, max 250. Schema declares default 50, but it is not applied — when omitted, no limit is sent and Meta's own default applies.
searchstringoptionalAccepted by validation but not currently forwarded to Meta.
industrystringoptionalOne of E_COMMERCE, FINANCIAL_SERVICES, TELECOMMUNICATION.
topicstringoptionalOne of ACCOUNT_UPDATE, CUSTOMER_FEEDBACK, ORDER_MANAGEMENT, PAYMENTS.
usecasestringoptionalA use-case code from the metadata endpoint. Accepted by validation but not currently forwarded to Meta.

Request

Request
GET /v3/whatsapp-official/message-templates/library?category=UTILITY&language=en_US&topic=ORDER_MANAGEMENT&limit=25

Response

200 OKdata.templates is Meta's library list, verbatim; empty array when nothing matches.

Response example
{
  "message": "Template library fetched successfully",
  "data": {
    "templates": [
      {
        "name": "order_delivery_update",
        "language": "en_US",
        "category": "UTILITY",
        "topic": "Order Management",
        "usecase": "Delivery Update",
        "industry": ["E-commerce"],
        "header": "Delivery update",
        "body": "Good news! Your order {{1}} is on its way. Track your delivery here.",
        "buttons": [{ "type": "URL", "text": "Track order", "url": { "base_url": "" } }]
      }
    ]
  }
}

Errors

StatusWhen
400Validation failed — missing/invalid category or language, limit out of range, invalid industry/topic/usecase code, or any unknown query param.
400No Authorization header provided (errorMessage: "Authorization header not provided").
401Invalid or expired JWT.
403Invalid api_key.
404No WhatsApp Official account is connected for this user (errorType: "General", errorMessage: "Not found").
400Meta Graph API rejected the call — errorType: "WhatsappOfficialAPI", errorMessage: "Whatsapp Official: <Meta message>" (or Meta's error_data.details text), raw Meta error JSON in errorRaw.
402Meta billing block on the WABA (missing/expired payment method).

Template library filters/metadata

GET/v3/whatsapp-official/message-templates/library/metadata

Returns the available filter values (industries, topics, use cases) for the Template Library endpoint. Served from static lists — no Meta call and no DB access. 3 industries, 4 topics, 25 use cases.

Auth — JWT or api_key.

Request

None.

Response

200 OK (lists truncated below).

Response example
{
  "message": "Template library metadata fetched successfully",
  "data": {
    "industries": [
      { "code": "E_COMMERCE", "name": "E-Commerce" },
      { "code": "FINANCIAL_SERVICES", "name": "Financial Services" },
      { "code": "TELECOMMUNICATION", "name": "Telecommunication" }
    ],
    "topics": [
      { "code": "ACCOUNT_UPDATE", "name": "Account Update" },
      { "code": "ORDER_MANAGEMENT", "name": "Order Management" }
    ],
    "use_cases": [
      { "code": "ORDER_CONFIRMATION", "name": "Order Confirmation" },
      { "code": "PAYMENT_DUE_REMINDER", "name": "Payment Due Reminder" }
    ]
  }
}

Errors

StatusWhen
400Missing Authorization header.
401Invalid JWT.
403Invalid api_key.

1 endpoint

Send Message

Send a single WhatsApp message (text, image, video, document, or template) to one recipient through the Meta Cloud API, using a phone number registered on your WABA. The body is validated with allowUnknown: false — because unknown fields are rejected, pass the api_key in the Authorization header, not the body.

Send a messageDeprecated annotation

POST/v3/whatsapp-official/message

Sends a single WhatsApp message (text / image / video / document / template) to one recipient. The route's Swagger annotation marks it deprecated (pointing to POST /phone-numbers/{phoneNumberId}/messages), but no such replacement exists — this remains the only send-message endpoint.

Auth — JWT or api_key. Authorization: Bearer <jwt> for JWT; any other non-empty Authorization value is treated as your api_key. This endpoint's validator rejects unknown body fields, so send the api_key in the Authorization header, not the body.

Request

Top-level body

Exactly one payload object — the one whose key matches type — must be present. Sending, say, type: "text" together with an image object fails validation.

FieldTypeRequiredDescription
phoneNumberIdstringrequiredThe Meta phone number ID (numeric ID of a phone number on your WABA, e.g. "106540352242922"), not the phone number itself.
recipientPhonestringrequiredDestination phone number. Meta expects international format (E.164, with or without +), e.g. "962790000000".
typestringrequiredOne of text, document, image, video, template.
textobjectif type=textRequired when type = "text"; forbidden otherwise.
imageobjectif type=imageRequired when type = "image"; forbidden otherwise.
videoobjectif type=videoRequired when type = "video"; forbidden otherwise.
documentobjectif type=documentRequired when type = "document"; forbidden otherwise.
templateobjectif type=templateRequired when type = "template"; forbidden otherwise.

text — when type = "text"

FieldTypeRequiredDescription
text.bodystringrequiredThe message text.
text.previewUrlbooleanoptionalDefault false. Whether to render a link preview for URLs in the body.

image / video — when type = "image" | "video"

FieldTypeRequiredDescription
linkstringrequiredPublicly reachable URL of the image/video.
captionstringoptionalCaption shown under the media.

document — when type = "document"

FieldTypeRequiredDescription
document.linkstringrequiredPublicly reachable URL of the document.
document.captionstringoptionalCaption.
document.filenamestringoptionalFilename shown to the recipient.

template — when type = "template"

Component objects allow unknown extra keys (they pass through to Meta unchanged); the top level of the body does not.

FieldTypeRequiredDescription
template.namestringrequiredName of an approved message template on your WABA.
template.languageobjectrequiredWrapper object.
template.language.codestringrequiredOne of the WhatsApp-supported template language codes (e.g. en, en_US, ar, pt_BR). Invalid codes rejected.
template.componentsarrayoptionalComponent objects that fill the template's variables. Omit for templates with no variables.
components[].typestringrequiredOne of header, body, footer, button.
components[].sub_typestringif buttonRequired when component type = "button"; url or quick_reply. Forbidden otherwise.
components[].indexnumberif buttonRequired when component type = "button": 0-based button position. Forbidden otherwise.
components[].parametersarrayoptionalParameter objects, not deep-validated (Joi.any()), forwarded verbatim to Meta (e.g. { type: "text", text }, { type: "image", image: { link } }, currency, date_time, or for buttons { type: "text" | "payload" }).
Text message
{
  "phoneNumberId": "106540352242922",
  "recipientPhone": "962790000000",
  "type": "text",
  "text": {
    "body": "Hi! Your order has shipped: https://example.com/track/AB123",
    "previewUrl": true
  }
}
Image message
{
  "phoneNumberId": "106540352242922",
  "recipientPhone": "962790000000",
  "type": "image",
  "image": {
    "link": "https://example.com/media/receipt.png",
    "caption": "Your payment receipt"
  }
}
Template (image header + body vars + dynamic URL button)
{
  "phoneNumberId": "106540352242922",
  "recipientPhone": "962790000000",
  "type": "template",
  "template": {
    "name": "order_shipped_v2",
    "language": { "code": "en_US" },
    "components": [
      {
        "type": "header",
        "parameters": [
          { "type": "image", "image": { "link": "https://example.com/media/banner.png" } }
        ]
      },
      {
        "type": "body",
        "parameters": [
          { "type": "text", "text": "Sarah" },
          { "type": "text", "text": "AB123" }
        ]
      },
      {
        "type": "button",
        "sub_type": "url",
        "index": 0,
        "parameters": [
          { "type": "text", "text": "track/AB123" }
        ]
      }
    ]
  }
}

Response

200 OK. Meta's response (contacts[].wa_id, messages[].id) is consumed server-side but not returned to the client — the endpoint responds with an empty data object. For per-message status, use message-history/analytics or webhooks.

Response example
{
  "message": "Message sent successfully",
  "data": {}
}

Errors

StatusWhen
400Validation failed (missing/unknown field, invalid type, wrong payload object for the chosen type, unsupported template language code). errorType: "Validation".
400Authorization header missing entirely ("Authorization header not provided").
400Meta rejected the send. Normalized: error_data.details becomes errorMessage, else "Whatsapp Official: <Meta message>" with errors = [error_user_title, error_user_msg]; raw Meta JSON in errorRaw. Typical causes: unregistered/invalid recipient, template not found/approved, wrong parameter count, media URL unreachable.
401Invalid or expired JWT (errorType: "Raw", "JWT error").
402Meta billing block (code 131042 — payment issue on the WABA).
403Invalid api_key ("Not Found", errors: ["Invalid API Key"]).
404No WhatsApp Official account set up — the account lookup throws before the send, mapped to errorType: "General", errorMessage: "Not found".

Notes

  • Calls the Meta Graph API (POST /<phoneNumberId>/messages).
  • Every Meta call (success or failure) is logged to meta_api_logs.
  • For template messages from a phone number linked to a Unilink inbox, the rendered template text is mirrored into the matching Unilink conversation as a private outgoing note (fire-and-forget; a failure never fails the send).
  • The message itself is not persisted to the application database. No emails are sent.

3 endpoints

Analytics

Read-only queries proxied to the Meta Graph API (GET /{waba-id}?fields=...) for your linked WABA. No writes to your data (each Meta call is logged), and the analytics payload passes through verbatim. All three endpoints validate the body with allowUnknown: false and abortEarly: true. Fields marked "string or array" accept either — a single value is treated as a one-element array.

Sent/delivered analytics

POST/v3/whatsapp-official/analytics/messaging

Returns the number of messages sent and delivered by your WABA between two timestamps, bucketed by granularity. Filters map directly to Meta's analytics field parameters.

Auth — JWT or api_key. Because unknown body fields are rejected, send api_key via the Authorization header (an api_key field in the body would fail validation).

Request

FieldTypeRequiredDescription
startintegerrequiredPositive UNIX timestamp (seconds). Start of the range.
endintegerrequiredPositive UNIX timestamp (seconds). End of the range.
granularitystringrequiredOne of HALF_HOUR, DAY, MONTH.
phone_numbersstring | string[]optionalBusiness phone number(s) to filter by. Omit for all numbers.
product_typesnumber | number[]optional0 (notification messages) or 2 (customer support messages).
country_codesstring | string[]optionalExactly 2-letter country codes (e.g. "JO", "US").
Request example
{
  "start": 1748736000,
  "end": 1751328000,
  "granularity": "DAY",
  "phone_numbers": ["962790000000"],
  "country_codes": ["JO", "SA"]
}

Response

200 OKdata is Meta's analytics object, passed through as returned by the Graph API.

Response example
{
  "message": "Messaging analytics fetched successfully",
  "data": {
    "analytics": {
      "phone_numbers": ["962790000000"],
      "country_codes": ["JO", "SA"],
      "granularity": "DAY",
      "data_points": [
        { "start": 1748736000, "end": 1748822400, "sent": 152, "delivered": 148 },
        { "start": 1748822400, "end": 1748908800, "sent": 98, "delivered": 97 }
      ]
    },
    "id": "102290129340398"
  }
}

Errors

StatusWhen
400Validation failed (missing/invalid field, unknown field). errorType: "Validation".
400Meta Graph API rejected the request (invalid date range, unsupported granularity/filter combo). errorType: "WhatsappOfficialAPI".
400No Authorization header provided (errorType: "General", errorMessage: "Authorization header not provided").
401Invalid or expired JWT (errorType: "Raw", errorMessage: "JWT error").
402Meta billing block on the WABA (Meta error code 131042).
403Invalid api_key (errorType: "General", errorMessage: "Not Found", errors: ["Invalid API Key"]).
404No WhatsApp Business Account linked to the user (errorType: "General", errorMessage: "Not found").

Notes

  • One GET call to the Meta Graph API; the call is logged internally. No user-data writes, no emails.

Conversation & cost analytics

POST/v3/whatsapp-official/analytics/conversation

Returns conversation counts and costs for your WABA (Meta's conversation_analytics field), optionally broken down by dimensions. Shows how many billable conversations occurred and what they cost.

Auth — JWT or api_key. Because unknown body fields are rejected, send api_key via the Authorization header (an api_key field in the body would fail validation).

Request

FieldTypeRequiredDescription
startintegerrequiredPositive UNIX timestamp (seconds).
endintegerrequiredPositive UNIX timestamp (seconds).
granularitystringrequiredOne of HALF_HOUR, DAILY, MONTHLY (note: different values than the messaging endpoint).
phone_numbersstring | string[]optionalBusiness phone number(s) to filter by.
conversation_typesstring | string[]optionalEach of REGULAR, FREE_ENTRY_POINT, FREE_TIER, REFERRAL_CONVERSION, SERVICE, UTILITY, AUTHENTICATION, MARKETING.
conversation_directionsstring | string[]optionalEach of business_initiated, user_initiated.
conversation_categoriesstring | string[]optionalEach of service, utility, authentication, marketing.
country_codesstring | string[]optionalExactly 2-letter country codes.
dimensionsstring | string[]optionalEach of conversation_type, conversation_direction, country, phone. Controls which breakdown fields appear per data point.
Request example
{
  "start": 1748736000,
  "end": 1751328000,
  "granularity": "MONTHLY",
  "conversation_categories": ["marketing", "utility"],
  "dimensions": ["conversation_type", "country"]
}

Response

200 OK — Meta's response, verbatim. Each data point carries a conversation count and a cost array (each entry type = amount_spent or cost_per_conversation, plus numeric value); breakdown fields appear only when the matching dimension was requested.

Response example
{
  "message": "Conversation analytics fetched successfully",
  "data": {
    "conversation_analytics": {
      "data": [
        {
          "granularity": "MONTHLY",
          "data_points": [
            {
              "start": 1748736000,
              "end": 1751328000,
              "conversation": 342,
              "conversation_type": "REGULAR",
              "country": "JO",
              "cost": [ { "type": "amount_spent", "value": 12.47 } ]
            },
            {
              "start": 1748736000,
              "end": 1751328000,
              "conversation": 51,
              "conversation_type": "FREE_TIER",
              "country": "SA",
              "cost": [ { "type": "amount_spent", "value": 0 } ]
            }
          ]
        }
      ]
    },
    "id": "102290129340398"
  }
}

Errors

StatusWhen
400Validation failed (missing/invalid field, unknown field). errorType: "Validation".
400Meta Graph API rejected the request (invalid date range, unsupported granularity/filter combo). errorType: "WhatsappOfficialAPI".
400No Authorization header provided (errorType: "General", errorMessage: "Authorization header not provided").
401Invalid or expired JWT (errorType: "Raw", errorMessage: "JWT error").
402Meta billing block on the WABA (Meta error code 131042).
403Invalid api_key (errorType: "General", errorMessage: "Not Found", errors: ["Invalid API Key"]).
404No WhatsApp Business Account linked to the user (errorType: "General", errorMessage: "Not found").

Notes

  • One GET call to the Meta Graph API; the call is logged internally. No user-data writes, no emails.

Per-template analytics

POST/v3/whatsapp-official/analytics/template

Returns per-template performance (Meta's template_analytics field): sent, delivered, read, button clicks, and cost. If metric_types is omitted, the backend requests all metrics by default.

Auth — JWT or api_key. Because unknown body fields are rejected, send api_key via the Authorization header (an api_key field in the body would fail validation).

Request

FieldTypeRequiredDescription
startintegerrequiredPositive UNIX timestamp (seconds).
endintegerrequiredPositive UNIX timestamp (seconds).
granularitystringrequiredOne of HALF_HOUR, DAILY, MONTHLY.
template_idsstring | string[]optionalMeta template ID(s) to limit results to.
metric_typesstring | string[]optionalEach of sent, delivered, read, clicked, cost. Default: all five.
use_waba_timezonebooleanoptionalWhen true, buckets align to the WABA's timezone instead of UTC.
Request example
{
  "start": 1750723200,
  "end": 1751328000,
  "granularity": "DAILY",
  "template_ids": ["1689234511230485"],
  "metric_types": ["sent", "delivered", "read", "clicked"],
  "use_waba_timezone": true
}

Response

200 OK — Meta's response, verbatim. clicked is an array of button-click breakdowns; cost (when requested) is an array of cost metrics.

Response example
{
  "message": "Template analytics fetched successfully",
  "data": {
    "template_analytics": {
      "data": [
        {
          "granularity": "DAILY",
          "waba_timezone": "Asia/Amman",
          "data_points": [
            {
              "template_id": "1689234511230485",
              "start": 1750723200,
              "end": 1750809600,
              "sent": 500,
              "delivered": 489,
              "read": 401,
              "clicked": [
                { "type": "url_button", "button_content": "View offer", "count": 87 }
              ]
            }
          ]
        }
      ]
    },
    "paging": { "cursors": { "before": "MAZDZD", "after": "MjQZD" } }
  }
}

Errors

StatusWhen
400Validation failed (missing/invalid field, unknown field). errorType: "Validation".
400Meta Graph API rejected the request (invalid date range, unsupported granularity/filter combo). errorType: "WhatsappOfficialAPI".
400No Authorization header provided (errorType: "General", errorMessage: "Authorization header not provided").
401Invalid or expired JWT (errorType: "Raw", errorMessage: "JWT error").
402Meta billing block on the WABA (Meta error code 131042).
403Invalid api_key (errorType: "General", errorMessage: "Not Found", errors: ["Invalid API Key"]).
404No WhatsApp Business Account linked to the user (errorType: "General", errorMessage: "Not found").

Notes

  • One GET call to the Meta Graph API; the call is logged internally. No user-data writes, no emails.

6 endpoints

Manager On-Behalf

Managers (users with the MANAGER role) operate WhatsApp Official features on behalf of accounts they created. Mounted at /v3/managers. Each endpoint behaves like its self-service /v3/whatsapp-official/* equivalent, but resolves the target user's WABA (looked up by :userId) instead of the caller's own.

List a managed user's phone numbers

GET/v3/managers/users/:userId/whatsapp-official/phone-numbers

Lists the phone numbers on the target user's WABA. Manager equivalent of List Phone Numbers; data is fetched live from Meta and returned verbatim.

Auth — JWT or api_key (manager). Authenticate as the manager, then two role checks run: checkManagerRole (403 if not a MANAGER) and checkManagerOwnsUser (403 unless targetUser.createdByManagerId === manager.id).

Path parameters

FieldTypeRequiredDescription
userIdnumberrequiredID of a user created by this manager (path parameter).

Response

200 OK.

Response example
{
  "message": "Phone numbers fetched successfully",
  "data": {
    "phoneNumbers": [
      {
        "id": "106540352242922",
        "cc": "JO",
        "country_dial_code": "962",
        "display_phone_number": "+962 7 9000 0000",
        "verified_name": "Acme Store",
        "status": "CONNECTED",
        "quality_rating": "GREEN",
        "platform_type": "CLOUD_API",
        "code_verification_status": "VERIFIED",
        "messaging_limit_tier": "TIER_1K",
        "country_code": "JO",
        "name_status": "APPROVED"
      }
    ]
  }
}

Errors

StatusWhen
400Missing Authorization header (and no api_key in body), or path/body validation failed (errorType: "Validation").
401Invalid or expired JWT.
403Invalid api_key, caller is not a MANAGER, or the target user was not created by this manager.
404Target user does not exist, or the target user has no WhatsApp Official account.

Notes

  • One read call to Meta (GET /{wabaId}/phone_numbers); no DB writes.

List a managed user's templates

GET/v3/managers/users/:userId/whatsapp-official/message-templates

Lists message templates on the target user's WABA. Manager equivalent of List Message Templates; returns the Meta list verbatim.

Auth — JWT or api_key (manager). Authenticate as the manager, then two role checks run: checkManagerRole (403 if not a MANAGER) and checkManagerOwnsUser (403 unless targetUser.createdByManagerId === manager.id).

Path parameters

FieldTypeRequiredDescription
userIdnumberrequiredID of a user created by this manager (path parameter).

Response

200 OK.

Response example
{
  "message": "Message templates fetched successfully",
  "data": {
    "messageTemplates": [
      {
        "name": "order_confirmation",
        "language": "en_US",
        "status": "APPROVED",
        "category": "UTILITY",
        "last_updated_time": "2026-06-30T10:15:00+0000",
        "components": [
          { "type": "BODY", "text": "Hi {{1}}, your order {{2}} is confirmed." }
        ],
        "id": "1189342119334455"
      }
    ]
  }
}

Errors

StatusWhen
400Missing Authorization header (and no api_key in body), or path/body validation failed (errorType: "Validation").
401Invalid or expired JWT.
403Invalid api_key, caller is not a MANAGER, or the target user was not created by this manager.
404Target user does not exist, or the target user has no WhatsApp Official account.

Notes

  • One read call to Meta (GET /{wabaId}/message_templates); no DB writes.

Create a template for a managed user

POST/v3/managers/users/:userId/whatsapp-official/message-templates

Creates a message template on the target user's WABA. Manager equivalent of Create Message Template — the body uses the same schema (createTemplateSchema); see the self-service section for the full component schemas.

Auth — JWT or api_key (manager). Authenticate as the manager, then two role checks run: checkManagerRole (403 if not a MANAGER) and checkManagerOwnsUser (403 unless targetUser.createdByManagerId === manager.id).

Request

Same schema as the self-service Create Template endpoint.

FieldTypeRequiredDescription
namestringrequiredregex ^[a-z0-9_]+$, max 512 chars.
categorystringrequiredOne of authentication, marketing, utility — all-lowercase or all-uppercase; mixed case rejected.
languagestringrequiredA supported WhatsApp language code (e.g. en_US, ar).
parameter_formatstringoptionalnamed or positional.
componentsarrayconditionalRequired when library_template_name is absent; forbidden when it is present. Item shape depends on category.
library_template_namestringoptionalUsing it requires category to be utility/UTILITY.
library_template_button_inputsarrayoptionalOnly allowed with library_template_name; URL or PHONE_NUMBER button inputs (phone in E.164).
Request example
POST /v3/managers/users/42/whatsapp-official/message-templates
{
  "name": "delivery_update",
  "category": "utility",
  "language": "en_US",
  "components": [
    { "type": "body", "text": "Hi {{1}}, your package will arrive on {{2}}.",
      "example": { "body_text": [["Sara", "Monday"]] } }
  ]
}

Response

200 OKdata.messageTemplate is the Meta create response, verbatim.

Response example
{
  "message": "Message template created successfully",
  "data": {
    "messageTemplate": {
      "id": "1189342119334455",
      "status": "PENDING",
      "category": "UTILITY"
    }
  }
}

Errors

StatusWhen
400Missing Authorization header (and no api_key in body), or path/body validation failed (errorType: "Validation").
401Invalid or expired JWT.
403Invalid api_key, caller is not a MANAGER, or the target user was not created by this manager.
404Target user does not exist, or the target user has no WhatsApp Official account.
400Meta rejected the template (duplicate name, invalid components, etc.).

Notes

  • Creates the template on Meta (POST /{wabaId}/message_templates); no DB writes.

Register a managed user's number

POST/v3/managers/users/:userId/whatsapp-official/register-phone-number/:phoneNumberId

Registers one of the target user's phone numbers with the WhatsApp Cloud API. Manager equivalent of Register Phone Number. Registers with the fixed two-step verification PIN 000000.

Auth — JWT or api_key (manager). Authenticate as the manager, then two role checks run: checkManagerRole (403 if not a MANAGER) and checkManagerOwnsUser (403 unless targetUser.createdByManagerId === manager.id).

Path parameters

FieldTypeRequiredDescription
userIdnumberrequiredID of a user created by this manager (path parameter).
phoneNumberIdstringrequiredMeta phone number ID belonging to the target user's WABA.

Response

200 OKdata.phoneNumber is Meta's register response verbatim.

Response example
{
  "message": "Phone number registered successfully",
  "data": {
    "phoneNumber": { "success": true }
  }
}

Errors

StatusWhen
400Missing Authorization header (and no api_key in body), or path/body validation failed (errorType: "Validation").
401Invalid or expired JWT.
403Invalid api_key, caller is not a MANAGER, or the target user was not created by this manager.
404Target user does not exist, or the target user has no WhatsApp Official account.
400Meta rejected the registration (number not verified, wrong PIN state).

Notes

  • Calls Meta POST /{phoneNumberId}/register; no DB writes.

Subscribe a managed user's WABA

POST/v3/managers/users/:userId/whatsapp-official/subscribe

Subscribes the application to the target user's WABA webhooks. Manager equivalent of Subscribe to Webhooks.

Auth — JWT or api_key (manager). Authenticate as the manager, then two role checks run: checkManagerRole (403 if not a MANAGER) and checkManagerOwnsUser (403 unless targetUser.createdByManagerId === manager.id).

Path parameters

FieldTypeRequiredDescription
userIdnumberrequiredID of a user created by this manager (path parameter).

Response

200 OKdata.subscription is Meta's subscribe response verbatim.

Response example
{
  "message": "Whatsapp official subscribed successfully",
  "data": {
    "subscription": { "success": true }
  }
}

Errors

StatusWhen
400Missing Authorization header (and no api_key in body), or path/body validation failed (errorType: "Validation").
401Invalid or expired JWT.
403Invalid api_key, caller is not a MANAGER, or the target user was not created by this manager.
404Target user does not exist, or the target user has no WhatsApp Official account.
400Meta rejected the subscription.

Notes

  • Calls Meta POST /{wabaId}/subscribed_apps and persists webhookSubscribed = true on the target user's account record.

Send a message as a managed user

POST/v3/managers/users/:userId/whatsapp-official/message

Sends a WhatsApp message from one of the target user's phone numbers. Manager equivalent of Send Message — the body uses the same schema (sendMessageSchema). Exactly one payload object matching type must be present.

Auth — JWT or api_key (manager). Authenticate as the manager, then two role checks run: checkManagerRole (403 if not a MANAGER) and checkManagerOwnsUser (403 unless targetUser.createdByManagerId === manager.id).

Request

Same per-type payload objects as the self-service Send Message endpoint.

FieldTypeRequiredDescription
phoneNumberIdstringrequiredSender phone number ID on the target user's WABA.
recipientPhonestringrequiredRecipient phone number.
typestringrequiredOne of text, document, image, video, template.
textobjectif type=text{ body (required), previewUrl (default false) }.
imageobjectif type=image{ link (required), caption? }.
videoobjectif type=video{ link (required), caption? }.
documentobjectif type=document{ link (required), caption?, filename? }.
templateobjectif type=template{ name (required), language: { code } (required), components? } — see the self-service Send Message component schema.
Request example
POST /v3/managers/users/42/whatsapp-official/message
{
  "phoneNumberId": "106540352242922",
  "recipientPhone": "+962790000000",
  "type": "text",
  "text": { "body": "Hello from Acme support!", "previewUrl": false }
}

Response

200 OK — this endpoint returns no payload data; the controller responds with an empty data object.

Response example
{
  "message": "Message sent successfully",
  "data": {}
}

Errors

StatusWhen
400Missing Authorization header (and no api_key in body), or path/body validation failed (errorType: "Validation").
401Invalid or expired JWT.
403Invalid api_key, caller is not a MANAGER, or the target user was not created by this manager.
404Target user does not exist, or the target user has no WhatsApp Official account.
400Meta rejected the send (unregistered number, recipient not on WhatsApp, template not approved, etc.).

Notes

  • Sends via Meta (POST /{phoneNumberId}/messages). For template messages, if the phone number is linked to a Unilink inbox, a rendered copy is synced to the Unilink conversation (fire-and-forget).

2 endpoints

Inbound Webhook

These endpoints are called by Meta (WhatsApp Cloud API), not by API consumers — you never call them yourself. Every raw Meta event received here is forwarded to the URLs configured on your account (unilinkWebhookUrl and unibotWebhookUrl). Inbound Meta traffic carries X-Hub-Signature-256, forwarded intact so you can re-verify downstream.

Webhook verification handshakeCalled by Meta

GET/v2/webhook/whatsapp/official/handler

Meta webhook verification handshake. When a subscription is configured in the Meta App dashboard, Meta sends this GET once; the platform echoes the challenge to confirm ownership of the callback URL.

Auth — None (validated via the platform's configured verify token, matched against hub.verify_token).

Query parameters

FieldTypeRequiredDescription
hub.modestringrequiredMust equal subscribe.
hub.verify_tokenstringrequiredMust match the platform's configured webhook verification token.
hub.challengestringoptionalRandom string generated by Meta; echoed back verbatim on success (always sent by Meta in practice).

Request

Request
GET /v2/webhook/whatsapp/official/handler?hub.mode=subscribe&hub.verify_token=<verify-token>&hub.challenge=1158201444

Response

200 OK — plain-text body containing the challenge value (no JSON envelope), e.g. 1158201444.

Errors

StatusWhen
403hub.mode is not subscribe, or hub.verify_token does not match (empty body).

Notes

  • No side effects.

Event ingress + forwardingCalled by Meta

POST/v2/webhook/whatsapp/official/handler

Meta event ingress. Receives WhatsApp Cloud API webhook events (inbound messages, delivery/read statuses, template updates), resolves which platform account they belong to, and fans the raw event out to that account's configured forward URLs.

Auth — Called by Meta; requires the X-Hub-Signature-256 header. The header is forwarded intact to your configured URLs so the payload can be re-verified downstream.

Request

Processing / fan-out: (1) parse the JSON body (malformed → 400 Invalid JSON payload); (2) match entry[0].id against a WhatsApp Official account's businessId (no match → 404, nothing forwarded); (3) POST the verbatim raw body to unibotWebhookUrl and unilinkWebhookUrl (each only if set), forwarding original headers minus host, connection, content-length; (4) both POSTs run in parallel and are awaited via Promise.allSettled before responding — a forward failure is only logged and never changes the response to Meta. No retries at this layer.

Raw Meta webhook payload

Captured as a raw buffer before JSON parsing so the forwarded copy is byte-for-byte identical to what Meta sent. No Joi schema; the handler requires only the fields below.

FieldTypeRequiredDescription
objectstringoptionalTypically whatsapp_business_account (not checked).
entryarrayrequiredMust be a non-empty array.
entry[0].idstringrequiredWABA ID — used to resolve the platform account (businessId).
entry[].changes[]arrayoptionalStandard Meta change objects (field + value) — passed through untouched.
Inbound text message event from Meta
{
  "object": "whatsapp_business_account",
  "entry": [
    {
      "id": "102290129340398",
      "changes": [
        {
          "field": "messages",
          "value": {
            "messaging_product": "whatsapp",
            "metadata": { "display_phone_number": "15550123456", "phone_number_id": "106540352242922" },
            "contacts": [ { "profile": { "name": "Kerry Fisher" }, "wa_id": "962790000000" } ],
            "messages": [
              {
                "from": "962790000000",
                "id": "wamid.HBgLOTYyNzkwMDAwMDAwFQIAEhgUM0E5...",
                "timestamp": "1752192000",
                "type": "text",
                "text": { "body": "Hello!" }
              }
            ]
          }
        }
      ]
    }
  ]
}
Status update event (delivery lifecycle)
{
  "object": "whatsapp_business_account",
  "entry": [
    {
      "id": "102290129340398",
      "changes": [
        {
          "field": "messages",
          "value": {
            "messaging_product": "whatsapp",
            "metadata": { "display_phone_number": "15550123456", "phone_number_id": "106540352242922" },
            "statuses": [
              {
                "id": "wamid.HBgLOTYyNzkwMDAwMDAwFQIAEhgUM0E5...",
                "status": "delivered",
                "timestamp": "1752192042",
                "recipient_id": "962790000000",
                "conversation": { "id": "0aa1c9...", "origin": { "type": "utility" } },
                "pricing": { "billable": true, "pricing_model": "PMP", "category": "utility" }
              }
            ]
          }
        }
      ]
    }
  ]
}

Response

200 OK — plain JSON (not the v3 { message, data } envelope): { "message": "Webhook received successfully" }.

Errors

StatusWhen
400Body is not valid JSON (Invalid JSON payload).
400Missing X-Hub-Signature-256 header (Missing signature header).
400Raw body missing (Missing request body).
400entry missing, not an array, or empty (Invalid payload structure).
400entry[0].id missing (Missing business ID in payload).
404No WhatsApp Official account with a matching businessId (Account not found) — the event is dropped, nothing forwarded.
500Unexpected processing error (Internal server error).

Notes

  • Reads the WhatsApp Official account (no writes); issues up to two outbound POSTs (to unibotWebhookUrl and unilinkWebhookUrl). No Meta Graph calls, no emails.
  • Your forward URL receives the untouched Meta payload. messages[].type may be text, image, video, audio, document, sticker, location, contacts, interactive, button, or reaction.
  • statuses[].status progresses sentdeliveredread, or failed (with an errors[] array). Respond with any 2xx quickly; the platform does not retry failed forwards.

Questions about this reference? Contact hello@unirsal.com.