WhatsApp Module
Send and receive WhatsApp messages via Meta Cloud API v21.0 or Twilio, with 24-hour messaging window management, template support, and privacy-compliant phone hashing.
Supported Providers
| Provider | API Version | Best For | Pricing Model |
|---|---|---|---|
| Meta Cloud API | v21.0 | Direct integration, full control, custom webhooks | Per-conversation (first 1,000 free/month) |
| Twilio | Messaging API | Quick setup, multi-channel, existing Twilio users | Per-message pricing |
24-Hour Messaging Window
WhatsApp enforces a 24-hour messaging window policy. You can only send free-form text messages to a user within 24 hours of their last inbound message to your business number. After the window closes, you must use pre-approved message templates.
User sends message to your business number
│
▼
┌─────────────────────────────────────────────┐
│ WhatsAppConversationModel updated │
│ last_inbound_at = now() │
│ window_expires_at = now() + 24h │
└─────────────────────────────────────────────┘
│
│ Within 24h: Send any text message (free-form)
│ After 24h: Must use approved template
│
▼
┌─────────────────────────────────────────────┐
│ On send attempt: │
│ if (now() < window_expires_at) │
│ → Send free-form text ✓ │
│ else │
│ → Reject OR fallback to template │
└─────────────────────────────────────────────┘
The WhatsAppConversationModel tracks each conversation:
| Column | Description |
|---|---|
organization_id | Owning organization |
phone_hash | SHA-256 hash of the phone number |
last_inbound_at | Timestamp of last message from user |
window_expires_at | When the 24h window closes |
conversation_id | Meta conversation ID (for billing tracking) |
Configuration: Meta Cloud API
| Field | Type | Required | Description |
|---|---|---|---|
provider | string | Yes | Set to "meta" |
business_account_id | string | Yes | WhatsApp Business Account ID from Meta Business Manager |
access_token | string | Yes | Permanent access token (System User token recommended) |
phone_number_id | string | Yes | Phone Number ID from WhatsApp Manager |
app_secret | string | Yes | App Secret for webhook signature verification |
verify_token | string | Yes | Custom string for webhook URL verification handshake |
{
"provider": "meta",
"business_account_id": "123456789012345",
"access_token": "EAAxxxxxxxxxxxxxxxxxxxxxxxxx",
"phone_number_id": "109876543210987",
"app_secret": "abcdef1234567890abcdef1234567890",
"verify_token": "my-custom-verify-token-2024"
}
Configuration: Twilio
| Field | Type | Required | Description |
|---|---|---|---|
provider | string | Yes | Set to "twilio" |
account_sid | string | Yes | Twilio Account SID (starts with AC) |
auth_token | string | Yes | Twilio Auth Token (stored encrypted) |
from_number | string | Yes | WhatsApp-enabled number in format whatsapp:+1234567890 |
Webhook Verification (Meta)
Meta requires webhook URL verification before it will send events. The process:
- In Meta Developer Dashboard, set your webhook URL to:
https://your-domain.com/api/webhooks/whatsapp - Set the Verify Token to match your module configuration's
verify_tokenvalue - Meta sends a GET request with
hub.mode=subscribe,hub.verify_token, andhub.challenge - Automation Hub validates the token and returns
hub.challengeas the response - Meta confirms the webhook is active and begins sending events
// GET /api/webhooks/whatsapp (verification)
public function verify(Request $request): Response
{
$mode = $request->query('hub_mode');
$token = $request->query('hub_verify_token');
$challenge = $request->query('hub_challenge');
if ($mode === 'subscribe' && $token === $config['verify_token']) {
return response($challenge, 200);
}
return response('Forbidden', 403);
}
// POST /api/webhooks/whatsapp (incoming messages)
// Signature validated via: hash_equals(
// 'sha256=' . hash_hmac('sha256', $rawBody, $appSecret),
// $request->header('X-Hub-Signature-256')
// )
Node Types
action.whatsapp.send
Sends a free-form text message. Requires an open 24-hour window.
| Config Field | Type | Required | Description |
|---|---|---|---|
to | string | Yes | Recipient phone number in E.164 format (e.g., +573001234567) |
message | string | Yes | Text message body (supports variables) |
fallback_template | string | No | Template name to use if window is closed |
action.whatsapp.send_template
Sends an approved message template. Works even outside the 24-hour window.
| Config Field | Type | Required | Description |
|---|---|---|---|
to | string | Yes | Recipient phone number in E.164 format |
template_name | string | Yes | Approved template name (as registered in Meta) |
language | string | Yes | Template language code (e.g., en_US, es) |
variables | array | No | Template variable values in order: ["{{1}}", "{{2}}"] |
{
"type": "action.whatsapp.send_template",
"config": {
"to": "{{trigger.phone}}",
"template_name": "order_confirmation",
"language": "es",
"variables": [
"{{trigger.customer_name}}",
"{{trigger.order_id}}",
"{{trigger.delivery_date}}"
]
}
}
Fallback Template Behavior
When using action.whatsapp.send with a fallback_template configured:
- The module checks if the 24-hour window is open for the recipient
- If open: sends the free-form text message directly
- If closed: automatically sends the specified template instead
- The execution step output indicates which path was taken (
sent_as: "text"orsent_as: "template")
Best Practice
Always configure a fallback template for critical notifications (like order updates). This ensures the message is delivered even if the customer hasn't messaged you recently.
Phone Number Hashing (Privacy)
For compliance with Ley 1581 de 2012 (Colombian data protection law) and GDPR-equivalent regulations, the WhatsApp Module stores phone numbers as SHA-256 hashes in the conversation tracking table.
// Phone numbers are normalized and hashed before storage
$normalized = preg_replace('/[^0-9+]/', '', $phoneNumber);
$hash = hash('sha256', $normalized);
// Stored in whatsapp_conversations.phone_hash
// The plain phone number is NEVER stored in the database
// It only exists transiently during message send/receive
This means:
- Phone numbers are never stored in plain text in the database
- Conversation windows can still be looked up by hashing the incoming number
- If the database is compromised, phone numbers cannot be recovered
- Compliant with data minimization principles
Common Issues
| Problem | Cause | Solution |
|---|---|---|
| Window closed error | Attempting to send free-form text outside 24h window | Use action.whatsapp.send_template or configure a fallback_template |
| Template not approved | Template still pending review or rejected by Meta | Check template status in Meta Business Manager. Use only APPROVED templates. |
| Invalid phone number | Number not in E.164 format or not on WhatsApp | Ensure format is +[country][number] with no spaces. Verify the number has WhatsApp. |
| Rate limited (Meta) | Too many messages sent; tier limit reached | Check your messaging tier in Meta Business Manager. Request tier upgrade if needed. |
| Webhook not receiving events | Verification failed or wrong URL | Verify the verify_token matches. Ensure HTTPS URL is publicly accessible. |
| Signature verification failed | Wrong app_secret configured |
Copy the App Secret from Meta Developer Dashboard → App Settings → Basic. |