Webhook-first architecture: The WhatsApp Cloud API is designed around webhooks for real-time message delivery. The message/getMany operation is intended for historical lookups, audits, and reporting — not for polling inbound messages in real-time. For live inbound message processing, use a WebhookTrigger node configured with your WhatsApp webhook endpoint.
When to Use
- Compliance audit export: A regulatory workflow exports all messages sent to a customer during a specified date range as part of a data access request or compliance audit.
- Campaign delivery reporting: A marketing workflow queries message delivery statuses for a batch of wamids sent during a campaign to calculate delivered, read, and failed rates.
- Failed message reconciliation: A daily scheduled workflow retrieves messages with
status: failed from the previous day and routes them to a retry queue or alert system.
- Conversation reconstruction: A customer service workflow retrieves the message history with a specific phone number to provide context to an agent before they respond.
- SLA reporting: A weekly reporting workflow retrieves all template messages sent in the past 7 days to compute average time-to-delivery and delivery rate for the SLA dashboard.
Configuration
Connection
| Field | Required | Description |
accessToken | Required | Permanent System User access token. Store in BizFirst Credentials Manager. |
phoneNumberId | Required | Numeric string ID of the WhatsApp Business phone number to query. |
apiVersion | Optional | Meta Graph API version. Defaults to v18.0. |
Operation
| Field | Required | Description |
limit | Optional | Maximum number of messages to return per page. Default: 20. Maximum: 100. Use with after cursor for pagination. |
after | Optional | Cursor string from the previous response's paging.cursors.after field. Use to retrieve the next page of results. |
before | Optional | Cursor string from the previous response's paging.cursors.before field. Use to retrieve the previous page. |
statusFilter | Optional | Filter by message status: sent, delivered, read, or failed. Omit to return all statuses. |
since | Optional | Unix timestamp (seconds). Only return messages sent on or after this time. |
until | Optional | Unix timestamp (seconds). Only return messages sent on or before this time. |
Sample Configuration
{
"resource": "message",
"operation": "getMany",
"accessToken": "{{ $credentials.whatsAppToken }}",
"phoneNumberId": "123456789012345",
"limit": "50",
"statusFilter": "failed",
"since": "{{ $input.reportStartTimestamp }}",
"until": "{{ $input.reportEndTimestamp }}"
}
Validation Errors
| Error | Cause |
accessToken is required | The accessToken field is empty. |
phoneNumberId is required | The phoneNumberId field is empty. |
100 (Meta) | Invalid cursor value — the pagination cursor is expired or malformed. Start from the beginning without a cursor. |
803 (Meta) | Phone number not found or not accessible with the provided access token. |
Output
Success Port
| Field | Type | Description |
messages | array | Array of message objects. Each object contains id, type, status, timestamp, to, and from fields. |
count | number | Number of messages in the current page. |
hasNextPage | boolean | true if there are more results beyond this page. |
afterCursor | string | Cursor to pass as after to retrieve the next page. Empty when hasNextPage is false. |
beforeCursor | string | Cursor to pass as before to go back to the previous page. |
rawResponse | string | Full raw JSON from the Meta API including the complete paging object. |
Error Port
| Field | Type | Description |
status | string | "failed" or "error". |
errorCode | string | Meta error code or BizFirst validation code. |
errorMessage | string | Human-readable error description. |
rawResponse | string | Raw API error response. |
Sample Output
{
"messages": [
{
"id": "wamid.HBgLMTQxNTU1NTI2NzEVAgARGBI2QzM4NjhGQTExMjQ3RDMAA",
"type": "text",
"status": "delivered",
"timestamp": "1716700412",
"to": "14155552671",
"from": "123456789012345"
},
{
"id": "wamid.HBgLMTQxNTU1NTI2NzEVAgARGBI3QzM4NjhGQTExMjQ3REMAA",
"type": "template",
"status": "read",
"timestamp": "1716700250",
"to": "14155559999",
"from": "123456789012345"
}
],
"count": 2,
"hasNextPage": true,
"afterCursor": "QVFIUkphYnNENjFiVzJGdkZAbmFfWEJBZAlBZT...",
"beforeCursor": "QVFIUkphYnNENjFiVzJGdkZAbnBfWEJBZAlBZT...",
"rawResponse": "..."
}
Expression Reference
| Value | Expression | Notes |
| All messages array | {{ $output.getMsgs.messages }} | Pass to a Loop node to process each message individually. |
| First message ID | {{ $output.getMsgs.messages[0].id }} | Access individual message fields using array index notation. |
| Total in page | {{ $output.getMsgs.count }} | Use in condition checks before processing the array. |
| Has more results | {{ $output.getMsgs.hasNextPage }} | Use in a Loop condition to drive paginated fetching. |
| Next page cursor | {{ $output.getMsgs.afterCursor }} | Pass as the after parameter in the next call for the next page. |
Node Policies & GuardRails
| Policy Area | Recommendation |
| Credential storage | Store the access token in BizFirst Credentials Manager. Never inline in configuration. |
| Pagination pattern | For large result sets, implement a Loop node that iterates pages using the afterCursor value until hasNextPage is false. Avoid retrieving more data than needed — use since/until and statusFilter to narrow the dataset. |
| Avoid real-time polling | Do not call this operation in a tight polling loop to detect new inbound messages. Use a WebhookTrigger node instead. The Graph API has read rate limits that apply to this operation. |
| Data retention | Meta does not retain message data indefinitely. For long-term compliance records, store messages in your own database via webhooks at the time of delivery. Do not rely on this operation for historical data older than the Meta retention period. |
| Limit size | Keep limit to 50 or fewer in production to reduce response latency and payload size. Use filters to narrow results rather than retrieving large pages and filtering client-side. |
| Error port | Connect the error port. Expired pagination cursors (error 100) require restarting pagination from the beginning rather than retrying with the same cursor. |
Examples
Failed Message Daily Report
{
"resource": "message",
"operation": "getMany",
"accessToken": "{{ $credentials.whatsAppToken }}",
"phoneNumberId": "123456789012345",
"limit": "100",
"statusFilter": "failed",
"since": "{{ $input.yesterdayStartTimestamp }}",
"until": "{{ $input.yesterdayEndTimestamp }}"
}
A ScheduledTrigger fires each morning at 7 AM. A Function node calculates yesterday's start and end Unix timestamps. This node retrieves all failed messages from yesterday. A Loop node iterates the results and stores each in a MongoDB node for the daily failure report. hasNextPage drives a conditional loop for full pagination.
Campaign Delivery Rate Check
{
"resource": "message",
"operation": "getMany",
"accessToken": "{{ $credentials.whatsAppToken }}",
"phoneNumberId": "123456789012345",
"limit": "100",
"since": "{{ $input.campaignStartTimestamp }}",
"until": "{{ $input.campaignEndTimestamp }}",
"after": "{{ $input.paginationCursor }}"
}
A reporting workflow pages through all messages sent during a campaign window. Each page's messages array is counted by status in a Function node (delivered, read, failed). When hasNextPage is false, the totals are written to a GSheets node as the campaign delivery report.