Webhook event: message.received
The payload Cooledge sends when a customer or an unknown sender texts or emails the business.
message.received
Sent when a customer or an unknown sender texts or emails the business and the message lands in Messages. Delivered to your webhook subscriptions. Deliveries are at-least-once, so dedupe on X-Cooledge-Event-Id.
It is not instant. Cooledge waits until an email's attachments (or a picture message's media) are stored, so the event lists every file, then sends it on a delivery run every few minutes. Expect it usually within a few minutes of the message arriving. Do not build anything that needs it in real time.
Only messages that arrive while a subscription to message.received exists are sent, so subscribing does not replay older messages. Calls and voicemails have their own events. A website chat that creates a lead is covered by lead.created instead. Internal team comments never fire it.
Subscribing to this event through the API needs the messages:read scope. See the events index.
Envelope
Every event shares the flat envelope: event_type, version ("v1"), event_id, business_id, occurred_at, plus the fields below. See the events overview for the envelope detail and webhooks for signature verification and delivery semantics. Here occurred_at is when the message arrived.
Fields
| Field | Type | Notes |
|---|---|---|
message | object | The message. |
message.id | string | UUID of the message. The same id the messages endpoint returns. |
message.direction | string | Always "in" for this event. |
message.channel | string | "sms" or "email". |
message.subject | string or null | The email subject. Null for a text. |
message.text | string or null | The plain text body. |
message.html | string or null | The email's HTML. Null for a text. This is the sender's content: treat it as untrusted and sanitise it before you render it. |
message.from | string or null | The sender's phone number or email address. |
message.to | string or null | The business number or address it was sent to. |
message.cc | array of strings | Email CC addresses. Empty for a text. |
message.at | string | ISO 8601 timestamp for when the message arrived. |
message.attachments | array | The files on the message, each { filename, content_type, size } with content_type and size (in bytes) possibly null. Inline email images are left out. Empty when there are none. |
conversation | object or null | The conversation the message is in: { id, state, is_unmatched }. state is one of open, snoozed, archived, trashed or spam. is_unmatched is true when the sender is not matched to a customer. |
customer | object or null | The matched customer. Null for an unknown sender. |
customer.id | string | UUID of the customer. |
customer.name | string or null | The customer's display name. |
customer.email | string or null | The customer's saved email, which may differ from the address the message came from. |
customer.phone | string or null | The customer's saved phone, which may differ from the number the message came from. |
contact | object or null | The customer's contact the message came from, when it came from one. Null otherwise. |
contact.id | string | UUID of the contact. |
contact.name | string or null | The contact's first and last name. |
sender_address | string or null | An unknown sender's phone number or email address. Null when the conversation is matched to a customer. |
The payload is built when the event is sent, a few minutes after the message arrived, so conversation.state and the customer details are as they were then.
Example
{
"version": "v1",
"event_type": "message.received",
"event_id": "b4e7d2a9-1c58-4f36-8e0b-6a3d9c2f7e15",
"business_id": "a7f3c9d1-2e84-4b6f-8c01-5d9e2a1f7b30",
"occurred_at": "2026-10-11T03:21:00.000Z",
"message": {
"id": "4a8e2c71-9b06-4d3f-a5e8-7c1d0f6b2a93",
"direction": "in",
"channel": "email",
"subject": "Aircon still making a noise",
"text": "Hi, can you come Thursday instead? I have attached a video of the noise.",
"html": "<p>Hi, can you come Thursday instead? I have attached a video of the noise.</p>",
"from": "priya.nair@example.com",
"to": "hello@coolairhvac.com.au",
"cc": [],
"at": "2026-10-11T03:21:00.000Z",
"attachments": [
{ "filename": "noise.mp4", "content_type": "video/mp4", "size": 4183920 }
]
},
"conversation": {
"id": "8f3a1c52-6d47-4b9e-a210-5c7e9d2b4f18",
"state": "open",
"is_unmatched": false
},
"customer": {
"id": "c4a9e1b2-7d3f-4a8c-9e2b-1f6a0d5c3b7e",
"name": "Priya Nair",
"email": "priya.nair@example.com",
"phone": "+61498765432"
},
"contact": null,
"sender_address": null
}
To reply, write a draft with POST /v1/drafts using conversation.id and, for an email, message.id as reply_to_message_id. See webhooks for signature verification and delivery semantics. See all events for the full list.
Need a hand with an integration? Contact support