Every reply carries a kind
message.received always includes kind: one of text, image, video, audio or document. When a file came with the message, a media object describes it. body is the caption — and an empty caption is normal, not missing, because most people send a photo with no words.
The media object
| Field | Type | Notes |
|---|---|---|
kind | string | image, video, audio or document. |
mimetype | string | The stored content type, e.g. image/jpeg. |
filename | string | null | Present for documents; usually null for photos and voice notes. |
byte_size | integer | null | Null when the transport did not report a size. |
duration_seconds | integer | null | Seconds, for audio and video. |
voice | boolean | True for a WhatsApp voice note, false for a forwarded audio file. |
download_url | string | null | Where the bytes are fetched. Null when the file cannot be retrieved. |
The voice flag is the one that saves you a guess: a voice note and an audio file forwarded from someone's music both arrive as audio, and only this tells them apart.
An image with a caption
{
"id": "whd_9f2c81a0e34b4d1e8c5a7b93f0d2e611",
"type": "message.received",
"created_at": "2026-09-22T09:14:03.118Z",
"workspace_id": "019bf4a1-0000-7000-8000-000000000000",
"data": {
"message_id": "019bf4c3-0000-7000-8000-000000000001",
"connection": "main",
"from": { "phone": "+15551234567" },
"body": "Is this the right unit?",
"kind": "image",
"media": {
"kind": "image",
"mimetype": "image/jpeg",
"filename": null,
"byte_size": 214883,
"duration_seconds": null,
"voice": false,
"download_url": "/v1/messages/019bf4c3-0000-7000-8000-000000000001/media"
},
"received_at": "2026-09-22T09:14:02.740Z"
}
}A voice note
{
"data": {
"message_id": "019bf4c3-0000-7000-8000-000000000002",
"connection": "main",
"from": { "phone": "+15551234567" },
"body": "",
"kind": "audio",
"media": {
"kind": "audio",
"mimetype": "audio/ogg; codecs=opus",
"filename": null,
"byte_size": 18244,
"duration_seconds": 7,
"voice": true,
"download_url": "/v1/messages/019bf4c3-0000-7000-8000-000000000002/media"
}
}
}Nothing inbound is rejected
Classification on receipt describes the file; it never refuses it. A sticker arrives as an image, and any type ChatRail cannot pin down is treated as a document — the same thing WhatsApp does with an arbitrary file. The message has already been sent by a real person, so losing it to a strict content check would lose a real reply.
A file the transport will not hand over still arrives, with download_url set to null. “A voice note arrived and we cannot fetch it” is worth knowing; dropping the event would lose the reply along with the file. Fetch the bytes with the download guide.