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, 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

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
}

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".

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.

This event is only sent to webhooks with webhook_type: "all".

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