Interactions & Webhooks
Interactions are events fired when users interact with a channel — clicking buttons, changing inputs, or triggering header actions. Outerfaced logs every interaction and optionally delivers a webhook payload to your configured URL.
How Interactions Work
User clicks button in channel UI
│
▼
Interaction is logged
│
└─ If webhook_url set → POST payload to webhook_url
Interaction Types
| Type | Trigger |
|---|---|
button_click |
User clicks a button block |
header_action |
User clicks a header action button |
input_change |
User changes an input/select/toggle with send_on_change: true |
reply |
User submits a text reply in an interactive channel |
Webhook Payload
When an interaction occurs on a channel with a webhook_url, Outerfaced sends a POST request with the following JSON body:
{
"interaction_id": "7bec44fd-ebf5-4fd7-8f22-d8f63fa44ed1",
"channel_id": "92b65844-0252-4020-998e-86572e301cdb",
"card_id": "dadef981-5a64-4d4c-a7fa-9ebd6ce2f380",
"timestamp": "2026-05-15T15:13:31.348041+00:00",
"trigger": { ... },
"card_state": [ ... ]
}
Top-level Fields
| Field | Type | Description |
|---|---|---|
interaction_id |
UUID | Unique ID of this interaction event |
channel_id |
UUID | Channel where the interaction occurred |
card_id |
UUID | null | Card that was interacted with. null for header actions |
timestamp |
ISO 8601 | UTC timestamp of when the interaction occurred |
trigger |
object | What triggered the webhook — the element, its type, and relevant metadata |
card_state |
array | null | Full state of every block on the card at the moment of the trigger. null for header actions |
The `trigger` Object
The trigger object describes exactly what the user did. Its shape depends on the interaction type.
`button_click`
Fired when a user clicks any button on a card.
{
"trigger": {
"type": "button_click",
"button_id": "btn_approve",
"label": "Approve"
}
}
| Field | Type | Description |
|---|---|---|
type |
string | Always "button_click" |
button_id |
string | The id you assigned to this button in the block definition |
label |
string | The visible label of the button that was clicked |
`header_action`
Fired when a user clicks one of the channel's header action buttons (not on a card).
{
"trigger": {
"type": "header_action",
"label": "Refresh"
}
}
| Field | Type | Description |
|---|---|---|
type |
string | Always "header_action" |
label |
string | The label of the header action button |
`reply`
Fired when a user submits a text reply in an interactive channel. Replies are not tied to a specific card, so card_id and card_state will both be null.
{
"trigger": {
"type": "reply",
"text": "I'll take care of this now."
}
}
| Field | Type | Description |
|---|---|---|
type |
string | Always "reply" |
text |
string | The message the user submitted |
`input_change`
Fired when a block with send_on_change: true is modified. By default, interactive block values are not sent in real time — they're captured via card_state on button click instead. Enable send_on_change on a specific block to receive live updates.
{
"trigger": {
"type": "input_change",
"block_type": "toggle",
"value": true
}
}
| Field | Type | Description |
|---|---|---|
type |
string | Always "input_change" |
block_type |
string | The block type that changed: "input", "select", "multiselect", or "toggle" |
value |
string | string[] | boolean | Current value of the block after the change |
The `card_state` Array
card_state is an ordered array of every block on the card, with its current runtime state at the moment the trigger fired. This lets you know the full context of the card — not just what was clicked, but what every field contained.
{
"card_state": [
{
"id": "header-text",
"type": "text",
"content": "Approve this request?"
},
{
"id": "notes-input",
"type": "input",
"label": "Notes",
"value": "Looks good to me"
},
{
"id": "priority-select",
"type": "select",
"label": "Priority",
"value": "high"
},
{
"id": "notify-toggle",
"type": "toggle",
"label": "Notify team",
"value": true
},
{
"id": "action-buttons",
"type": "button",
"buttons": [
{ "id": "btn_approve", "label": "Approve", "style": "primary" },
{ "id": "btn_reject", "label": "Reject", "style": "danger" }
]
}
]
}
Each entry in the array shares a common id and type, then includes block-specific fields:
Interactive blocks (have a `value`)
type |
value type |
Description |
|---|---|---|
input |
string |
Current text content of the input |
select |
string | null |
Currently selected option value, or null if nothing selected |
multiselect |
string[] |
Array of selected option values |
toggle |
boolean |
true if enabled, false if disabled |
Display blocks (no `value`)
type |
Extra fields | Description |
|---|---|---|
text |
content |
The text content of the block |
badge |
label, variant |
Badge label and its style variant |
kv |
rows |
Array of { key, value } pairs |
image |
url, alt |
Image URL and optional alt text |
button |
buttons |
Array of button definitions (each with id, label, style) |
divider |
— | No extra fields |
Tip: reading `card_state` vs using `trigger`
- Use
triggerto know what the user did (which button, which action). - Use
card_stateto know what state the card was in when they did it (form values, toggle states, etc.).
For example, to get the value of a specific toggle after a button click, find it by id in card_state:
// n8n / JS example
const state = $json.body.card_state
const notifyToggle = state.find(b => b.id === 'notify-toggle')
const shouldNotify = notifyToggle?.value // true or false
Webhook Logs
Every delivery attempt is logged. View logs in the dashboard under Channel → Logs.
| Field | Values | Description |
|---|---|---|
status |
"success", "failed" |
Whether delivery succeeded |
response_code |
integer | null | HTTP response code returned by your endpoint |
attempts |
integer | Number of delivery attempts made |
Webhook Requirements
Your endpoint must:
- Accept
POSTrequests withContent-Type: application/json - Return a
2xxHTTP status to acknowledge receipt - Respond within 10 seconds (requests time out after this)
Non-2xx responses are logged as "failed". Outerfaced does not automatically retry failed deliveries.
Setting a Webhook URL
Set the webhook_url when creating or updating a channel:
curl -X PATCH /api/v1/channels/{channelId} \
-H "Authorization: Bearer ofd_your_key" \
-H "Content-Type: application/json" \
-d '{
"webhook_url": "https://hook.make.com/your-scenario-id"
}'
Compatible with Make, Zapier, n8n, and any HTTP webhook receiver. When using n8n, your actual payload is under the body key — reference fields as $json.body.trigger, $json.body.card_state, etc.