Webhook events
Event types, headers, payloads, and signature verification for Blooio webhooks.
Overview
Blooio sends POST requests to your configured webhook URL for message lifecycle events. All webhooks are cryptographically signed so you can verify they originated from Blooio.
Headers
Every webhook request includes these headers:
| Header | Description | Example |
|---|---|---|
Content-Type | Always application/json | application/json |
X-Blooio-Event | The event type | message.sent |
X-Blooio-Message-Id | Associated message ID | gK2Ig_XGR2M6UkSgmT9FK |
X-Blooio-Signature | HMAC-SHA256 signature for verification | t=1703123457,v1=abc123... |
Replay headers
When a webhook is replayed, these additional headers are included:
| Header | Description |
|---|---|
X-Blooio-Replay | Set to true for replayed webhooks |
X-Original-Event-Id | The original event ID that was replayed |
Signature verification
Security best practice: Always verify webhook signatures in production to ensure requests are authentic and haven't been tampered with.
Every webhook includes an X-Blooio-Signature header with the format:
X-Blooio-Signature: t=1703123457,v1=f67c3b8ca86b024ee7090f57edb5e4c4f3a050862f50cb7baa73d7541a39f535Where:
t= Unix timestamp (seconds) when the webhook was sentv1= HMAC-SHA256 signature of{timestamp}.{raw_json_body}
Quick verification example
const crypto = require('crypto');
function verifyWebhook(secret, signatureHeader, rawBody) {
const [tPart, sigPart] = signatureHeader.split(',');
const timestamp = tPart.split('=')[1];
const signature = sigPart.split('=')[1];
// Compute expected signature
const payload = `${timestamp}.${rawBody}`;
const expected = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
// Use timing-safe comparison
return crypto.timingSafeEqual(
Buffer.from(expected, 'hex'),
Buffer.from(signature, 'hex')
);
}Getting your signing secret
Your webhook signing secret is returned only once when you create a webhook. If you lose it, you can rotate it via:
- The Dashboard → Webhooks → Click the rotate button
- The API:
POST /v2/api/webhooks/{webhookId}/secret/rotate
Rotating a secret immediately invalidates the old one. Update your endpoint before rotating.
Base fields
All webhook events include these fields:
| Field | Type | Description |
|---|---|---|
event | string | One of: message.received, message.sent, message.delivered, message.failed, message.read, group.name_changed, group.icon_changed |
message_id | string | Unique identifier for the message |
external_id | string | Phone number or email |
protocol | string | One of: imessage, sms, rcs, non-imessage |
timestamp | integer | Unix timestamp (ms) when the webhook was sent |
internal_id | string | Phone number of the device that handled the event |
message.received
Fired when an inbound message is received.
| Field | Type | Description |
|---|---|---|
text | string | Text content of the inbound message |
attachments | array | Attachment URLs included in the message |
received_at | integer | Unix timestamp (ms) when the device received the message |
sender | string | Phone number or email of the message sender |
is_group | boolean | Whether this message is from a group chat |
group_id | string | Group identifier (only present for group messages) |
group_name | string | Name of the group (only present for group messages) |
participants | array | Array of group participants (only present for group messages) |
{
"event": "message.received",
"message_id": "CZIsqG9ZWd9gwjIEhZpHY",
"external_id": "+15551234567",
"text": "Hello, I need help with my order",
"attachments": [],
"protocol": "imessage",
"timestamp": 1703123457474,
"internal_id": "+14155551234",
"received_at": 1703123456789,
"sender": "+15551234567",
"is_group": false,
"group_id": null,
"group_name": null,
"participants": null
}{
"event": "message.received",
"message_id": "GRP12ab34cd56",
"external_id": "grp_abc123xyz",
"text": "Hey everyone!",
"attachments": [],
"protocol": "imessage",
"timestamp": 1704123457000,
"internal_id": "+14155559876",
"received_at": 1704123456900,
"sender": "+15559876543",
"is_group": true,
"group_id": "grp_abc123xyz",
"group_name": "Sales Team",
"participants": [
{
"contact_id": "ct_xyz789",
"identifier": "+15551234567",
"name": "John Doe"
},
{
"contact_id": "ct_abc456",
"identifier": "+15559876543",
"name": null
}
]
}message.sent
Fired when an outbound message is sent by the device.
| Field | Type | Description |
|---|---|---|
text | string | Text content of the sent message (text variant only) |
attachments | array | Array of attachment URLs (attachment variant only) |
sent_at | integer | Unix timestamp (ms) when the message was actually sent |
{
"event": "message.sent",
"message_id": "gK2Ig_XGR2M6UkSgmT9FK",
"external_id": "+15551234567",
"protocol": "imessage",
"timestamp": 1703123458158,
"text": "Thanks for contacting us! How can I help?",
"internal_id": "+14155551234",
"sent_at": 1703123457370
}message.delivered
Fired when the message is confirmed delivered to the recipient.
Group messages: Delivery receipts are not available for group chats. Messages sent to groups will reach message.sent status but will not receive message.delivered webhooks.
| Field | Type | Description |
|---|---|---|
delivered_at | integer | Unix timestamp (ms) when the message was delivered |
{
"event": "message.delivered",
"message_id": "gK2Ig_XGR2M6UkSgmT9FK",
"external_id": "+15551234567",
"protocol": "imessage",
"timestamp": 1703123460773,
"internal_id": "+14155551234",
"delivered_at": 1703123457563
}message.failed
Fired when message delivery fails.
| Field | Type | Description |
|---|---|---|
error_code | string | Error code indicating the type of failure |
error_message | string | Human-readable error message |
{
"event": "message.failed",
"message_id": "jkl012_failed_msg",
"external_id": "+15551234567",
"protocol": "sms",
"timestamp": 1703123467000,
"internal_id": "+14155551234",
"error_code": "delivery_timeout",
"error_message": "Delivery timeout"
}message.read
Fired when the recipient reads the message (iMessage only, requires read receipts enabled).
Group messages: Read receipts are not available for group chats. This event only fires for 1:1 iMessage conversations where the recipient has read receipts enabled.
| Field | Type | Description |
|---|---|---|
read_at | integer | Unix timestamp (ms) when the message was read |
{
"event": "message.read",
"message_id": "gK2Ig_XGR2M6UkSgmT9FK",
"external_id": "+15551234567",
"protocol": "imessage",
"timestamp": 1703123469000,
"internal_id": "+14155551234",
"read_at": 1703123468402
}group.name_changed
Triggered when a participant changes the name of a group chat.
This event is only sent to webhooks with webhook_type: "all".
| Field | Type | Description |
|---|---|---|
group_id | string | Unique identifier for the group |
name | string | The new group name |
previous_name | string | The previous group name (null if previously unnamed) |
timestamp | integer | Unix timestamp (ms) when the event occurred |
{
"event": "group.name_changed",
"group_id": "grp_abc123xyz",
"name": "Sales Team 2025",
"previous_name": "Sales Team",
"timestamp": 1704123457000
}group.icon_changed
Triggered when a participant changes the icon/photo of a group chat.
This event is only sent to webhooks with webhook_type: "all".
| Field | Type | Description |
|---|---|---|
group_id | string | Unique identifier for the group |
icon_url | string | URL of the new group icon |
previous_icon_url | string | URL of the previous group icon (null if no previous icon) |
timestamp | integer | Unix timestamp (ms) when the event occurred |
{
"event": "group.icon_changed",
"group_id": "grp_abc123xyz",
"icon_url": "https://bucket.blooio.com/group-icons/abc123.png",
"previous_icon_url": null,
"timestamp": 1704123457000
}Best practices
Troubleshooting
| Issue | Solution |
|---|---|
| Signature verification fails | Ensure you're using the raw request body (not parsed JSON). The signature is computed on the exact bytes received. |
| Missing webhooks | Check webhook logs in the dashboard. Your endpoint may be returning errors or timing out. |
| Old timestamps | If testing with saved payloads, signature verification may fail due to timestamp checks. Increase tolerance for testing. |
| Secret not working | You may have rotated your secret. Get the current secret from the dashboard or rotate again. |