Blooio API Reference

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:

HeaderDescriptionExample
Content-TypeAlways application/jsonapplication/json
X-Blooio-EventThe event typemessage.sent
X-Blooio-Message-IdAssociated message IDgK2Ig_XGR2M6UkSgmT9FK
X-Blooio-SignatureHMAC-SHA256 signature for verificationt=1703123457,v1=abc123...

Replay headers

When a webhook is replayed, these additional headers are included:

HeaderDescription
X-Blooio-ReplaySet to true for replayed webhooks
X-Original-Event-IdThe 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=f67c3b8ca86b024ee7090f57edb5e4c4f3a050862f50cb7baa73d7541a39f535

Where:

  • t = Unix timestamp (seconds) when the webhook was sent
  • v1 = 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:

FieldTypeDescription
eventstringOne of: message.received, message.sent, message.delivered, message.failed, message.read, message.reaction, poll.received, poll.created, poll.voted, group.name_changed, group.icon_changed
message_idstringUnique identifier for the message
external_idstringPhone number or email
protocolstringOne of: imessage, sms, rcs, non-imessage
timestampintegerUnix timestamp (ms) when the webhook was sent
internal_idstringPhone number of the device that handled the event

Poll webhooks

Poll events are sent for native iMessage polls.

Poll events are only delivered to webhooks configured with webhook_type: "poll" or webhook_type: "all".

poll.received

Fired when an inbound poll is received.

FieldTypeDescription
poll_idstringUnique poll message identifier
chat_idstringContact identifier or group ID
senderstringPhone number/email of the sender
titlestringPoll title/question
optionsarrayPoll option texts
is_groupbooleanWhether this poll is from a group chat
group_idstringGroup identifier (group events only)
group_namestringGroup name (group events only)
participantsarrayGroup participants (group events only)
Example
{
  "event": "poll.received",
  "poll_id": "pl_abc123",
  "chat_id": "+15551234567",
  "sender": "+15551234567",
  "title": "Lunch spot?",
  "options": ["Sushi", "Tacos", "Salad"],
  "is_group": false,
  "timestamp": 1705123457000
}

poll.created

Fired when your org creates/sends a poll.

FieldTypeDescription
poll_idstringUnique poll message identifier
chat_idstringContact identifier or group ID
senderstringSending number/identifier
titlestringPoll title/question
optionsarrayPoll option texts
is_groupbooleanWhether this poll targets a group chat
Example
{
  "event": "poll.created",
  "poll_id": "pl_def456",
  "chat_id": "+15557654321",
  "sender": "+14155551234",
  "title": "What time?",
  "options": ["10:00", "11:00", "12:00"],
  "is_group": false,
  "timestamp": 1705123459000
}

poll.voted

Fired when a participant votes (or updates their vote) on a poll.

FieldTypeDescription
poll_idstringIdentifier of the original poll
chat_idstringContact identifier or group ID
voterstringPhone number/email of the voter
titlestringPoll title
voted_optionsarrayCurrently selected option texts
is_groupbooleanWhether vote happened in a group chat
group_idstringGroup identifier (group events only)
group_namestringGroup name (group events only)
participantsarrayGroup participants (group events only)
Example
{
  "event": "poll.voted",
  "poll_id": "pl_def456",
  "chat_id": "+15557654321",
  "voter": "+15557654321",
  "title": "What time?",
  "voted_options": ["11:00"],
  "is_group": false,
  "timestamp": 1705123461000
}

Message webhooks

Message lifecycle and reaction events are delivered through the message.* event namespace.

Message events are delivered to webhooks configured with webhook_type: "message" or webhook_type: "all".

message.received

Fired when an inbound message is received.

FieldTypeDescription
textstringText content of the inbound message
attachmentsarrayAttachment URLs included in the message
received_atintegerUnix timestamp (ms) when the device received the message
senderstringPhone number or email of the message sender
is_groupbooleanWhether this message is from a group chat
group_idstringGroup identifier (only present for group messages)
group_namestringName of the group (only present for group messages)
participantsarrayArray of group participants (only present for group messages)
Example
{
  "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
}
Group message example
{
  "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.

FieldTypeDescription
textstringText content of the sent message (text variant only)
attachmentsarrayArray of attachment URLs (attachment variant only)
sent_atintegerUnix timestamp (ms) when the message was actually sent
Example
{
  "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.

FieldTypeDescription
delivered_atintegerUnix timestamp (ms) when the message was delivered
Example
{
  "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.

FieldTypeDescription
error_codestringError code indicating the type of failure
error_messagestringHuman-readable error message
Example
{
  "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.

FieldTypeDescription
read_atintegerUnix timestamp (ms) when the message was read
Example
{
  "event": "message.read",
  "message_id": "gK2Ig_XGR2M6UkSgmT9FK",
  "external_id": "+15551234567",
  "protocol": "imessage",
  "timestamp": 1703123469000,
  "internal_id": "+14155551234",
  "read_at": 1703123468402
}

message.reaction

Fired when someone adds or removes a tapback reaction (love, like, dislike, laugh, emphasize, question) on a message.

FieldTypeDescription
directionstringinbound (someone reacted to a message) or outbound (you sent the reaction)
reactionstringOne of: love, like, dislike, laugh, emphasize, question
actionstringadd (reaction added) or remove (reaction removed)
senderstringPhone number or email of the person who sent the reaction
original_textstringText content of the message that was reacted to
Example — reaction added
{
  "event": "message.reaction",
  "direction": "inbound",
  "message_id": "msg_abc123def456",
  "external_id": "+15551234567",
  "reaction": "love",
  "action": "add",
  "sender": "+15551234567",
  "original_text": "Thanks for your help!",
  "timestamp": 1703123470000,
  "internal_id": "+14155551234"
}
Example — reaction removed
{
  "event": "message.reaction",
  "direction": "inbound",
  "message_id": "msg_abc123def456",
  "external_id": "+15551234567",
  "reaction": "love",
  "action": "remove",
  "sender": "+15551234567",
  "original_text": "Thanks for your help!",
  "timestamp": 1703123475000,
  "internal_id": "+14155551234"
}

Group webhooks

Group events are delivered through the group.* event namespace.

Group events are only delivered to webhooks configured with webhook_type: "all".

group.name_changed

Triggered when a participant changes the name of a group chat.

FieldTypeDescription
group_idstringUnique identifier for the group
namestringThe new group name
previous_namestringThe previous group name (null if previously unnamed)
timestampintegerUnix timestamp (ms) when the event occurred
Example
{
  "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.

FieldTypeDescription
group_idstringUnique identifier for the group
icon_urlstringURL of the new group icon
previous_icon_urlstringURL of the previous group icon (null if no previous icon)
timestampintegerUnix timestamp (ms) when the event occurred
Example
{
  "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

IssueSolution
Signature verification failsEnsure you're using the raw request body (not parsed JSON). The signature is computed on the exact bytes received.
Missing webhooksCheck webhook logs in the dashboard. Your endpoint may be returning errors or timing out.
Old timestampsIf testing with saved payloads, signature verification may fail due to timestamp checks. Increase tolerance for testing.
Secret not workingYou may have rotated your secret. Get the current secret from the dashboard or rotate again.

On this page