Webhooks
Receive inbound messages and lifecycle events in real time, and verify their signatures.
Webhooks deliver events to your server as they happen. Create a subscription with POST /webhooks, choosing which events you want with webhook_type:
curl -X POST https://api.blooio.com/v2/api/webhooks \
-H "Authorization: Bearer bl_live_..." \
-H "Content-Type: application/json" \
-d '{ "webhook_url": "https://example.com/blooio", "webhook_type": "all" }'Try itThe creation response includes a signing_secret once — store it to verify deliveries.
Event payload
Each delivery is a JSON body identifying the event and the message it relates to:
{
"event": "message.received",
"message_id": "msg_...",
"chat_id": "+15551234567",
"data": { }
}Verifying signatures
Each delivery is signed with your signing_secret (HMAC-SHA256) and sent in the X-Blooio-Signature header. Recompute the signature over {timestamp}.{raw_body} and compare it before trusting the payload — see Verify webhook signatures.
Event types
Set webhook_type to control which events you receive:
message— inbound and outbound message events (received, sent, delivered, failed, read, reactions).status— message status/lifecycle events only.all— every event, including polls, groups, and contact card events.
Inspect and replay past deliveries with GET /webhooks/{webhookId}/logs.
Webhooks deliver events to your server as they happen. Create a subscription with POST /webhooks, choosing the events you care about (or * for all):
curl -X POST https://api.blooio.com/v4/webhooks \
-H "Authorization: Bearer bl_live_..." \
-H "Content-Type: application/json" \
-d '{ "url": "https://example.com/blooio", "event_types": ["message.received", "message.*"] }'Try itThe creation response includes a signing_secret once — store it to verify deliveries.
Event envelope
Every delivery shares the same envelope. type is the event name, created_at is epoch milliseconds, and data carries the fully-hydrated resource — the message body, poll, group, or chat endpoints — so you never have to call the API back to fetch it:
{
"id": "evt_018f7b2a-1c3d-7e4f-9a2b-0c1d2e3f4a5b",
"type": "message.received",
"created_at": 1718000000000,
"organization_id": "org_abc123",
"data": { }
}The examples below show the data object for each event (the envelope is omitted for brevity). Endpoints are direction-aware: on inbound events sender is the contact and recipient is your channel; on outbound events they're reversed.
Verifying signatures
Each delivery is signed with your signing_secret (HMAC-SHA256) and sent in the X-Blooio-Signature header (t=<timestamp>,v1=<hmac>). Recompute the signature over {timestamp}.{raw_body} and compare it before trusting the payload — see Webhook signatures. For local development, see Receive webhooks locally.
Event types
Subscribe to exact types, category wildcards (message.*, poll.*, group.*, typing.*), or * for everything.
Messages
message.received, message.queued, message.sent, message.delivered, message.failed, message.read, message.reaction
Inbound message (message.received):
{
"message_id": "msg_018f7b2a-9f6c-7a11-b2c3-1a2b3c4d5e6f",
"chat_id": "chat_018f7b2a-77aa-7c22-9d3e-4f5a6b7c8d9e",
"channel_id": "ch_018f7b2a-1c3d-7e4f-9a2b-0c1d2e3f4a5b",
"channel_type": "blooio",
"kind": "received",
"direction": "inbound",
"text": "Hey — is the blue one still in stock?",
"status": "received",
"protocol": "imessage",
"provider_message_id": "p:0/1A2B3C4D-5E6F-7089-ABCD-EF0123456789",
"message_type": "text",
"sender": "+15551234567",
"recipient": "+15557654321",
"contact": { "identifier": "+15551234567" },
"channel_address": "+15557654321",
"attachments": []
}Delivery status (message.delivered — also sent, read, failed, queued). A failed event adds an error object:
{
"message_id": "msg_018f7b2a-9f6c-7a11-b2c3-1a2b3c4d5e6f",
"chat_id": "chat_018f7b2a-77aa-7c22-9d3e-4f5a6b7c8d9e",
"channel_id": "ch_018f7b2a-1c3d-7e4f-9a2b-0c1d2e3f4a5b",
"channel_type": "blooio",
"kind": "delivered",
"direction": "outbound",
"text": "Yes! Want me to set one aside?",
"status": "delivered",
"protocol": "imessage",
"message_type": "text",
"sender": "+15557654321",
"recipient": "+15551234567",
"contact": { "identifier": "+15551234567" },
"channel_address": "+15557654321",
"attachments": []
}For media messages, message_type is media and attachments is populated, e.g. [{ "url": "https://.../photo.jpg", "media_type": "image/jpeg", "caption": "Check this out" }].
Reaction (message.reaction) — keyed by the reacted message, so text/status reflect that message:
{
"message_id": "msg_018f7b2a-9f6c-7a11-b2c3-1a2b3c4d5e6f",
"chat_id": "chat_018f7b2a-77aa-7c22-9d3e-4f5a6b7c8d9e",
"channel_id": "ch_018f7b2a-1c3d-7e4f-9a2b-0c1d2e3f4a5b",
"channel_type": "blooio",
"reaction": "+love",
"action": "add",
"direction": "inbound",
"text": "Yes! Want me to set one aside?",
"status": "delivered",
"message_type": "text",
"sender": "+15551234567",
"recipient": "+15557654321",
"contact": { "identifier": "+15551234567" },
"channel_address": "+15557654321"
}Polls
poll.created, poll.received, poll.voted
Poll created (poll.created; inbound polls arrive as poll.received):
{
"poll_id": "msg_018f7b2a-aa11-7b22-8c33-9d44e55f6a77",
"chat_id": "chat_018f7b2a-77aa-7c22-9d3e-4f5a6b7c8d9e",
"title": "Which day works best?",
"options": ["Monday", "Wednesday", "Friday"],
"direction": "outbound",
"poll": { "title": "Which day works best?", "options": ["Monday", "Wednesday", "Friday"] },
"status": "sent",
"sender": "+15557654321",
"recipient": "+15551234567",
"contact": { "identifier": "+15551234567" },
"channel_address": "+15557654321"
}Poll vote (poll.voted) — toggled is on/off and voted_options is the voter's current selection:
{
"poll_id": "msg_018f7b2a-aa11-7b22-8c33-9d44e55f6a77",
"chat_id": "chat_018f7b2a-77aa-7c22-9d3e-4f5a6b7c8d9e",
"voted_options": ["Wednesday"],
"toggled": "on",
"direction": "inbound",
"poll": { "title": "Which day works best?", "options": ["Monday", "Wednesday", "Friday"] },
"status": "delivered",
"sender": "+15551234567",
"recipient": "+15557654321",
"contact": { "identifier": "+15551234567" },
"channel_address": "+15557654321"
}Groups
group.name_changed, group.icon_changed
Name changed (group.name_changed). group.icon_changed is the same shape but carries icon_url / previous_icon_url instead of the name fields:
{
"group_id": "grp_018f7b2a-bb22-7c33-9d44-e55f6a778b99",
"name": "Weekend Trip",
"previous_name": "Trip planning",
"group": {
"group_id": "grp_018f7b2a-bb22-7c33-9d44-e55f6a778b99",
"name": "Weekend Trip",
"icon_url": null,
"chat_id": "chat_018f7b2a-77aa-7c22-9d3e-4f5a6b7c8d9e",
"channel_id": "ch_018f7b2a-1c3d-7e4f-9a2b-0c1d2e3f4a5b",
"channel_type": "blooio",
"member_count": 4
},
"channel_address": "+15557654321"
}Contact card
contact.shared — fired when a contact card is shared into a chat:
{
"chat_id": "chat_018f7b2a-77aa-7c22-9d3e-4f5a6b7c8d9e",
"direction": "inbound",
"sender": "+15551234567",
"recipient": "+15557654321",
"contact": { "identifier": "+15551234567" },
"channel_address": "+15557654321",
"is_group": false,
"group": null
}Typing
typing.started, typing.stopped
{
"chat_id": "chat_018f7b2a-77aa-7c22-9d3e-4f5a6b7c8d9e",
"direction": "inbound",
"sender": "+15551234567",
"recipient": "+15557654321",
"contact": { "identifier": "+15551234567" },
"channel_address": "+15557654321",
"is_group": false,
"group": null
}Inspect and replay past deliveries with GET /webhooks/{webhookId}/deliveries.