WhatsApp Business Cloud ExecModule
Overview
WhatsAppBusinessCloudModule connects ValkyrAI workflows to Meta's hosted WhatsApp Business Cloud API through the native map I/O ExecModule ABI. Workflow Studio discovers it as WhatsAppBusinessCloudModule; the Meta system-user access token resolves only from an encrypted, READY IntegrationAccount.
The initial operation set covers identity, outbound customer messaging, read receipts, reactions, and bounded media lifecycle workflows:
list_phone_numbersandget_phone_numbersend_text,send_template,send_media,send_location, andsend_contactsmark_message_readandreact_to_messageget_media_url,upload_media, and guardeddelete_media
Identity and media-metadata reads can retry transient Meta failures. Message sends, receipts, reactions, uploads, and deletions run once so an ambiguous timeout cannot duplicate a customer-visible or destructive side effect.
Usage
- Create or select a Meta business portfolio, developer app, WhatsApp Business Account, and registered sender phone number.
- Grant the app only the required
whatsapp_business_messagingand, when identity discovery is used,whatsapp_business_managementpermissions. - Create a system-user access token and store it in the encrypted
apiKeyfield of anIntegrationAccount. - Store the numeric WhatsApp Business Account ID in
IntegrationAccount.accountIdand the default sender phone-number ID inIntegrationAccount.username. - Set the account status to
READYand bind it throughExecModuleConfig.authConfig.integrationAccount. - Run
list_phone_numbers, thenget_phone_number, to verify the bound WABA and sender identity before a mutation. - Use an approved template to start a business-initiated conversation; use free-form messages only when Meta's current customer-service-window and policy rules allow them.
- Reconcile provider state before repeating any write after an ambiguous connection failure.
Workflow input cannot supply an access token, WABA ID, or arbitrary provider endpoint. Production routing is fixed to graph.facebook.com/v25.0 over HTTPS. A workflow-supplied phoneNumberId must match the bound IntegrationAccount.username when both are present.
Inputs
| Name | Type | Requirement | Default | Description and constraints |
|---|---|---|---|---|
operation | string | Required | None | One of the 12 documented operations. |
phoneNumberId | numeric string | Sender operations | IntegrationAccount.username | Meta phone-number ID, 1–32 digits; cannot override the bound identity. |
recipientPhoneNumber | string | Outbound message/reaction operations | None | E.164 number with 7–15 digits; common punctuation is removed. |
text | string | send_text | None | 1–4,096 characters. |
previewUrl | boolean | Optional send_text | false | Requests a Meta link preview. |
templateName | string | send_template | None | Approved template name containing letters, digits, or underscores. |
templateLanguage | string | send_template | en_US | Approved two- or three-letter language code with optional region. |
templateComponents | array or JSON string | Optional template | None | 1–20 components, 256 KiB maximum, eight nesting levels. |
mediaType | string | send_media | None | image, audio, video, document, or sticker. |
mediaId | string | Media send/get/delete | None | Opaque Meta media ID, maximum 1,024 characters. |
mediaUrl | HTTPS URL | Alternative media send | None | Public URL, maximum 2,048 characters; mutually exclusive with mediaId. |
caption | string | Optional image/video/document | None | Maximum 1,024 characters. |
fileName | string | Optional document/upload | None | Simple filename, maximum 240 characters; path separators are rejected. |
latitude / longitude | number | send_location | None | Latitude −90–90 and longitude −180–180. |
locationName / locationAddress | string | Optional location | None | Maximum 1,000 characters each. |
contacts | array or JSON string | send_contacts | None | 1–10 bounded contact objects; each requires name.formatted_name. |
messageId | string | Read receipt/reaction | None | Opaque message ID, maximum 1,024 characters. |
reactionEmoji | string | react_to_message | None | At most eight Unicode code points; empty removes the reaction where Meta permits it. |
mediaBytesBase64 | base64 string | upload_media | None | Decodes to 1 byte through 5 MiB. |
mimeType | string | upload_media | None | Image, audio, video, PDF, or plain-text media type. |
limit | integer | Optional phone list | 100 | 1–100 records from one provider page. |
confirmDelete | boolean | delete_media | false | Must be true before the DELETE request is sent. |
Structured contact and template data reject excessive arrays, deep nesting, large text, and credential-like nested fields. Contact top-level fields are limited to addresses, birthday, emails, name, org, phones, and urls.
Outputs
| Name | Type | When present | Description |
|---|---|---|---|
status | string | Always | success or error. |
operation | string | Always | Normalized operation name. |
attempts | integer | Always | Total provider attempts. |
httpStatus | integer | Provider responded | Last Meta HTTP status. |
data | object | Success | Normalized provider response. |
items / count | array / integer | Phone list | Phone identities and returned count. |
phoneNumberId | string | Phone identity read | Verified Meta phone-number ID. |
messageId | string | Message mutation | Created WhatsApp message ID. |
waId | string | Message mutation | Normalized recipient WhatsApp ID returned by Meta. |
mediaId | string | Media upload/read | Uploaded or inspected media ID. |
mediaUrl | string | get_media_url | Short-lived provider media URL; never persist it as a durable public URL. |
mimeType / fileSize | string / integer | Media read | Provider media metadata. |
nextCursor | string | Another phone page exists | Opaque cursor for an explicit later request. |
providerSuccess | boolean | Receipt/delete response | Meta's explicit success flag. |
error | object | Failure | Safe code, redacted message, optional HTTP status, and retryable flag. |
Access tokens never enter outputs. Provider error messages pass through ValkyrAI sensitive-data redaction, and the token-bearing Authorization header is never logged.
IntegrationAccount Requirements
| Setting | Requirement |
|---|---|
| Provider | Meta developer app connected to a WhatsApp Business Account |
apiKey | System-user or approved OAuth access token in an encrypted SecureField |
password | Legacy encrypted access-token fallback only; prefer apiKey |
accountId | Numeric WhatsApp Business Account ID used by list_phone_numbers |
username | Numeric default sender phone-number ID |
status | Must be READY |
| Permissions | whatsapp_business_messaging; add whatsapp_business_management for management/identity reads |
Business verification, app review, system-user provisioning, token rotation, WABA ownership, phone registration, payment configuration, quality monitoring, webhook verification, and template approval belong to the Meta/IntegrationAccount lifecycle. Workflow inputs never handle credentials.
Configuration
{
"version": "1.0.0",
"authConfig": {
"authStrategy": 1,
"integrationAccount": "integration-account:whatsapp-customer-operations"
},
"retryPolicy": {
"maxAttempts": 3,
"backoffStrategy": "EXPONENTIAL",
"initialDelayMs": 1000,
"maxDelayMs": 60000,
"jitter": false
},
"executionConfig": {"timeoutMs": 30000},
"payloadConfig": {
"parameters": "{\"operation\":\"list_phone_numbers\",\"limit\":100}"
}
}
The integration-account value is symbolic. Persisted workflows use the generated relationship rather than plaintext tokens or IDs.
Operations
| Operation | Meta behavior | Side effect |
|---|---|---|
list_phone_numbers | Reads one bounded WABA phone-number page. | Read-only; safe retries. |
get_phone_number | Reads the bound sender identity and quality/status fields. | Read-only; safe retries. |
send_text | Sends one text message with optional link preview. | Customer-visible; single attempt. |
send_template | Sends one approved template and bounded component set. | Customer-visible; single attempt. |
send_media | Sends existing or HTTPS-hosted image/audio/video/document/sticker media. | Customer-visible; single attempt. |
send_location | Sends one coordinate with optional name/address. | Customer-visible; single attempt. |
send_contacts | Sends 1–10 bounded contact cards. | Customer-visible; single attempt. |
mark_message_read | Marks one inbound message read. | Mutates conversation state; single attempt. |
react_to_message | Adds, changes, or removes a reaction. | Customer-visible; single attempt. |
get_media_url | Resolves temporary media metadata and URL. | Read-only; safe retries. |
upload_media | Uploads up to 5 MiB multipart media to the bound sender. | Creates provider media; single attempt. |
delete_media | Deletes one provider media object after explicit confirmation. | Destructive; single attempt. |
Errors and Failure Modes
| Code | Typical cause | Retryable | Resolution |
|---|---|---|---|
VALIDATION_ERROR | Missing/invalid ID, recipient, message, media, MIME type, nested data, or deletion guard. | No | Correct input; no unsafe provider request was sent. |
UNSUPPORTED_OPERATION | Unknown operation. | No | Select a documented operation. |
INTEGRATION_ACCOUNT_REQUIRED | No bound account. | No | Bind a WhatsApp IntegrationAccount. |
INTEGRATION_ACCOUNT_NOT_READY | Account is not READY. | No | Repair, authorize, or replace the account. |
CREDENTIAL_MISSING | Bound account has no decryptable token. | No | Reconnect or rotate the Meta credential. |
WHATSAPP_API_100 | Invalid field or request shape. | Usually no | Correct the redacted provider detail and request. |
WHATSAPP_API_131026 | Recipient/message delivery issue. | Provider-defined | Check recipient eligibility, policy, and delivery webhook. |
WHATSAPP_API_190_* | Expired, revoked, or invalid token. | No | Rotate and update the IntegrationAccount. |
WHATSAPP_API_4, 17, 32, 613, 429, or 5xx | Meta throttling or transient provider failure. | Yes for reads | Honor backoff; reconcile writes before any retry. |
NETWORK_ERROR | Timeout, DNS, TLS, or connectivity failure. | Yes for reads | Verify connectivity and provider state. |
PAYLOAD_TOO_LARGE | JSON request exceeded 256 KiB. | No | Reduce template/contact data. |
RESPONSE_TOO_LARGE | Response exceeded 5 MiB. | No | Narrow the identity page or media read. |
Meta may return an error_subcode; ValkyrAI appends it to the safe error code without exposing fbtrace_id, tokens, or full provider bodies.
Example
Send one approved order update template:
{
"operation": "send_template",
"recipientPhoneNumber": "+1 (415) 555-0123",
"templateName": "order_update",
"templateLanguage": "en_US",
"templateComponents": [
{
"type": "body",
"parameters": [
{"type": "text", "text": "Order 123"},
{"type": "text", "text": "shipped"}
]
}
]
}
Expected result:
{
"status": "success",
"operation": "send_template",
"attempts": 1,
"httpStatus": 200,
"messageId": "wamid.example",
"waId": "14155550123"
}
Notes
- Pagination:
list_phone_numbersreturns one page of at most 100 identities and exposesnextCursor; it does not auto-follow Meta'spaging.nextURL. This keeps each workflow request bounded and prevents token-bearing provider URLs from becoming workflow input. - Rate limits: only
list_phone_numbers,get_phone_number, andget_media_urlretry HTTP 408, 429, 500, 502, 503, and 504 plus transient Graph codes 1, 2, 4, 17, 32, and 613.Retry-Afteris honored. All writes remain single-attempt. - API limits: JSON bodies are capped at 256 KiB, responses at 5 MiB, upload bytes at 5 MiB, text at 4,096 characters, captions at 1,024, filenames at 240, contacts at 10, component arrays at 20, generic nested arrays at 100, and nesting at eight levels. Provider-specific MIME and size limits may be lower or vary by media type.
- Idempotency: WhatsApp message, read-receipt, reaction, upload, and deletion endpoints do not expose a universal caller idempotency key. Each write is sent once. Reconcile with delivery/status webhooks, the returned
wamid, media IDs, or WhatsApp Manager before retrying after an ambiguous failure. - Destructive behavior: only
delete_mediais destructive and requiresconfirmDelete=true. Deleting provider media does not retract messages that already reference it. - Customer consent and policy: the connector does not infer opt-in, permitted purpose, conversation category, customer-service window, template approval, quality tier, or regional/legal requirements. Workflows must provide approved content and honor Meta policy, applicable law, and organizational consent records.
- Security: fixed Graph API routing, bound WABA/sender identities, encrypted token storage, E.164 normalization, HTTPS-only media links, strict MIME/filename checks, bounded structured data, forbidden credential-like nested keys, capped bodies/responses, and redacted Graph errors prevent arbitrary routing and secret reflection.
- Media URLs:
get_media_urlreturns a short-lived Meta URL that still requires authorization. Download bytes promptly through an authorized, separately governed file flow; do not publish or persist the URL as public media. - External verification: request construction, response normalization, read retry, single-attempt writes, identity binding, deletion guards, media bounds, redaction, and metadata discovery have deterministic local tests. Live Meta business verification, token permissions, phone registration, template approval, customer consent, delivery, quality/rate limits, and webhook receipts require separately authorized WhatsApp credentials and are not exercised in repository tests.
- Deferred operations: inbound webhooks/triggers, template CRUD/approval, flows, catalogs/products, interactive list/button messages, groups, calls, payments, business profiles, phone registration, encryption setup, webhook subscription, media-byte download, resumable uploads above 5 MiB, and automated delivery-status reconciliation.
- Functional references: n8n WhatsApp node source, n8n WhatsApp Business Cloud node, and Meta's official WhatsApp Cloud API collection.