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, typing.started, typing.stopped, poll.received, poll.created, poll.voted, group.name_changed, group.icon_changed, contact.shared
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"
}

Typing webhooks

Typing events are delivered through the typing.* event namespace. They fire when a recipient starts or stops composing in a chat, for both 1:1 and group conversations.

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

iMessage only: typing indicators are carried by the iMessage protocol. SMS and RCS threads do not emit composing state, so no typing.* events fire for those chats.

typing.started

Fired when a recipient starts typing.

FieldTypeDescription
external_idstringThe chat the recipient is typing in — a phone number/email for 1:1, or the group ID for groups
senderstringPhone number or email of the participant typing (1:1 only; null for groups, where the protocol does not identify which participant is composing)
is_groupbooleanWhether the typing is happening in a group chat
group_idstringGroup identifier (only present for group chats)
group_namestringName of the group (only present for group chats)
internal_idstringPhone number of the device line that observed the event
timestampintegerUnix timestamp (ms) when the webhook was sent
1:1 example
{
  "event": "typing.started",
  "external_id": "+15551234567",
  "sender": "+15551234567",
  "is_group": false,
  "group_id": null,
  "group_name": null,
  "internal_id": "+14155551234",
  "timestamp": 1703123457474
}
Group example
{
  "event": "typing.started",
  "external_id": "grp_abc123xyz",
  "sender": null,
  "is_group": true,
  "group_id": "grp_abc123xyz",
  "group_name": "Sales Team",
  "internal_id": "+14155559876",
  "timestamp": 1704123457000
}

typing.stopped

Fired when a recipient stops typing. Same payload shape as typing.started.

Example
{
  "event": "typing.stopped",
  "external_id": "+15551234567",
  "sender": "+15551234567",
  "is_group": false,
  "group_id": null,
  "group_name": null,
  "internal_id": "+14155551234",
  "timestamp": 1703123459000
}

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
}

Contact webhooks

Contact events are delivered through the contact.* event namespace.

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

contact.shared

Triggered when a recipient shares their Name & Photo contact card (Apple's "Name and Photo Sharing") with your number. This is inbound — it fires when the other party shares their card with you, not when you share yours.

When this event fires, Blooio also auto-updates the matching contact in your organization (the contact identified by address): the display name, first/last name, and photo are applied to the contact record (creating it if needed).

FieldTypeDescription
addressstringPhone number or email of the contact who shared their card
display_namestring | nullThe shared display name
first_namestring | nullThe shared first name
last_namestring | nullThe shared last name
avatar_urlstring | nullPublic URL of the shared avatar image (stored by Blooio); null if no photo was shared
is_pendingbooleantrue if the share is awaiting approval, false if already accepted/active
timestampintegerUnix timestamp (ms) when the event occurred
Example
{
  "event": "contact.shared",
  "address": "+15551234567",
  "display_name": "Jamie Rivera",
  "first_name": "Jamie",
  "last_name": "Rivera",
  "avatar_url": "https://bucket.blooio.com/contact-avatars/V1StGXR8_Z5jdHi6B.png",
  "is_pending": false,
  "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