Appearance
WhatsApp integration
WhatsApp lets your app message customers through the WhatsApp Business Cloud API: send texts, media, approved templates, interactive buttons and lists, locations and contact cards, manage message templates, and react to inbound messages and delivery statuses via webhook triggers.
Use cases
- Send order updates, alerts and reminders with approved message templates
- Build a support bot that replies to inbound customer messages
- Send interactive reply buttons or list menus and react to the customer's pick
- Deliver invoices, images, locations and contact cards from backend workflows
- Track whether messages were sent, delivered, read — or failed
Setup
- Go to Meta for Developers, click Create app and pick the Connect with customers through WhatsApp use case (a Meta Business portfolio is required).
- In the App Dashboard open
WhatsApp → API Setupand copy the Phone Number ID and the WhatsApp Business Account ID. - Create a permanent token: in Business Settings open
Users → System usersand create an Admin system user. Click Assign assets and grant your app with Manage app plus your WhatsApp account, then Generate token with thewhatsapp_business_messaging,whatsapp_business_managementandbusiness_managementpermissions. - In WeWeb, open the
Data & APItab, then theIntegrationssubtab. SelectWhatsApp, then clickAdd Connection. - Paste the Access Token, Phone Number ID and WhatsApp Business Account ID. The connection applies to all environments (
Editor,Staging,Production); to use different values per environment, override the generated environment variables. - Test with List Templates — if configured, it returns the account's message templates (a fresh WhatsApp Business test setup comes with the pre-approved
hello_worldtemplate).
Enabling triggers (webhook)
The two triggers (see below) require the connection's webhook:
- On the connection, set the App Secret — from
App settings → Basicin the App Dashboard. It is a 32-character hex string used only to verify webhook signatures; it is not the access token. - Set
WhatsApp WebhooktoEnabled, keep (or adjust) theWebhook Path(default/whatsapp/webhook), and set a Verify Token (use theGeneratebutton). - Save the connection and publish your project — the webhook route only exists after publishing.
- In your Meta app open
WhatsApp → Configurationand register the Callback URL:https://<your-app-domain>/api/<webhookPath>— use the connection'sCopy <env> urlbutton. The/apiprefix is mandatory, and there must be no trailing slash. Enter the same Verify Token and clickVerify and save. - After verification succeeds, subscribe the webhook to the
messagesfield (Webhook fieldssection). Without that subscription the handshake succeeds but no events are ever delivered.
ONE WEBHOOK FIELD, TWO TRIGGERS
Both triggers arrive through the single messages webhook field: inbound customer messages fire On message received, and delivery-status changes of your outbound messages fire On message status updated.
Triggers
With the webhook enabled, verified and subscribed, two backend triggers become available:
On message received
Runs each time a customer sends a message to your WhatsApp business number — texts, media, and taps on your interactive buttons or list rows all arrive here. Reply with Send Text bound to event.from, or mark it read with Mark as Read bound to event.messageId. event.text is only set for text messages; other types carry their details in event.raw.
Example event
json
{
"messageId": "wamid.HBgLMTU1NTAxMjM0NTYVAgASGBQzQTBCM0Y3RjA4RjY4N0VCMkE5RQA=",
"from": "15550123456",
"senderName": "Jane",
"timestamp": "1724668800",
"type": "text",
"text": "Hello",
"phoneNumberId": "123456789012345",
"raw": {
"from": "15550123456",
"id": "wamid.HBgLMTU1NTAxMjM0NTYVAgASGBQzQTBCM0Y3RjA4RjY4N0VCMkE5RQA=",
"timestamp": "1724668800",
"text": { "body": "Hello" },
"type": "text"
}
}On message status updated
Runs on each delivery-status change of a message you sent: sent, delivered, read or failed. Check event.status and event.errors to log or retry failures (event.errors is only present on a failed status) — this is also where asynchronous media failures surface (see pitfalls).
Example event
json
{
"messageId": "wamid.HBgLMTU1NTAxMjM0NTYVAgARGBI5QTNDQTVCM0Q0Q0Q2RTY3RTcA",
"status": "delivered",
"timestamp": "1724668805",
"recipientId": "15550123456",
"errors": [],
"phoneNumberId": "123456789012345",
"raw": {
"id": "wamid.HBgLMTU1NTAxMjM0NTYVAgARGBI5QTNDQTVCM0Q0Q0Q2RTY3RTcA",
"status": "delivered",
"timestamp": "1724668805",
"recipient_id": "15550123456"
}
}Using WhatsApp as a data source for tables
You can create WeWeb tables backed by the account's message templates — a single resource, so no table configuration is required. Each row is one template (one language variant) with the columns id, name, category, language, status, body_text (the flattened text of the template's BODY component) and components (the raw component objects).
Views can filter by Status (APPROVED, PENDING, REJECTED, PAUSED, DISABLED), Category (UTILITY, MARKETING, AUTHENTICATION), Language (locale code, e.g. en_US) and Name or content (text search). Pagination is cursor-based: default 25 rows per page, maximum 100; there is no server-side sort.
Common pitfalls (setup & usage)
Free-form messages fail outside the 24-hour window
Free-form sends (Send Text, Send Media, Send Interactive, Send Location, Send Contacts, Send Reaction) only reach customers who messaged you within the last 24 hours. Outside that window Meta rejects with error 131047 — use Send Template with an APPROVED template to open or reopen the conversation.
New templates cannot be sent until approved
Create Template returns status: "PENDING" — the template goes through Meta review (minutes to 24h+) and cannot be sent until it is APPROVED. The Send Template dropdown only lists approved templates; check the review outcome with List Templates or the templates table.
Body variable count must match exactly
Send Template's Body variables must contain exactly one value per {{n}} placeholder in the template body, in order — Meta rejects a mismatch with error 132000. Header variable is only accepted when the template has a variable TEXT header.
Mark as Read needs an inbound message ID
Mark as Read only works on messages the customer sent you — bind event.messageId from the On message received trigger. Passing an outbound wamid (from a send result) fails with Meta error #100 ("message is outgoing"). In a trigger workflow, a failing action halts the chain: put your reply before Mark as Read, or wrap it in a try/catch block.
Delete Template removes every language variant
Delete Template with only a Name deletes all language variants of that template name. Pass the Template ID (from List Templates) to delete a single variant.
Media URLs are validated asynchronously
Send Media returns a message ID even when the URL doesn't point at a valid media file — Meta downloads and validates the URL after accepting the send. Don't trust the synchronous result for media: watch On message status updated for a failed status.
Test numbers only reach verified recipients
Meta's free test business number can only message up to 5 verified recipient numbers. Sending to any other number fails with error #131030 ("Recipient phone number not in allowed list") — add recipients in WhatsApp → API Setup, or connect a real business number.
Trailing slash breaks the webhook handshake
The callback URL must be exactly https://<your-app-domain>/api/<webhookPath> — a trailing slash makes Meta's verification GET miss the route (404), and omitting the /api prefix does the same. Use the Copy <env> url button on the connection instead of typing the URL.
App Secret is not the access token
Pasting the access token into the App Secret field breaks webhook deliveries silently: the handshake and all actions still work (the secret is only used to verify delivery signatures), but every incoming event is rejected with a 400 and no trigger ever fires. The App Secret is the 32-character hex string from App settings → Basic.
All Actions
This integration provides eleven actions mapped to the WhatsApp Cloud API and the template management API.
| Action | Description |
|---|---|
| Send Text | Send a free-form text message |
| Send Media | Send an image, audio, video, document or sticker by public URL |
| Send Template | Send a pre-approved message template — the only message type allowed outside the 24h window |
| Send Interactive | Send up to 3 reply buttons, or a list menu |
| Send Reaction | React to a message with an emoji, or remove a reaction |
| Send Location | Send a location pin |
| Send Contacts | Send one or more contact cards |
| Mark as Read | Mark an inbound message as read, optionally with a typing indicator |
| Create Template | Create a message template (goes through Meta review) |
| List Templates | List the account's message templates with filters and cursor pagination |
| Delete Template | Delete a message template by name, or a single language variant by ID |
Action details
Send Text
Send a free-form text message to a WhatsApp number. Only works within 24h of the recipient's last inbound message (error 131047 otherwise — use Send Template).
Inputs
| Display Key | Example Input | Description | Restrictions |
|---|---|---|---|
To | "15550123456" | Recipient phone number in international format, digits only. Typically bound from a trigger event (event.from). | Required; recipient must have written to you in the last 24 hours |
Message | "Your order has shipped!" | Text of the message. URLs are allowed. | Required; max 4096 characters |
Preview URLOptional | true | Render a link preview for the first URL found in the message body. | Boolean, default false |
Reply to message IDOptional | "wamid.HBgL..." | Send this message as a contextual reply to a previous message. | A wamid from a send result (messages[0].id) or a trigger event (event.messageId) |
Example output
json
{
"messaging_product": "whatsapp",
"contacts": [{ "input": "15550123456", "wa_id": "15550123456" }],
"messages": [{ "id": "wamid.HBgLMTU1NTAxMjM0NTYVAgARGBI5QTNDQTVCM0Q0Q0Q2RTY3RTcA" }]
}Documentation of API endpoint that powers action: Meta Cloud API docs – Messages (POST /{phone-number-id}/messages)
Send Media
Send a media message (image, audio, video, document or sticker) from a public URL. Same 24h customer-service window as free-form texts. The URL is validated asynchronously — see pitfalls.
Inputs
| Display Key | Example Input | Description | Restrictions |
|---|---|---|---|
To | "15550123456" | Recipient phone number in international format, digits only. | Required; recipient must have written to you in the last 24 hours |
Media type | "image" | Type of media to send. | Required; one of image, audio, video, document, sticker |
Media URL | "https://example.com/image.jpg" | Public HTTPS URL of the media file — WhatsApp downloads it from there. Mind Meta's per-type format and size limits (e.g. images 5MB jpeg/png, stickers webp). | Required |
CaptionOptional | "Here is your invoice" | Text shown under the media. | Only for image, video, document media types |
File nameOptional | "invoice.pdf" | File name shown to the recipient. | Only for document media type |
Reply to message IDOptional | "wamid.HBgL..." | Send this message as a contextual reply to a previous message. | A wamid |
Example output
json
{
"messaging_product": "whatsapp",
"contacts": [{ "input": "15550123456", "wa_id": "15550123456" }],
"messages": [{ "id": "wamid.HBgLMTU1NTAxMjM0NTYVAgARGBI5QTNDQTVCM0Q0Q0Q2RTY3RTcA" }]
}Documentation of API endpoint that powers action: Meta Cloud API docs – Messages (POST /{phone-number-id}/messages)
Send Template
Send a pre-approved message template — the only message type allowed outside the 24h customer-service window.
Inputs
| Display Key | Example Input | Description | Restrictions |
|---|---|---|---|
To | "15550123456" | Recipient phone number in international format, digits only. | Required |
Template | "order_confirmation" | Approved message template to send — pick from the dropdown (refreshable). Manage templates with the template actions or the table view. | Required; the dropdown lists only APPROVED templates |
Language code | "en_US" | Locale of the template — set automatically when picking the template. | Required; must match the template's language exactly |
Body variablesOptional | ["John", "ORD-1234"] | Values for the {{1}}, {{2}}… placeholders of the template body, in order. | Shown once a template is selected; count must exactly match the template's placeholders (Meta error 132000) |
Header variableOptional | "ORD-1234" | Value for the template header's {{1}} placeholder. | Shown once a template is selected; only for templates with a variable TEXT header — leave empty otherwise |
Example output
json
{
"messaging_product": "whatsapp",
"contacts": [{ "input": "15550123456", "wa_id": "15550123456" }],
"messages": [{ "id": "wamid.HBgLMTU1NTAxMjM0NTYVAgARGBI5QTNDQTVCM0Q0Q0Q2RTY3RTcA" }]
}Documentation of API endpoint that powers action: Meta Cloud API docs – Template messages (POST /{phone-number-id}/messages)
Send Interactive
Send an interactive message — up to 3 reply buttons, or a list menu. The customer's tap comes back through the On message received trigger. Same 24h window as free-form texts.
Inputs
| Display Key | Example Input | Description | Restrictions |
|---|---|---|---|
To | "15550123456" | Recipient phone number in international format, digits only. | Required; recipient must have written to you in the last 24 hours |
Interactive type | "Reply buttons" | Reply buttons shows up to 3 tappable buttons; List opens a menu of up to 10 options. | Required; default Reply buttons |
Header textOptional | "Choose an option" | Text shown at the top of the message. | Max 60 characters |
Body text | "What would you like to do?" | Main text of the interactive message. | Required; max 1024 characters |
Footer textOptional | "Reply anytime" | Smaller text shown under the body. | Max 60 characters |
Buttons | [{ "id": "confirm", "title": "Confirm" }] | Reply buttons — each with an ID (returned when tapped, max 256 characters) and a Title (visible label, max 20 characters). | Required for the Reply buttons type; 1–3 buttons |
List button text | "View options" | Label of the button that opens the list. | Required for the List type; max 20 characters |
Sections | [{ "title": "Drinks", "rows": [{ "id": "coffee", "title": "Coffee", "description": "Freshly brewed" }] }] | List sections, each with rows the user can pick. Row ID (max 200 characters) and Title (max 24 characters) are required per row; Description (max 72 characters) and the section Title (max 24 characters, required by Meta with several sections) are optional. | Required for the List type; max 10 rows across all sections |
Example output
json
{
"messaging_product": "whatsapp",
"contacts": [{ "input": "15550123456", "wa_id": "15550123456" }],
"messages": [{ "id": "wamid.HBgLMTU1NTAxMjM0NTYVAgARGBI5QTNDQTVCM0Q0Q0Q2RTY3RTcA" }]
}Documentation of API endpoint that powers action: Meta Cloud API docs – Interactive messages (POST /{phone-number-id}/messages)
Send Reaction
React to a message with an emoji, or remove a previous reaction by sending an empty emoji.
Inputs
| Display Key | Example Input | Description | Restrictions |
|---|---|---|---|
To | "15550123456" | Recipient phone number in international format, digits only. | Required; recipient must have written to you in the last 24 hours |
Message ID | "wamid.HBgL..." | The wamid of the message to react to — from a send action result or an inbound message. | Required; message must be less than 30 days old |
EmojiOptional | "👍" | A single emoji. | Leave empty to remove a previous reaction |
Example output
json
{
"messaging_product": "whatsapp",
"contacts": [{ "input": "15550123456", "wa_id": "15550123456" }],
"messages": [{ "id": "wamid.HBgLMTU1NTAxMjM0NTYVAgARGBI5QTNDQTVCM0Q0Q0Q2RTY3RTcA" }]
}Documentation of API endpoint that powers action: Meta Cloud API docs – Reaction messages (POST /{phone-number-id}/messages)
Send Location
Send a location pin (latitude/longitude with optional name and address). Same 24h customer-service window as free-form texts.
Inputs
| Display Key | Example Input | Description | Restrictions |
|---|---|---|---|
To | "15550123456" | Recipient phone number in international format, digits only. | Required; recipient must have written to you in the last 24 hours |
Latitude | 37.4847 | Latitude in decimal degrees. | Required; number between -90 and 90 |
Longitude | -122.1477 | Longitude in decimal degrees. | Required; number between -180 and 180 |
NameOptional | "Head office" | Name of the location shown on the pin. | String |
AddressOptional | "1 Hacker Way, Menlo Park, CA" | Address shown under the location name. | String |
Example output
json
{
"messaging_product": "whatsapp",
"contacts": [{ "input": "15550123456", "wa_id": "15550123456" }],
"messages": [{ "id": "wamid.HBgLMTU1NTAxMjM0NTYVAgARGBI5QTNDQTVCM0Q0Q0Q2RTY3RTcA" }]
}Documentation of API endpoint that powers action: Meta Cloud API docs – Location messages (POST /{phone-number-id}/messages)
Send Contacts
Send one or more contact cards. Same 24h customer-service window as free-form texts.
Inputs
| Display Key | Example Input | Description | Restrictions |
|---|---|---|---|
To | "15550123456" | Recipient phone number in international format, digits only. | Required; recipient must have written to you in the last 24 hours |
Contacts | [{ "formattedName": "Jane Doe", "phone": "+15550123456" }] | Contact cards to send. Each card has a Formatted name (required), plus optional First name, Last name, Phone, Email and Company. | Required; at least one contact, each with a Formatted name |
Example output
json
{
"messaging_product": "whatsapp",
"contacts": [{ "input": "15550123456", "wa_id": "15550123456" }],
"messages": [{ "id": "wamid.HBgLMTU1NTAxMjM0NTYVAgARGBI5QTNDQTVCM0Q0Q0Q2RTY3RTcA" }]
}Documentation of API endpoint that powers action: Meta Cloud API docs – Contacts messages (POST /{phone-number-id}/messages)
Mark as Read
Mark an inbound message as read (blue ticks), optionally showing a typing indicator.
Inputs
| Display Key | Example Input | Description | Restrictions |
|---|---|---|---|
Message ID | "wamid.HBgL..." | The wamid of the inbound message to mark as read — bind event.messageId from the On message received trigger. | Required; outbound message IDs are rejected by Meta (error #100) |
Show typing indicatorOptional | true | Also display a typing… indicator to the customer (clears after ~25 seconds or when you reply). | Boolean, default false |
Example output
json
{
"success": true
}Documentation of API endpoint that powers action: Meta Cloud API docs – Mark messages as read (POST /{phone-number-id}/messages)
Create Template
Create a message template on the WhatsApp Business account. The template starts in PENDING status and goes through Meta review before it can be sent.
Inputs
| Display Key | Example Input | Description | Restrictions |
|---|---|---|---|
Name | "order_confirmation" | Template name. | Required; lowercase letters, digits and underscores only, max 512 characters |
Category | "UTILITY" | Meta template category — drives per-conversation pricing and review strictness: UTILITY for transactional, MARKETING for promotional, AUTHENTICATION for OTP codes. | Required; one of UTILITY, MARKETING, AUTHENTICATION |
Language | "en_US" | Locale code of the template. | Required; a Meta locale code (e.g. en_US, fr, de) |
Header textOptional | "Order update" | TEXT header shown at the top of the template. | Max 60 characters |
Body text | "Hi 1, your order 2 has shipped!" | Body of the template. Use {{1}}, {{2}}… for variables. | Required; max 1024 characters |
Footer textOptional | "Reply STOP to unsubscribe" | Footer shown under the body. | Max 60 characters |
Body example valuesOptional | ["John", "ORD-1234"] | Example values for the {{n}} body placeholders, in order — used by Meta review. | Required by Meta when the body contains placeholders |
Example output
json
{
"id": "1234567890123456",
"status": "PENDING",
"category": "UTILITY"
}Documentation of API endpoint that powers action: Meta Business Management API docs – Message Templates (POST /{waba-id}/message_templates)
List Templates
List the message templates of the WhatsApp Business account, with optional filters and cursor pagination.
Inputs
| Display Key | Example Input | Description | Restrictions |
|---|---|---|---|
StatusOptional | "APPROVED" | Only return templates with this review status. | One of APPROVED, PENDING, REJECTED, PAUSED, DISABLED |
CategoryOptional | "UTILITY" | Only return templates of this category. | One of UTILITY, MARKETING, AUTHENTICATION |
Name or contentOptional | "order" | Only return templates whose name or content contains this text. | String |
LimitOptional | 25 | Maximum number of templates per page. | 1–100, clamped server-side; when omitted, Meta's Graph default (25) applies |
After cursorOptional | "MjQZD" | Pagination cursor — pass paging.cursors.after from the previous page. | Note: Graph returns cursors.after even on the last page — paging.next being present is the real "has more" signal |
Example output
json
{
"data": [
{
"id": "1234567890123456",
"name": "order_confirmation",
"category": "UTILITY",
"language": "en_US",
"status": "APPROVED",
"components": [
{ "type": "HEADER", "format": "TEXT", "text": "Order update" },
{
"type": "BODY",
"text": "Hi {{1}}, your order {{2}} has shipped!",
"example": { "body_text": [["John", "ORD-1234"]] }
}
]
}
],
"paging": { "cursors": { "before": "MAZDZD", "after": "MjQZD" } }
}Documentation of API endpoint that powers action: Meta Business Management API docs – Message Templates (GET /{waba-id}/message_templates)
Delete Template
Delete a message template by name. Without a Template ID, all language variants of that name are deleted.
Inputs
| Display Key | Example Input | Description | Restrictions |
|---|---|---|---|
Name | "order_confirmation" | Name of the template to delete. | Required; alone it deletes every language variant of this name |
Template IDOptional | "1234567890123456" | Template id (from List Templates) — scopes the deletion to that single language variant. | String |
Example output
json
{
"success": true
}Documentation of API endpoint that powers action: Meta Business Management API docs – Message Templates (DELETE /{waba-id}/message_templates)
Error handling
WeWeb validates the connection and a few arguments before calling Meta; everything else is Meta's own error, passed through verbatim.
WeWeb-generated errors — thrown before any call to Meta reaches the network:
| Error code and type | Reason |
|---|---|
| 400 — connection missing / no access token | The WhatsApp connection is missing or not configured — check the connection selected on the action. |
| 400 — no Phone Number ID configured | The connection has no Phone Number ID (App Dashboard → WhatsApp → API Setup) — check the connection selected on the action. |
| 400 — no Business Account ID configured | Template actions and the templates table need the WhatsApp Business Account ID (WABA ID) — check the connection selected on the action. |
| 400 — invalid media type | Send Media with a Media type outside image, audio, video, document, sticker (only possible via binding). |
| 400 — numeric offset on the templates table | Templates use cursor pagination — a numeric offset > 0 cannot be translated to a cursor; pass the nextOffset from the previous page (or 0 for the first page). |
Meta pass-through errors — returned as { "error": { "message", "type", "code", "error_subcode", "fbtrace_id" } }:
| Error code and type | Reason |
|---|---|
401 OAuthException | Invalid or expired access token. |
400 code 100 | Invalid parameter — e.g. Mark as Read with an outbound message ID ("message is outgoing, use an incoming message ID"), or a malformed field value. |
400 code 131030 | Recipient phone number not in allowed list — test numbers can only message up to 5 verified recipients. |
400 code 131047 | Re-engagement message — a free-form send outside the 24-hour customer-service window; use Send Template. |
400 code 132000 | Template parameter count mismatch — Body variables doesn't match the template's {{n}} placeholders. |
400 code 132001 | Template not found or not approved — wrong name, wrong Language code, or the template is still PENDING/REJECTED. |
FAQs
How do I get a permanent access token?
The token shown in WhatsApp → API Setup is temporary. For a permanent one, create an Admin system user in Business Settings (Users → System users), assign your app (Manage app) and your WhatsApp account to it, then generate a token with the whatsapp_business_messaging, whatsapp_business_management and business_management permissions.
Why do my messages to a new user fail?
Free-form messages only reach customers who wrote to you within the last 24 hours — outside that window Meta rejects with error 131047. Start (or reopen) the conversation with Send Template using an APPROVED template; once the customer replies, the 24-hour window opens and free-form sends work.
How can I test without a real business number?
Meta gives every WhatsApp app a free test business number (WhatsApp → API Setup) with the pre-approved hello_world template. It can only message up to 5 verified recipient numbers — add yours in the API Setup page; other recipients fail with error #131030.
Why don't my triggers fire?
Check, in order: the webhook is Enabled on the connection with App Secret and Verify Token set; the project has been published (the route only exists after publishing); the callback URL registered in Meta is exactly https://<your-app-domain>/api/<webhookPath> (the /api prefix is mandatory, no trailing slash); the webhook is subscribed to the messages field; and the App Secret is the 32-character hex string from App settings → Basic — pasting the access token there makes Meta's deliveries fail silently with 400.
Can I upload a media file directly from a workflow?
No — Send Media sends by public HTTPS URL only: WhatsApp downloads the file from there. Host the file somewhere publicly accessible (e.g. WeWeb storage) and pass its URL. Also note the URL is validated asynchronously — a bad URL still returns a message ID, and the failure only shows up on the On message status updated trigger.
How do I know a message was actually delivered?
A successful send only means Meta accepted the message. Enable the webhook and use the On message status updated trigger: it fires with event.status = sent, delivered, read or failed (with details in event.errors).

