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
Surface
Base path
Consumers
Self-service API
/v3/whatsapp-official
Account holders (JWT or account api_key)
Manager on-behalf
/v3/managers/users/:userId/whatsapp-official
Managers acting for users they created (manager JWT or api_key + ownership check)
Webhook handler
/v2/webhook/whatsapp/official/handler
Called by Meta (WhatsApp Cloud API) — verification handshake + event ingress, fanned out to your configured forward URLs
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:
Source
Behavior
body.api_key
If 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: Bearer
A 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 400Authorization header not provided. An unknown API key fails with 403; an invalid/expired JWT fails with 401.
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.
All errors — auth, validation, business logic, and upstream Meta failures — share one JSON shape. Unexpected failures return 500 with errorType: "Internal".
Error category — Validation, General, WhatsappOfficialAPI, Billable, Internal, Raw, Unauthorized.
errorMessage
Human-readable summary.
errors
Array of extra detail strings, or null.
errorRaw
Raw underlying error when available (for Meta errors: the Meta error response as a JSON string), or null.
errorsValidation
Usually 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:
Aspect
Behavior
400
Almost always 400 with errorType: "WhatsappOfficialAPI".
402
Special 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.
errorMessage
Meta's error.error_data.details verbatim when present; otherwise Whatsapp Official: <Meta error.message>.
errors
Meta's error_user_title and error_user_msg on the generic/billing branch; empty [] when the error_data.details message was used.
errorRaw
The 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:
1Connect a WhatsApp Business Account via Meta Embedded Signup and complete the setup endpoint so the backend stores your account link.
2List the phone numbers attached to the account and pick a phone_number_id.
3Register that phone number for Cloud API messaging.
4Create a message template and wait for Meta to approve it (status moves from PENDING to APPROVED — poll the template endpoints).
5Start sending messages — template messages to open new conversations, free-form messages inside the 24-hour customer service window.
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, exceptPOST /v3/whatsapp-official/setup-link, which is deliberately unauthenticated (linkage-checked instead).
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
Field
Type
Description
id
integer
Internal account row id.
businessId
string
The connected WABA ID.
ownerBusinessId
string
ID of the Meta business that owns the WABA.
ownerBusinessName
string
Display name of the owning Meta business.
webhookSubscribed
boolean
Whether the WABA is currently subscribed to the app's webhooks.
unilinkWebhookUrl
string | null
Forward URL for inbound events (Unilink), or null.
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
Field
Type
Required
Description
authCode
string
required
One-time authorization code from Meta Embedded Signup.
Validation failed (missing/unknown fields), or Meta rejected the code exchange / Graph call (mapped to a WhatsappOfficialAPI error).
401
Missing or invalid credentials.
402
Meta billing-block error codes (e.g. 131042, payment issue on the WABA).
404
Authenticated 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.
Same Embedded Signup code-exchange as /setup, but for the manager-invite flow: a manager sends a setup link to a user they created, and this endpoint links the resulting WABA to that user rather than to the caller.
Auth — None (unguarded route). Authorization is enforced in code via a linkage check: the target userId must have been created by invitedById (user.createdByManagerId === invitedById), otherwise 403.
Request
Field
Type
Required
Description
authCode
string
required
One-time authorization code from Meta Embedded Signup.
businessId
string
required
The WABA ID selected during Embedded Signup.
invitedById
string
required
ID of the manager who issued the setup link.
userId
string
required
ID of the user (created by that manager) to attach the WABA to.
Filter 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 OK — data 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.
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
Field
Type
Required
Description
phoneNumberId
string
required
Meta 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 OK — data.phoneNumber is Meta's registration response passed through verbatim (typically { "success": true }).
(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 OK — data.subscription is Meta's response passed through verbatim (typically { "success": true }).
Meta Graph API error (e.g. expired/invalid stored access token).
401
Missing or invalid credentials.
404
No 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 400WhatsappOfficialAPI (or 402 for billing-block code 131042).
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.
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.
Field
Type
Required
Description
phone_number
string
required
The phone number without country code, e.g. "6315551234" (enforced by Meta).
verified_name
string
required
Display name to verify for the number (enforced by Meta).
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
Status
When
400
Auth failed — missing Authorization header (also 401 for invalid/expired JWT, 403 for unknown api_key).
404
No WhatsApp Official account is set up for this user.
400
Meta rejected the number (already registered elsewhere, invalid format, missing field, etc.).
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
Field
Type
Required
Description
phoneNumberId
string
required
Meta phone number ID (path parameter).
Response
200 OK"Phone number fetched successfully". The Meta object is passed through verbatim.
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
Field
Type
Required
Description
phoneNumberId
string
required
Meta phone number ID (path parameter).
Request
No request body.
Response
200 OK"Phone number registered successfully". Meta's response is passed through verbatim.
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.
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
Field
Type
Required
Description
phoneNumberId
string
required
Meta phone number ID (path parameter).
Request
No Joi validation on this route; codeMethod is forwarded to Meta, which rejects invalid values.
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
Field
Type
Required
Description
phoneNumberId
string
required
Meta phone number ID (path parameter).
Request
No Joi validation on this route.
Field
Type
Required
Description
code
string
required
The 6-digit code received by SMS/voice (enforced by Meta).
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
Field
Type
Required
Description
phoneNumberId
string
required
Meta 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).
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.
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
Field
Type
Required
Description
phoneNumberId
string
required
Meta phone number ID (path parameter).
Request
No request body.
Response
200 OK"Contact sync started successfully". Meta's response is passed through verbatim.
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
Field
Type
Required
Description
phoneNumberId
string
required
Meta 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
Status
When
400
Auth failed — missing Authorization header (also 401 for invalid/expired JWT, 403 for unknown api_key).
404
No WhatsApp Official account is set up for this user.
400
Meta 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.
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
Field
Type
Required
Description
phoneNumberId
string
required
Meta phone number ID (path parameter).
Query parameters
Field
Type
Required
Description
message_id
string
optional
Filter by a specific WhatsApp message ID (WAMID).
fields
string
optional
Comma-separated field selector forwarded to Meta, e.g. id,message_id,events.
limit
integer
optional
Min 1, max 100. Default 25.
after
string
optional
Cursor for the next page.
before
string
optional
Cursor 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.
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
Field
Type
Required
Description
phoneNumberId
string
required
Meta 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.
Auth failed — missing Authorization header (also 401 for invalid/expired JWT, 403 for unknown api_key).
404
No WhatsApp Official account is set up for this user.
400
Meta 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).
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.
No Authorization header provided (errorMessage: "Authorization header not provided").
401
Invalid or expired JWT.
403
Invalid api_key.
404
No WhatsApp Official account is connected for this user (errorType: "General", errorMessage: "Not found").
400
Meta 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.
402
Meta billing block on the WABA (missing/expired payment method).
No Authorization header provided (errorMessage: "Authorization header not provided").
401
Invalid or expired JWT.
403
Invalid api_key.
404
No WhatsApp Official account is connected for this user (errorType: "General", errorMessage: "Not found").
400
Meta 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.
402
Meta billing block on the WABA (missing/expired payment method).
400
The ID does not exist or is not accessible by your WABA (Meta error).
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
Field
Type
Required
Description
name
string
required
Lowercase alphanumeric and underscores only (^[a-z0-9_]+$), max 512 chars.
category
string
required
One of authentication, marketing, utility (uppercase variants accepted). Must be utility/UTILITY when library_template_name is used.
language
string
required
A supported WhatsApp language code (e.g. en_US, ar, es_MX).
parameter_format
string
optional
named or positional.
components
array
conditional
Required for custom templates; forbidden when library_template_name is set. Item shape depends on category (see below).
library_template_name
string
optional
Name of a Meta Template Library template to instantiate.
library_template_button_inputs
array
optional
Only 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.
Field
Type
Required
Description
type
string
required
header, body, footer, or buttons.
format
string
optional
Only on header: text, image, video, or document.
text
string
optional
Component text; use {{1}}, {{2}}, … for variables.
Validation 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".
400
Meta rejected the template (duplicate name for the same language, malformed components).
402
See common errors.
404
See common errors.
Notes
Creates the template on Meta (subject to Meta's review). Nothing is stored in the local database.
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
Field
Type
Required
Description
messageTemplateId
string
required
Meta template ID (sent as hsm_id).
Request
Field
Type
Required
Description
name
string
recommended
The template's name; passed to Meta alongside the ID. No Joi validation on this route.
No Authorization header provided (errorMessage: "Authorization header not provided").
401
Invalid or expired JWT.
403
Invalid api_key.
404
No WhatsApp Official account is connected for this user (errorType: "General", errorMessage: "Not found").
400
Meta 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.
402
Meta billing block on the WABA (missing/expired payment method).
400
The ID/name pair does not match an existing template (Meta error).
Notes
Deleting by name removes all language versions of that name.
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.
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
Field
Type
Required
Description
category
string
required
AUTHENTICATION, MARKETING, or UTILITY (uppercase only).
language
string
required
A supported WhatsApp language code (e.g. en_US).
limit
integer
optional
Positive, max 250. Schema declares default 50, but it is not applied — when omitted, no limit is sent and Meta's own default applies.
search
string
optional
Accepted by validation but not currently forwarded to Meta.
industry
string
optional
One of E_COMMERCE, FINANCIAL_SERVICES, TELECOMMUNICATION.
topic
string
optional
One of ACCOUNT_UPDATE, CUSTOMER_FEEDBACK, ORDER_MANAGEMENT, PAYMENTS.
usecase
string
optional
A 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 OK — data.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
Status
When
400
Validation failed — missing/invalid category or language, limit out of range, invalid industry/topic/usecase code, or any unknown query param.
400
No Authorization header provided (errorMessage: "Authorization header not provided").
401
Invalid or expired JWT.
403
Invalid api_key.
404
No WhatsApp Official account is connected for this user (errorType: "General", errorMessage: "Not found").
400
Meta 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.
402
Meta billing block on the WABA (missing/expired payment method).
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.
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.
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.
Field
Type
Required
Description
phoneNumberId
string
required
The Meta phone number ID (numeric ID of a phone number on your WABA, e.g. "106540352242922"), not the phone number itself.
recipientPhone
string
required
Destination phone number. Meta expects international format (E.164, with or without +), e.g. "962790000000".
type
string
required
One of text, document, image, video, template.
text
object
if type=text
Required when type = "text"; forbidden otherwise.
image
object
if type=image
Required when type = "image"; forbidden otherwise.
video
object
if type=video
Required when type = "video"; forbidden otherwise.
document
object
if type=document
Required when type = "document"; forbidden otherwise.
template
object
if type=template
Required when type = "template"; forbidden otherwise.
text — when type = "text"
Field
Type
Required
Description
text.body
string
required
The message text.
text.previewUrl
boolean
optional
Default false. Whether to render a link preview for URLs in the body.
image / video — when type = "image" | "video"
Field
Type
Required
Description
link
string
required
Publicly reachable URL of the image/video.
caption
string
optional
Caption shown under the media.
document — when type = "document"
Field
Type
Required
Description
document.link
string
required
Publicly reachable URL of the document.
document.caption
string
optional
Caption.
document.filename
string
optional
Filename 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.
Field
Type
Required
Description
template.name
string
required
Name of an approved message template on your WABA.
template.language
object
required
Wrapper object.
template.language.code
string
required
One of the WhatsApp-supported template language codes (e.g. en, en_US, ar, pt_BR). Invalid codes rejected.
template.components
array
optional
Component objects that fill the template's variables. Omit for templates with no variables.
components[].type
string
required
One of header, body, footer, button.
components[].sub_type
string
if button
Required when component type = "button"; url or quick_reply. Forbidden otherwise.
components[].index
number
if button
Required when component type = "button": 0-based button position. Forbidden otherwise.
components[].parameters
array
optional
Parameter 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
}
}
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
Status
When
400
Validation failed (missing/unknown field, invalid type, wrong payload object for the chosen type, unsupported template language code). errorType: "Validation".
400
Authorization header missing entirely ("Authorization header not provided").
400
Meta 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.
401
Invalid or expired JWT (errorType: "Raw", "JWT error").
402
Meta billing block (code 131042 — payment issue on the WABA).
403
Invalid api_key ("Not Found", errors: ["Invalid API Key"]).
404
No 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.
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
Field
Type
Required
Description
start
integer
required
Positive UNIX timestamp (seconds). Start of the range.
end
integer
required
Positive UNIX timestamp (seconds). End of the range.
granularity
string
required
One of HALF_HOUR, DAY, MONTH.
phone_numbers
string | string[]
optional
Business phone number(s) to filter by. Omit for all numbers.
product_types
number | number[]
optional
0 (notification messages) or 2 (customer support messages).
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
Field
Type
Required
Description
start
integer
required
Positive UNIX timestamp (seconds).
end
integer
required
Positive UNIX timestamp (seconds).
granularity
string
required
One of HALF_HOUR, DAILY, MONTHLY (note: different values than the messaging endpoint).
phone_numbers
string | string[]
optional
Business phone number(s) to filter by.
conversation_types
string | string[]
optional
Each of REGULAR, FREE_ENTRY_POINT, FREE_TIER, REFERRAL_CONVERSION, SERVICE, UTILITY, AUTHENTICATION, MARKETING.
conversation_directions
string | string[]
optional
Each of business_initiated, user_initiated.
conversation_categories
string | string[]
optional
Each of service, utility, authentication, marketing.
country_codes
string | string[]
optional
Exactly 2-letter country codes.
dimensions
string | string[]
optional
Each of conversation_type, conversation_direction, country, phone. Controls which breakdown fields appear per data point.
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.
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
Field
Type
Required
Description
start
integer
required
Positive UNIX timestamp (seconds).
end
integer
required
Positive UNIX timestamp (seconds).
granularity
string
required
One of HALF_HOUR, DAILY, MONTHLY.
template_ids
string | string[]
optional
Meta template ID(s) to limit results to.
metric_types
string | string[]
optional
Each of sent, delivered, read, clicked, cost. Default: all five.
use_waba_timezone
boolean
optional
When true, buckets align to the WABA's timezone instead of UTC.
No 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.
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
Field
Type
Required
Description
userId
number
required
ID of a user created by this manager (path parameter).
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
Field
Type
Required
Description
userId
number
required
ID of a user created by this manager (path parameter).
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.
Field
Type
Required
Description
name
string
required
regex ^[a-z0-9_]+$, max 512 chars.
category
string
required
One of authentication, marketing, utility — all-lowercase or all-uppercase; mixed case rejected.
language
string
required
A supported WhatsApp language code (e.g. en_US, ar).
parameter_format
string
optional
named or positional.
components
array
conditional
Required when library_template_name is absent; forbidden when it is present. Item shape depends on category.
library_template_name
string
optional
Using it requires category to be utility/UTILITY.
library_template_button_inputs
array
optional
Only 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 OK — data.messageTemplate is the Meta create response, verbatim.
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
Field
Type
Required
Description
userId
number
required
ID of a user created by this manager (path parameter).
phoneNumberId
string
required
Meta phone number ID belonging to the target user's WABA.
Response
200 OK — data.phoneNumber is Meta's register response verbatim.
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
Field
Type
Required
Description
userId
number
required
ID of a user created by this manager (path parameter).
Response
200 OK — data.subscription is Meta's subscribe response verbatim.
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.
Field
Type
Required
Description
phoneNumberId
string
required
Sender phone number ID on the target user's WABA.
recipientPhone
string
required
Recipient phone number.
type
string
required
One of text, document, image, video, template.
text
object
if type=text
{ body (required), previewUrl (default false) }.
image
object
if type=image
{ link (required), caption? }.
video
object
if type=video
{ link (required), caption? }.
document
object
if type=document
{ link (required), caption?, filename? }.
template
object
if 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
Status
When
400
Missing Authorization header (and no api_key in body), or path/body validation failed (errorType: "Validation").
401
Invalid or expired JWT.
403
Invalid api_key, caller is not a MANAGER, or the target user was not created by this manager.
404
Target user does not exist, or the target user has no WhatsApp Official account.
400
Meta 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.
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
Field
Type
Required
Description
hub.mode
string
required
Must equal subscribe.
hub.verify_token
string
required
Must match the platform's configured webhook verification token.
hub.challenge
string
optional
Random 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
Status
When
403
hub.mode is not subscribe, or hub.verify_token does not match (empty body).
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.
Field
Type
Required
Description
object
string
optional
Typically whatsapp_business_account (not checked).
entry
array
required
Must be a non-empty array.
entry[0].id
string
required
WABA ID — used to resolve the platform account (businessId).
entry[].changes[]
array
optional
Standard Meta change objects (field + value) — passed through untouched.
entry missing, not an array, or empty (Invalid payload structure).
400
entry[0].id missing (Missing business ID in payload).
404
No WhatsApp Official account with a matching businessId (Account not found) — the event is dropped, nothing forwarded.
500
Unexpected 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 sent → delivered → read, or failed (with an errors[] array). Respond with any 2xx quickly; the platform does not retry failed forwards.