Webhooks¶
A webhook is an HTTP callback configured at the account level. When something happens in ChannelX — a message is created, a conversation status changes — ChannelX sends a POST request to the URLs you've registered. You can set up several webhooks per account and choose which events each one listens for.
Adding a webhook¶
- Go to Settings → Integrations → Webhooks and click Configure.
- Click Add new webhook. In the dialog, enter the URL that should receive the POST request and select the events you want to subscribe to. Subscribing only to relevant events keeps your endpoint from being flooded with noise.
ChannelX then sends a POST request to that URL whenever a subscribed event fires.
A sample payload¶
{
"event": "message_created",
"id": "1",
"content": "Hi",
"created_at": "2020-03-03 13:05:57 UTC",
"message_type": "incoming",
"content_type": "text",
"content_attributes": {},
"source_id": "",
"sender": {
"id": "1",
"name": "Agent",
"email": "agent@example.com"
},
"contact": {
"id": "1",
"name": "contact-name"
},
"conversation": {
"display_id": "1",
"additional_attributes": {
"browser": {
"device_name": "Macbook",
"browser_name": "Chrome",
"platform_name": "Macintosh"
}
}
},
"account": {
"id": "1",
"name": "ChannelX"
}
}
message_type is incoming (from the visitor), outgoing (from an agent), or template. content_type can be text, input_select, cards, or form.
Objects¶
An event payload is built from a small set of objects. The core ones are:
- Account —
id,name. - Inbox —
id,name. - Contact —
id,name,avatar,type, and the parent account. - User (agent/admin) —
id,name,email,type. - Conversation —
id,inbox_id,status,can_reply,channel,unread_count, thecontact_inboxlink, an array of messages, and ametablock with the sender and assignee. It also carriesadditional_attributes(browser, referer, initiated_at) and seen-timestamps. - Message —
id,content,message_type,content_type,content_attributes,private,source_id, plus the sender, account, conversation, and inbox.
Supported events¶
| Event | Fires when |
|---|---|
conversation_created |
A new conversation is created. |
conversation_updated |
Any conversation attribute changes. The payload includes changed_attributes with the current and previous values. |
conversation_status_changed |
A conversation's status changes. Not supported when using agent bot APIs instead of webhooks. |
message_created |
A message is created in a conversation. |
message_updated |
A message is updated. |
webwidget_triggered |
The visitor opens the live-chat widget. |
conversation_typing_on |
An agent starts typing. Use the is_private flag to tell a note apart from a customer reply. |
conversation_typing_off |
An agent stops typing or leaves the conversation window. |
Verifying webhooks¶
ChannelX signs every outgoing request so your server can confirm the payload genuinely came from ChannelX and wasn't altered in transit. The signing secret is shown once when the webhook is created and can be viewed again on the webhook's edit form.
Each request carries these headers:
X-ChannelX-Signature— the HMAC-SHA256 signature, prefixed withsha256=.X-ChannelX-Timestamp— the Unix timestamp (seconds) when the request was signed.X-ChannelX-Delivery— a unique delivery ID for the event, when available.
The signature is computed as:
sha256=HMAC-SHA256(webhook_secret, "{timestamp}.{raw_body}")
Where webhook_secret is the webhook's secret, timestamp is the value of the timestamp header, and raw_body is the raw JSON body exactly as received.
Verification steps¶
- Read
X-ChannelX-SignatureandX-ChannelX-Timestampfrom the headers. - Read the raw request body as bytes — do not parse and re-serialize it.
- Compute
sha256=HMAC-SHA256(secret, "{timestamp}.{raw_body}"). - Compare your result against the received signature using a constant-time comparison.
- Optionally reject requests whose timestamp is too old, to block replay attacks.
Example (Python)¶
import hmac
import hashlib
def verify_signature(raw_body: bytes, timestamp: str, received_signature: str, secret: str) -> bool:
message = f"{timestamp}.".encode() + raw_body
expected = "sha256=" + hmac.new(secret.encode(), message, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, received_signature)
Equivalent implementations work the same way in Ruby (ActiveSupport::SecurityUtils.secure_compare), Node.js (crypto.timingSafeEqual), and Go (hmac.Equal).
Do the verification right
- Always verify against the raw body. Parsing and re-serializing the JSON can reorder keys or change whitespace and will break the signature.
- Always use a constant-time comparison. A naive
==check leaks timing information that an attacker can exploit. - Reject requests older than about five minutes to mitigate replay attacks.
- Store the webhook secret securely and never expose it in client-side code or logs.