> For the complete documentation index, see [llms.txt](https://api.bind.hr/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://api.bind.hr/channels/models.md).

# Models

## The MessageRequest object

```json
{"openapi":"3.0.3","info":{"title":"Channels API","version":"1.0"},"components":{"schemas":{"MessageRequest":{"type":"object","required":["channels"],"properties":{"channels":{"type":"array","minItems":1,"description":"The channels to send on, in order. At least one. Each entry carries everything that channel needs.\n","items":{"$ref":"#/components/schemas/ChannelEntry"}}}},"ChannelEntry":{"type":"object","required":["channel","profile_id","to","content"],"description":"One channel to send on. Everything the send needs is on the entry itself - a missing field is a `400` naming it.\n","properties":{"channel":{"type":"string","description":"Channel name, e.g. `viber` or `sms`."},"profile_id":{"type":"string","format":"uuid","description":"The sender profile this channel sends from, by the UUID we issued for it. We fill in the sender from it, and it is what the message is attributed to in your reporting. A profile belongs to one channel, so each entry has its own. A UUID that matches no active profile of yours is rejected with `PROFILE_NOT_FOUND`.\n"},"to":{"type":"string","description":"Recipient phone number in international format."},"content":{"$ref":"#/components/schemas/Content"},"failover_after":{"type":"integer","minimum":1,"description":"How many seconds to wait for delivery on this channel before moving to the next entry. Only meaningful when there is a next entry.\n"}}},"Content":{"description":"For `sms`, the content is a plain text string. For rich channels like `viber` it is an object. The two are interchangeable for plain text: a string sent to Viber goes out as a text message, and `{\"type\": \"text\", \"text\": \"...\"}` sent to SMS goes out as that text.\n","oneOf":[{"type":"string","description":"Plain text."},{"$ref":"#/components/schemas/ViberContent"}]},"ViberContent":{"type":"object","description":"Viber message content. Not every combination of fields is a valid Viber message, so the ones that work are listed here in full. Anything else is rejected with `UNSUPPORTED_CONTENT`. There is a ready-made example of each under **Request samples** on both send endpoints.\n\n**These work:**\n\n| Content | With text | With a button | |---|---|---| | `text` | - | yes | | `media_url` (image) | yes | yes | | `video` | yes | yes | | `file` | no | no | | `carousel` | yes | no | | `list` | yes | no |\n\n**These do not:** an image and a video (or a file) together; a carousel and a list together; a file with anything else at all; a button on its own, with no text, image or video to attach it to; a button with no `caption`; a completely empty content object. `ttl` may accompany any of the working combinations.\n\nOnly fields you actually fill in are sent on; empty strings, empty objects and empty lists count as not set. Keys are snake_case, and the camelCase spelling noted per field also works.\n","properties":{"text":{"type":"string"},"media_url":{"type":"string","format":"uri","description":"Image URL. Also accepted as `mediaUrl`."},"video":{"type":"object","description":"A video. This is an object, not a plain URL string.","properties":{"media_url":{"type":"string","format":"uri","description":"Video URL. Also accepted as `mediaUrl`."},"thumbnail":{"type":"string","format":"uri"}}},"file":{"type":"object","description":"A file. This is an object, not a plain URL string. A file must be sent on its own - no text, button, image or video alongside it.\n","properties":{"media_url":{"type":"string","format":"uri","description":"File URL. Also accepted as `mediaUrl`."},"file_name":{"type":"string","description":"Also accepted as `fileName`."}}},"button":{"type":"object","required":["caption"],"description":"A button needs text, an image or a video alongside it, and it must have a caption.\n","properties":{"caption":{"type":"string","description":"The button label. Required."},"action":{"type":"string","description":"What the button opens - an `https://` or `viber://` link. Note the key is `action`; a `url` key is not supported and is ignored.\n"}}},"carousel":{"type":"object","description":"A carousel of cards. Send it on its own or with text - not with an image, video, file, button or list.\n","properties":{"items":{"type":"array","items":{"type":"object","properties":{"title":{"type":"string"},"image_url":{"type":"string","format":"uri","description":"Also accepted as `imageUrl`."},"primary_button":{"$ref":"#/components/schemas/CarouselButton"},"secondary_button":{"$ref":"#/components/schemas/CarouselButton"}}}}}},"list":{"type":"object","description":"A survey list. Send it on its own or with text - not with an image, video, file, button or carousel. `survey.options` must have at least one option.\n","properties":{"survey":{"type":"object","properties":{"options":{"type":"array","minItems":1,"items":{"type":"string"}}}}}},"ttl":{"type":"integer","description":"How long the message stays valid, in seconds."},"type":{"type":"string","description":"Accepted and ignored on Viber. It only matters if you send the same object to the SMS channel, where `\"text\"` marks it as convertible to plain text.\n"},"message_type":{"type":"string","description":"Accepted and ignored. Also accepted as `messageType`."}}},"CarouselButton":{"type":"object","description":"A button on a carousel card. The containing keys are also accepted as `primaryButton` / `secondaryButton`.\n","properties":{"label":{"type":"string"},"action_url":{"type":"string","format":"uri","description":"Also accepted as `actionUrl`."}}}}}}
```

## The ChannelEntry object

```json
{"openapi":"3.0.3","info":{"title":"Channels API","version":"1.0"},"components":{"schemas":{"ChannelEntry":{"type":"object","required":["channel","profile_id","to","content"],"description":"One channel to send on. Everything the send needs is on the entry itself - a missing field is a `400` naming it.\n","properties":{"channel":{"type":"string","description":"Channel name, e.g. `viber` or `sms`."},"profile_id":{"type":"string","format":"uuid","description":"The sender profile this channel sends from, by the UUID we issued for it. We fill in the sender from it, and it is what the message is attributed to in your reporting. A profile belongs to one channel, so each entry has its own. A UUID that matches no active profile of yours is rejected with `PROFILE_NOT_FOUND`.\n"},"to":{"type":"string","description":"Recipient phone number in international format."},"content":{"$ref":"#/components/schemas/Content"},"failover_after":{"type":"integer","minimum":1,"description":"How many seconds to wait for delivery on this channel before moving to the next entry. Only meaningful when there is a next entry.\n"}}},"Content":{"description":"For `sms`, the content is a plain text string. For rich channels like `viber` it is an object. The two are interchangeable for plain text: a string sent to Viber goes out as a text message, and `{\"type\": \"text\", \"text\": \"...\"}` sent to SMS goes out as that text.\n","oneOf":[{"type":"string","description":"Plain text."},{"$ref":"#/components/schemas/ViberContent"}]},"ViberContent":{"type":"object","description":"Viber message content. Not every combination of fields is a valid Viber message, so the ones that work are listed here in full. Anything else is rejected with `UNSUPPORTED_CONTENT`. There is a ready-made example of each under **Request samples** on both send endpoints.\n\n**These work:**\n\n| Content | With text | With a button | |---|---|---| | `text` | - | yes | | `media_url` (image) | yes | yes | | `video` | yes | yes | | `file` | no | no | | `carousel` | yes | no | | `list` | yes | no |\n\n**These do not:** an image and a video (or a file) together; a carousel and a list together; a file with anything else at all; a button on its own, with no text, image or video to attach it to; a button with no `caption`; a completely empty content object. `ttl` may accompany any of the working combinations.\n\nOnly fields you actually fill in are sent on; empty strings, empty objects and empty lists count as not set. Keys are snake_case, and the camelCase spelling noted per field also works.\n","properties":{"text":{"type":"string"},"media_url":{"type":"string","format":"uri","description":"Image URL. Also accepted as `mediaUrl`."},"video":{"type":"object","description":"A video. This is an object, not a plain URL string.","properties":{"media_url":{"type":"string","format":"uri","description":"Video URL. Also accepted as `mediaUrl`."},"thumbnail":{"type":"string","format":"uri"}}},"file":{"type":"object","description":"A file. This is an object, not a plain URL string. A file must be sent on its own - no text, button, image or video alongside it.\n","properties":{"media_url":{"type":"string","format":"uri","description":"File URL. Also accepted as `mediaUrl`."},"file_name":{"type":"string","description":"Also accepted as `fileName`."}}},"button":{"type":"object","required":["caption"],"description":"A button needs text, an image or a video alongside it, and it must have a caption.\n","properties":{"caption":{"type":"string","description":"The button label. Required."},"action":{"type":"string","description":"What the button opens - an `https://` or `viber://` link. Note the key is `action`; a `url` key is not supported and is ignored.\n"}}},"carousel":{"type":"object","description":"A carousel of cards. Send it on its own or with text - not with an image, video, file, button or list.\n","properties":{"items":{"type":"array","items":{"type":"object","properties":{"title":{"type":"string"},"image_url":{"type":"string","format":"uri","description":"Also accepted as `imageUrl`."},"primary_button":{"$ref":"#/components/schemas/CarouselButton"},"secondary_button":{"$ref":"#/components/schemas/CarouselButton"}}}}}},"list":{"type":"object","description":"A survey list. Send it on its own or with text - not with an image, video, file, button or carousel. `survey.options` must have at least one option.\n","properties":{"survey":{"type":"object","properties":{"options":{"type":"array","minItems":1,"items":{"type":"string"}}}}}},"ttl":{"type":"integer","description":"How long the message stays valid, in seconds."},"type":{"type":"string","description":"Accepted and ignored on Viber. It only matters if you send the same object to the SMS channel, where `\"text\"` marks it as convertible to plain text.\n"},"message_type":{"type":"string","description":"Accepted and ignored. Also accepted as `messageType`."}}},"CarouselButton":{"type":"object","description":"A button on a carousel card. The containing keys are also accepted as `primaryButton` / `secondaryButton`.\n","properties":{"label":{"type":"string"},"action_url":{"type":"string","format":"uri","description":"Also accepted as `actionUrl`."}}}}}}
```

## The Content object

```json
{"openapi":"3.0.3","info":{"title":"Channels API","version":"1.0"},"components":{"schemas":{"Content":{"description":"For `sms`, the content is a plain text string. For rich channels like `viber` it is an object. The two are interchangeable for plain text: a string sent to Viber goes out as a text message, and `{\"type\": \"text\", \"text\": \"...\"}` sent to SMS goes out as that text.\n","oneOf":[{"type":"string","description":"Plain text."},{"$ref":"#/components/schemas/ViberContent"}]},"ViberContent":{"type":"object","description":"Viber message content. Not every combination of fields is a valid Viber message, so the ones that work are listed here in full. Anything else is rejected with `UNSUPPORTED_CONTENT`. There is a ready-made example of each under **Request samples** on both send endpoints.\n\n**These work:**\n\n| Content | With text | With a button | |---|---|---| | `text` | - | yes | | `media_url` (image) | yes | yes | | `video` | yes | yes | | `file` | no | no | | `carousel` | yes | no | | `list` | yes | no |\n\n**These do not:** an image and a video (or a file) together; a carousel and a list together; a file with anything else at all; a button on its own, with no text, image or video to attach it to; a button with no `caption`; a completely empty content object. `ttl` may accompany any of the working combinations.\n\nOnly fields you actually fill in are sent on; empty strings, empty objects and empty lists count as not set. Keys are snake_case, and the camelCase spelling noted per field also works.\n","properties":{"text":{"type":"string"},"media_url":{"type":"string","format":"uri","description":"Image URL. Also accepted as `mediaUrl`."},"video":{"type":"object","description":"A video. This is an object, not a plain URL string.","properties":{"media_url":{"type":"string","format":"uri","description":"Video URL. Also accepted as `mediaUrl`."},"thumbnail":{"type":"string","format":"uri"}}},"file":{"type":"object","description":"A file. This is an object, not a plain URL string. A file must be sent on its own - no text, button, image or video alongside it.\n","properties":{"media_url":{"type":"string","format":"uri","description":"File URL. Also accepted as `mediaUrl`."},"file_name":{"type":"string","description":"Also accepted as `fileName`."}}},"button":{"type":"object","required":["caption"],"description":"A button needs text, an image or a video alongside it, and it must have a caption.\n","properties":{"caption":{"type":"string","description":"The button label. Required."},"action":{"type":"string","description":"What the button opens - an `https://` or `viber://` link. Note the key is `action`; a `url` key is not supported and is ignored.\n"}}},"carousel":{"type":"object","description":"A carousel of cards. Send it on its own or with text - not with an image, video, file, button or list.\n","properties":{"items":{"type":"array","items":{"type":"object","properties":{"title":{"type":"string"},"image_url":{"type":"string","format":"uri","description":"Also accepted as `imageUrl`."},"primary_button":{"$ref":"#/components/schemas/CarouselButton"},"secondary_button":{"$ref":"#/components/schemas/CarouselButton"}}}}}},"list":{"type":"object","description":"A survey list. Send it on its own or with text - not with an image, video, file, button or carousel. `survey.options` must have at least one option.\n","properties":{"survey":{"type":"object","properties":{"options":{"type":"array","minItems":1,"items":{"type":"string"}}}}}},"ttl":{"type":"integer","description":"How long the message stays valid, in seconds."},"type":{"type":"string","description":"Accepted and ignored on Viber. It only matters if you send the same object to the SMS channel, where `\"text\"` marks it as convertible to plain text.\n"},"message_type":{"type":"string","description":"Accepted and ignored. Also accepted as `messageType`."}}},"CarouselButton":{"type":"object","description":"A button on a carousel card. The containing keys are also accepted as `primaryButton` / `secondaryButton`.\n","properties":{"label":{"type":"string"},"action_url":{"type":"string","format":"uri","description":"Also accepted as `actionUrl`."}}}}}}
```

## The ViberContent object

```json
{"openapi":"3.0.3","info":{"title":"Channels API","version":"1.0"},"components":{"schemas":{"ViberContent":{"type":"object","description":"Viber message content. Not every combination of fields is a valid Viber message, so the ones that work are listed here in full. Anything else is rejected with `UNSUPPORTED_CONTENT`. There is a ready-made example of each under **Request samples** on both send endpoints.\n\n**These work:**\n\n| Content | With text | With a button | |---|---|---| | `text` | - | yes | | `media_url` (image) | yes | yes | | `video` | yes | yes | | `file` | no | no | | `carousel` | yes | no | | `list` | yes | no |\n\n**These do not:** an image and a video (or a file) together; a carousel and a list together; a file with anything else at all; a button on its own, with no text, image or video to attach it to; a button with no `caption`; a completely empty content object. `ttl` may accompany any of the working combinations.\n\nOnly fields you actually fill in are sent on; empty strings, empty objects and empty lists count as not set. Keys are snake_case, and the camelCase spelling noted per field also works.\n","properties":{"text":{"type":"string"},"media_url":{"type":"string","format":"uri","description":"Image URL. Also accepted as `mediaUrl`."},"video":{"type":"object","description":"A video. This is an object, not a plain URL string.","properties":{"media_url":{"type":"string","format":"uri","description":"Video URL. Also accepted as `mediaUrl`."},"thumbnail":{"type":"string","format":"uri"}}},"file":{"type":"object","description":"A file. This is an object, not a plain URL string. A file must be sent on its own - no text, button, image or video alongside it.\n","properties":{"media_url":{"type":"string","format":"uri","description":"File URL. Also accepted as `mediaUrl`."},"file_name":{"type":"string","description":"Also accepted as `fileName`."}}},"button":{"type":"object","required":["caption"],"description":"A button needs text, an image or a video alongside it, and it must have a caption.\n","properties":{"caption":{"type":"string","description":"The button label. Required."},"action":{"type":"string","description":"What the button opens - an `https://` or `viber://` link. Note the key is `action`; a `url` key is not supported and is ignored.\n"}}},"carousel":{"type":"object","description":"A carousel of cards. Send it on its own or with text - not with an image, video, file, button or list.\n","properties":{"items":{"type":"array","items":{"type":"object","properties":{"title":{"type":"string"},"image_url":{"type":"string","format":"uri","description":"Also accepted as `imageUrl`."},"primary_button":{"$ref":"#/components/schemas/CarouselButton"},"secondary_button":{"$ref":"#/components/schemas/CarouselButton"}}}}}},"list":{"type":"object","description":"A survey list. Send it on its own or with text - not with an image, video, file, button or carousel. `survey.options` must have at least one option.\n","properties":{"survey":{"type":"object","properties":{"options":{"type":"array","minItems":1,"items":{"type":"string"}}}}}},"ttl":{"type":"integer","description":"How long the message stays valid, in seconds."},"type":{"type":"string","description":"Accepted and ignored on Viber. It only matters if you send the same object to the SMS channel, where `\"text\"` marks it as convertible to plain text.\n"},"message_type":{"type":"string","description":"Accepted and ignored. Also accepted as `messageType`."}}},"CarouselButton":{"type":"object","description":"A button on a carousel card. The containing keys are also accepted as `primaryButton` / `secondaryButton`.\n","properties":{"label":{"type":"string"},"action_url":{"type":"string","format":"uri","description":"Also accepted as `actionUrl`."}}}}}}
```

## The CarouselButton object

```json
{"openapi":"3.0.3","info":{"title":"Channels API","version":"1.0"},"components":{"schemas":{"CarouselButton":{"type":"object","description":"A button on a carousel card. The containing keys are also accepted as `primaryButton` / `secondaryButton`.\n","properties":{"label":{"type":"string"},"action_url":{"type":"string","format":"uri","description":"Also accepted as `actionUrl`."}}}}}}
```

## The ErrorEntry object

```json
{"openapi":"3.0.3","info":{"title":"Channels API","version":"1.0"},"components":{"schemas":{"ErrorEntry":{"type":"object","properties":{"property":{"type":"string","description":"The request field that is wrong."},"error":{"type":"string","description":"What is wrong with it."}}}}}}
```

## The Problem object

```json
{"openapi":"3.0.3","info":{"title":"Channels API","version":"1.0"},"components":{"schemas":{"Problem":{"type":"object","description":"Every error, in one shape - `application/problem+json` (RFC 9457). There is not a different body per kind of failure, so you only need one parser.\n","required":["title","status","code"],"properties":{"title":{"type":"string","description":"Short human-readable summary of what went wrong. Meant for your logs, not for branching on - the wording may change.\n"},"status":{"type":"integer","description":"The HTTP status, repeated here so the body stands on its own."},"code":{"type":"string","description":"Stable machine-readable code. **This is the one to branch on** - it never changes for a given condition, while the status and title may be refined.\n","enum":["VALIDATION_FAILED","UNAUTHORIZED","OUT_OF_BALANCE","CLIENT_PRICE_NOT_FOUND","IP_NOT_ALLOWED","DISABLED_CONNECTION","DISABLED_CLIENT","CLIENT_ARCHIVED","DISABLED_ACCOUNT","DISABLED_PRODUCT","DISABLED_CLIENT_PRODUCT","CHANNEL_DISABLED","INVALID_SENDER","DESTINATION_COUNTRY_FORBIDDEN","UNKNOWN_CHANNEL","CHANNEL_NOT_CONFIGURED","CLIENT_PRODUCT_DOES_NOT_EXIST","PROFILE_NOT_FOUND","UNKNOWN_BRAND","NO_DNS_CONFIGURED","NOT_FOUND","METHOD_NOT_ALLOWED","UNSUPPORTED_MEDIA_TYPE","PAYLOAD_TOO_LARGE","UNSUPPORTED_CONTENT","REJECTED","TOO_MANY_REQUESTS","NO_ROUTES","INTERNAL_ERROR"]},"errors":{"type":"array","description":"Only on a `400`: one entry per offending field, so you can point at the exact problem rather than re-reading the whole request.\n","items":{"$ref":"#/components/schemas/ErrorEntry"}}}},"ErrorEntry":{"type":"object","properties":{"property":{"type":"string","description":"The request field that is wrong."},"error":{"type":"string","description":"What is wrong with it."}}}}}}
```

## The DeliveryReport object

```json
{"openapi":"3.0.3","info":{"title":"Channels API","version":"1.0"},"components":{"schemas":{"DeliveryReport":{"type":"object","description":"The delivery report we POST to your account's default webhook.","properties":{"type":{"type":"string","enum":["DLR"]},"message_id":{"type":"string","format":"uuid","description":"The `request_id` you got when the message was accepted."},"to":{"type":"string","description":"Recipient phone number."},"status":{"type":"string","enum":["DELIVERED","SEEN","UNDELIVERABLE"],"description":"`DELIVERED` and `UNDELIVERABLE` are final - the message is done, and every channel it could be tried on has been. `SEEN` is a read receipt and is not final; expect it in addition to, not instead of, a final report.\n"},"error":{"type":"string","description":"Result code - `000` for `DELIVERED` and `SEEN`, `001` for a generic failure, or a more specific code when the channel gives us one.\n"},"cost":{"type":"number","nullable":true,"description":"What this attempt actually charged, in your account currency - `0` if the message was never charged (e.g. an `UNDELIVERABLE` outcome on an account billed per delivery). May differ from the `cost` on the send response, which was only an indicative estimate for the first channel tried.\n"}}}}}}
```
