WhatsApp HIL Channel
WhatsApp Business Platform supports interactive messages with reply buttons and list
menus. The 256-character button payload limit requires a correlation table strategy
rather than embedding the full ExecutionResId directly.
WhatsApp Interactive Message Types
| Type | Max buttons | Best for |
|---|---|---|
Reply Buttons (button) | 3 buttons | Approve / Reject / Request Changes |
List Message (list) | 10 rows across sections | Multi-option selections, category choices |
| Template Message | 3 quick-reply buttons | First contact — required for outbound messages to new users |
Correlation Key Strategy: Short Token Table
WhatsApp button payloads are limited to 256 characters, but a GUID + any prefix
already uses 36+ characters. The safe approach is a correlation table
mapping a short opaque token to the ExecutionResId.
// HilCorrelation table (created by your Social ExecutionNodes project)
// Columns: Token (varchar 16), ExecutionResId (uniqueidentifier), ExpiresAt, TenantId
// Generate a short token
string token = GenerateShortToken(); // e.g. "A7xQ2mNp" (8 chars)
// Store it
await _hilCorrelationStore.StoreAsync(token, resId, expiresAt: DateTime.UtcNow.AddDays(3));
// Embed token in button payload (well within 256-char limit)
"payload": "hil:A7xQ2mNp:approve"
"payload": "hil:A7xQ2mNp:reject"
Building the Interactive Message
private object BuildWhatsAppMessage(
IReadOnlyList<ResolvedHilField> fields,
string token,
string recipientPhone)
{
var bodyLines = new List<string> { "*Approval Required*\n" };
foreach (var field in fields)
{
if (field.DisplayMode == HilDisplayMode.Concealed) continue;
string val = field.DisplayMode == HilDisplayMode.ReadableMasked
? "••••••••"
: field.CurrentValue?.ToString() ?? "—";
string editMarker = field.InputMode != HilInputMode.Locked ? " ✏️" : "";
bodyLines.Add($"*{field.Label}:*{editMarker} {val}");
if (!string.IsNullOrEmpty(field.Description))
bodyLines.Add($"_{field.Description}_");
bodyLines.Add("");
}
return new {
messaging_product = "whatsapp",
recipient_type = "individual",
to = recipientPhone,
type = "interactive",
interactive = new {
type = "button",
body = new { text = string.Join("\n", bodyLines) },
action = new {
buttons = new[] {
new { type = "reply", reply = new { id = $"hil:{token}:approve", title = "Approve ✅" } },
new { type = "reply", reply = new { id = $"hil:{token}:reject", title = "Reject ❌" } }
}
}
}
};
}
Sending the Message
// In OnProcess:
string token = GenerateShortToken();
await _correlationStore.StoreAsync(token, resId, expiresAt: DateTime.UtcNow.AddDays(3));
var message = BuildWhatsAppMessage(hilFields, token, settings.RecipientPhone);
await _whatsAppClient.SendMessageAsync(message, cancellationToken);
return NodeExecutionResult.Suspend("waiting");
Webhook Handler
WhatsApp sends events to your registered webhook URL. Button reply events arrive with
type = "interactive" and the reply payload in button_reply.id.
[HttpPost("webhooks/whatsapp")]
public async Task<IActionResult> HandleWhatsApp([FromBody] WhatsAppWebhookPayload payload)
{
// 1. Verify webhook token (GET challenge) or signature (POST events)
if (!_whatsAppVerifier.Verify(Request, payload))
return Unauthorized();
foreach (var entry in payload.Entry)
foreach (var change in entry.Changes)
foreach (var message in change.Value.Messages ?? [])
{
if (message.Type != "interactive") continue;
var buttonId = message.Interactive.ButtonReply?.Id;
if (buttonId == null || !buttonId.StartsWith("hil:")) continue;
// Parse "hil:{token}:{action}"
var parts = buttonId.Split(':');
var token = parts[1];
var action = parts[2]; // "approve" or "reject"
// Look up the ExecutionResId
var resId = await _correlationStore.ResolveAsync(token);
if (resId == null) continue; // expired or not found
var responseData = new Dictionary<string, object>
{
["approved"] = action == "approve",
["respondedBy"] = message.From,
["respondedAt"] = DateTime.UtcNow
};
_ = Task.Run(() => _continuationOrchestrator.ContinueAsync(resId, responseData));
}
return Ok();
}
Handling Editable Fields via Text Reply
WhatsApp has no inline text input on interactive messages. For fields requiring human-provided
values (RequiredFromHuman), use a two-step flow:
- Send the interactive message with an "Edit Values" button.
- When the human taps "Edit Values", send a follow-up text message prompting for each field in sequence.
- Track the conversation state in a session table (keyed by phone number + token).
- When all fields are collected, call
ContinueAsync.
// Session state table
// Columns: Phone, Token, PendingFieldKey, CollectedValues (JSON), CreatedAt
Required Configuration
| Config Field | Value |
|---|---|
PhoneNumberId | WhatsApp Business phone number ID (from Meta) |
AccessToken | @{secret:WhatsAppAccessToken} |
WebhookVerifyToken | @{secret:WhatsAppWebhookVerifyToken} |
RecipientPhone | Runtime field — @{input:phoneNumber} |