Portal Community

WhatsApp Interactive Message Types

TypeMax buttonsBest for
Reply Buttons (button)3 buttonsApprove / Reject / Request Changes
List Message (list)10 rows across sectionsMulti-option selections, category choices
Template Message3 quick-reply buttonsFirst contact — required for outbound messages to new users
Template requirement for first contact WhatsApp only allows free-form messages within a 24-hour customer service window. If the workflow reaches the HIL node more than 24 hours after the human last messaged your number — or if this is the first message ever — you must use a pre-approved template message. Register a template in the Meta Business Manager for each HIL scenario.

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:

  1. Send the interactive message with an "Edit Values" button.
  2. When the human taps "Edit Values", send a follow-up text message prompting for each field in sequence.
  3. Track the conversation state in a session table (keyed by phone number + token).
  4. When all fields are collected, call ContinueAsync.
// Session state table
// Columns: Phone, Token, PendingFieldKey, CollectedValues (JSON), CreatedAt

Required Configuration

Config FieldValue
PhoneNumberIdWhatsApp Business phone number ID (from Meta)
AccessToken@{secret:WhatsAppAccessToken}
WebhookVerifyToken@{secret:WhatsAppWebhookVerifyToken}
RecipientPhoneRuntime field — @{input:phoneNumber}
Token expiry Always set an expiry on correlation tokens. If a human never responds, the token and the suspended execution should be cleaned up by a background job. Match the token expiry to your workflow's SLA for the approval step.