LINE HIL Channel
LINE dominates messaging in Japan, Thailand, Taiwan, and Indonesia, making it essential for workflows involving users in those markets. LINE Messaging API supports Confirm Templates (two-button yes/no) and Flex Messages (rich card layouts) for HIL delivery. Postback data carries the correlation key.
LINE Message Types for HIL
| Type | Description | Best for HIL |
|---|---|---|
| Confirm Template | Simple two-button message with a text body | Approve/Reject flows — minimal setup |
| Buttons Template | Text + up to 4 action buttons | Multi-option approvals |
| Flex Message | Fully custom card layout (similar to Adaptive Cards) | Rich field display with context + actions |
| Quick Reply | Tap buttons shown above the keyboard | Quick approve/reject after a text message |
Correlation Strategy: Postback Data
LINE Postback action data field supports up to 300 characters.
A prefixed GUID (38 chars) fits easily:
"data": "hil_approve|3fa85f64-5717-4562-b3fc-2c963f66afa6"
"data": "hil_reject|3fa85f64-5717-4562-b3fc-2c963f66afa6"
Building a Confirm Template Message
The Confirm Template is the simplest option for binary approve/reject flows:
private object BuildLineConfirmMessage(
IReadOnlyList<ResolvedHilField> fields,
string resId,
string recipientUserId)
{
var lines = new List<string> { "Approval Required:" };
foreach (var field in fields)
{
if (field.DisplayMode == HilDisplayMode.Concealed) continue;
string val = field.DisplayMode == HilDisplayMode.ReadableMasked
? "****"
: field.CurrentValue?.ToString() ?? "—";
lines.Add($"• {field.Label}: {val}");
}
return new {
to = recipientUserId,
messages = new[] {
new {
type = "template",
altText = "Approval required — please open LINE to respond.",
template = new {
type = "confirm",
text = string.Join("\n", lines),
actions = new object[] {
new {
type = "postback",
label = "Approve ✅",
data = $"hil_approve|{resId}"
},
new {
type = "postback",
label = "Reject ❌",
data = $"hil_reject|{resId}"
}
}
}
}
}
};
}
Building a Flex Message (Rich Layout)
For workflows with many fields, a Flex Message provides a structured card view:
private object BuildLineFlexMessage(
IReadOnlyList<ResolvedHilField> fields,
string resId,
string recipientUserId)
{
var bodyContents = new List<object>();
bodyContents.Add(new {
type = "text",
text = "Approval Required",
weight = "bold",
size = "xl",
color = "#6c8cff"
});
bodyContents.Add(new { type = "separator", margin = "md" });
foreach (var field in fields)
{
if (field.DisplayMode == HilDisplayMode.Concealed) continue;
string val = field.DisplayMode == HilDisplayMode.ReadableMasked
? "••••••••"
: field.CurrentValue?.ToString() ?? "—";
bodyContents.Add(new {
type = "box",
layout = "horizontal",
margin = "sm",
contents = new object[] {
new { type = "text", text = field.Label, color = "#8892b0", size = "sm", flex = 2 },
new { type = "text", text = val, color = "#e2e8f0", size = "sm", flex = 3, wrap = true }
}
});
}
return new {
to = recipientUserId,
messages = new[] {
new {
type = "flex",
altText = "Approval required",
contents = new {
type = "bubble",
body = new { type = "box", layout = "vertical", contents = bodyContents },
footer = new {
type = "box",
layout = "horizontal",
contents = new object[] {
new {
type = "button",
style = "primary",
color = "#34d399",
action = new { type = "postback", label = "Approve", data = $"hil_approve|{resId}" }
},
new {
type = "button",
style = "primary",
color = "#f87171",
action = new { type = "postback", label = "Reject", data = $"hil_reject|{resId}" }
}
}
}
}
}
}
};
}
Sending the Message
// POST to https://api.line.me/v2/bot/message/push
await _httpClient.PostAsJsonAsync(
"https://api.line.me/v2/bot/message/push",
message,
cancellationToken);
// Set Authorization: Bearer {channelAccessToken} header
return NodeExecutionResult.Suspend("waiting");
Webhook Handler
LINE sends a JSON webhook with an events array. Postback interactions
arrive with type = "postback" and postback.data containing
the value you set.
[HttpPost("webhooks/line")]
public async Task<IActionResult> Handle([FromBody] LineWebhookPayload payload)
{
// 1. Validate LINE signature (HMAC-SHA256 over raw body with channel secret)
var rawBody = await Request.ReadBodyAsStringAsync();
var signature = Request.Headers["X-Line-Signature"].ToString();
if (!_lineSignatureVerifier.Verify(rawBody, signature))
return Unauthorized();
foreach (var ev in payload.Events)
{
if (ev.Type != "postback") continue;
var data = ev.Postback?.Data;
if (data == null || !data.StartsWith("hil_")) continue;
var parts = data.Split('|', 2);
var action = parts[0]; // "hil_approve" or "hil_reject"
var resId = parts[1];
var responseData = new Dictionary<string, object>
{
["approved"] = action == "hil_approve",
["respondedBy"] = ev.Source?.UserId
};
_ = Task.Run(() => _continuationOrchestrator.ContinueAsync(resId, responseData));
}
return Ok();
}
LINE Signature Verification
// Compute HMAC-SHA256(channelSecret, rawBody) → Base64
var key = Encoding.UTF8.GetBytes(channelSecret);
var body = Encoding.UTF8.GetBytes(rawBody);
using var hmac = new HMACSHA256(key);
var computed = Convert.ToBase64String(hmac.ComputeHash(body));
bool valid = computed == signatureHeader;
Required Configuration
| Config Field | Value |
|---|---|
ChannelAccessToken | @{secret:LineChannelAccessToken} |
ChannelSecret | @{secret:LineChannelSecret} |
RecipientUserId | @{input:lineUserId} |
UseFlexMessage | true / false — use Confirm Template for simple flows |
U) are obtained from inbound webhook events
when a user adds your bot as a friend. Store the User ID against your user records at
that point, then reference it via @{input:lineUserId} in the workflow.
altText field as a push notification preview and in
chat list summaries. Always set a meaningful altText — "Approval required"
ensures the human knows something needs their attention even before opening the chat.