Message Blocks
A message can carry plain text (body), structured rich content blocks
(blocks), or both. Blocks let bots and agents send buttons, cards, carousels,
images, files and translations rather than only flat text.
blocks is an array of a discriminated union keyed by the type field.
Every block object declares its own type; the remaining fields depend on that
type. The machine-readable contract is the MessageBlock schema in
apps/api/openapi/openapi.yaml (and its generated TypeScript union in
@smaibot/ts-contracts); this page is the narrative reference.
Sending blocks
Blocks are sent through the same endpoint as plain messages:
POST /api/conversations/{conversationId}/messages
{
"blocks": [
{ "type": "text", "text": "Here are your options:" },
{
"type": "buttons",
"buttons": [
{ "title": "Open docs", "url": "https://example.com/docs" },
{ "title": "Talk to sales", "payload": "SALES" }
]
}
]
}
A message must include a non-empty body, a non-empty blocks array, or both —
sending neither is a 422. The sender identity is always derived from the
authenticated agent and is never read from the request body.
Validation
Each block is validated as a discriminated union in two steps:
- Discriminator.
typemust be one of the known block types. An unknown type fails with a validation error onblocks.{index}.type. - Payload. The fields of the block are validated against the rules for that
specific type. For example a
textblock requires a non-emptytext, and animageblock requires a validurl. A bad payload fails with an error on the offending field, e.g.blocks.0.textorblocks.2.buttons.0.title.
Validation failures use the standard 422 ValidationError envelope (see
Errors), so the client can map each error back to the exact block
and field.
Block types
All blocks share the type discriminator. The payload fields are listed below.
text
A plain-text block.
| Field | Type | Required | Notes |
|---|---|---|---|
text | string | yes | Non-empty content. |
{ "type": "text", "text": "Hello there" }
buttons
A row of actionable buttons, optionally introduced by a text prompt.
| Field | Type | Required | Notes |
|---|---|---|---|
text | string | no | Prompt shown above the row. |
buttons | Button[] | yes | At least one button. |
A Button has a title plus either a url (link) or a payload (postback
sent back to the bot):
| Field | Type | Required | Notes |
|---|---|---|---|
title | string | yes | Visible label. |
url | string | no | Valid URL for link buttons. |
payload | string | no | Postback value for the bot. |
{
"type": "buttons",
"text": "Pick one",
"buttons": [
{ "title": "Docs", "url": "https://example.com" },
{ "title": "More", "payload": "MORE" }
]
}
card
A card with a title and optional subtitle, image and buttons.
| Field | Type | Required | Notes |
|---|---|---|---|
title | string | yes | Non-empty. |
subtitle | string | no | |
image_url | string | no | Valid URL. |
buttons | Button[] | no | Same shape as above. |
{
"type": "card",
"title": "Pro plan",
"subtitle": "Everything you need",
"image_url": "https://example.com/pro.png",
"buttons": [{ "title": "Upgrade", "payload": "UPGRADE_PRO" }]
}
slider
A horizontally-scrollable carousel of card blocks.
| Field | Type | Required | Notes |
|---|---|---|---|
cards | Card[] | yes | At least one card. |
{
"type": "slider",
"cards": [
{ "type": "card", "title": "Starter" },
{ "type": "card", "title": "Pro", "subtitle": "Most popular" }
]
}
image
An inline image referenced by URL.
| Field | Type | Required | Notes |
|---|---|---|---|
url | string | yes | Valid URL. |
alt | string | no | Alt text. |
width | integer | no | Pixels. |
height | integer | no | Pixels. |
{ "type": "image", "url": "https://example.com/p.png", "alt": "Screenshot" }
file
A downloadable file reference, rendered as an attachment chip.
| Field | Type | Required | Notes |
|---|---|---|---|
url | string | yes | Valid URL. |
filename | string | yes | Non-empty. |
mime | string | no | MIME type. |
size | integer | no | Bytes. |
{
"type": "file",
"url": "https://example.com/report.pdf",
"filename": "report.pdf",
"mime": "application/pdf",
"size": 20480
}
translation
A machine translation carrying both the original and translated text.
| Field | Type | Required | Notes |
|---|---|---|---|
original | string | yes | Source text. |
translated | string | yes | Translated text. |
locale | string | yes | Target locale. |
{
"type": "translation",
"original": "Merhaba",
"translated": "Hello",
"locale": "en"
}
How blocks are returned
The Message resource returns blocks in the same structured shape that was
sent, normalized through the typed block model — so the round-trip is
predictable and unknown fields are dropped. blocks is null when a message
has none.
Webhooks
Outbound message webhooks carry the same blocks array in their payload. A
consumer should switch on type and treat any unrecognized type defensively
(new block types may be added over time).