Message content
Write the copy a campaign draft carries: template variables and short links, the sender that speaks for it, SMS Unicode, RCS buttons, the SMS fallback for contacts RCS cannot reach, and how to declare languages to translate later.
channel decides the shape of everything on this page. A campaign is sms or rcs, the choice is made at creation and cannot change afterwards, and each channel accepts fields the other ignores: allowUnicode is meaningless for RCS, suggestions is meaningless for SMS, and only a channel that cannot reach every contact takes a fallback.
What both share is the core: one block of copy, in one language, and a sender to speak for it.
One block of copy
{
"message": {
"text": "Your order is ready for pickup at our downtown store."
}
}message.text is required whenever you send a message at all. You can also omit message entirely — the result is a valid draft with an audience, a sender and a date, and no copy, for someone to write in the dashboard. That is a legitimate way to use this endpoint: prepare the boring parts programmatically and leave the words to a human.
What this endpoint never does is invent copy. Nothing is generated, completed or translated on your behalf; a draft contains exactly the text you supplied.
Template variables
The text supports the same template variables as the rest of the platform. Two of them matter for almost every campaign:
| Variable | Effect |
|---|---|
{{short:https://example.com/page}} | Replaces the URL with a short link. |
{{unsubscribe}} | Inserts an unsubscribe link. |
A short link costs 10–15 characters instead of the full URL, and every tap on it is measurable in the campaign's reporting. In SMS, where length is billed, that is usually worth doing for every link you include.
{
"message": {
"text": "Summer sale starts today: 30% off everything.\nShop now: {{short:https://example.com/sale}}"
}
}Choosing a sender
sender is a top-level field, not part of message, and it takes the id of a sender that belongs to this project. An id from another project is refused with unknown-sender rather than silently ignored.
Omit it and the project's default sender for the channel applies. If the project has no default either, the draft is still created — with no sender, and a no-default-sender entry in metadata.warnings. The campaign cannot be sent until someone picks one in the dashboard, which is why that warning is worth surfacing in your own UI rather than dropping.
Whichever sender ends up on the campaign is also inherited by every language declared through translateTo that does not name its own.
SMS
An SMS body has no hard maximum in this API; length maps to cost instead.
allowUnicodebooleandefault: falseSMS only. Set it to true when the text may contain non-GSM-7 characters — accents, emoji, most non-Latin scripts. Ignored on RCS campaigns.
A GSM-7 part holds 160 characters; a Unicode part holds 70. Longer copy is split into several parts, and each part is billed as a message — so a 200-character GSM-7 message costs two, and the same text carrying one emoji costs three. That is simply the price of the copy you chose; a {{short:...}} link is the cheapest way to claw characters back.
{
"channel": "sms",
"title": "Flash sale for Spanish contacts",
"message": {
"text": "Solo hoy: 30% de descuento en toda la tienda.",
"allowUnicode": true
}
}RCS
An RCS campaign carries a text bubble of up to 3072 characters and, optionally, up to 4 tappable buttons under it. The richer RCS formats — cards, carousels, media — are not surfaced on this endpoint yet; a draft that needs them is finished in the dashboard.
suggestionsarrayRCS only. The buttons shown under the message, in order, up to 4.
typestringrequiredurl opens a link; dialer starts a phone call.
displayTextstringrequiredThe label the recipient sees, up to 25 characters.
urlstringRequired when type is url. A plain absolute URL — not wrapped in a template variable.
phoneNumberstringRequired when type is dialer. The number to call, in E.164 format.
{
"channel": "rcs",
"title": "Order pickup reminder",
"audience": {
"include": ["pending-pickups"]
},
"message": {
"text": "Your order is ready for pickup at our downtown store.",
"suggestions": [
{
"type": "url",
"displayText": "View order",
"url": "https://example.com/orders"
},
{
"type": "dialer",
"displayText": "Call the store",
"phoneNumber": "+34600000000"
}
]
}
}Two behaviours to know when you generate suggestions programmatically:
- Incomplete or unrecognised entries are dropped, not fatal. A
urlbutton with nourl, or a type this endpoint does not support, is left out and the rest of the draft is created. Count the buttons on the response if your caller needs to know what survived. - More than four accepted buttons is an error (
invalid-message). The cap is a channel limit, not a preference, so it fails loudly rather than truncating your list at an arbitrary point.
Falling back to SMS
RCS does not reach every contact: the handset and the network have to support it. A fallback covers the rest with an SMS, forming an rcs -> sms chain — the contacts RCS can reach get the rich message, everyone else gets the SMS.
fallbackobjectRCS only. Sending it on an SMS campaign is refused with fallback-not-supported: SMS already reaches everyone, so there is nothing to fall back to.
textstringrequiredThe SMS body. Required whenever fallback is present.
senderstringId of the SMS sender for this leg. Must belong to the project.
allowUnicodebooleanAllow non-GSM characters in the fallback SMS.
{
"channel": "rcs",
"title": "Order pickup reminder",
"audience": {
"include": ["pending-pickups"]
},
"message": {
"text": "Your order is ready for pickup at our downtown store.",
"suggestions": [
{
"type": "url",
"displayText": "View order",
"url": "https://example.com/orders"
}
]
},
"fallback": {
"text": "Your order is ready for pickup at our downtown store: {{short:https://example.com/orders}}"
}
}The fallback copy is required and is never derived from the RCS body. Beyond the standing rule that this endpoint writes no copy nobody supplied, there is a cost reason: an RCS body may run to 3072 characters, and pushing that into SMS would bill a long multi-part message nobody asked to send. Write the SMS version deliberately, short, with a short link.
Which sender the fallback uses
The SMS leg resolves its sender from the most specific answer available:
The explicit one
fallback.sender, when you send it.The one the RCS sender designates
The SMS sender configured on the RCS sender itself — whoever set up the RCS sender already chose which SMS speaks for it.
The project default
The project's default SMS sender.
If none of the three exists the draft is still created, with no-fallback-sender in metadata.warnings.
Declaring other languages
translateTo lists the other languages the campaign should eventually go out in, as two-letter lowercase codes:
{
"channel": "sms",
"title": "Product launch",
"audience": {
"include": ["newsletter-subscribers"]
},
"message": {
"language": "en",
"text": "Our new collection is live. Take a look: {{short:https://example.com/new}}"
},
"translateTo": ["fr", "de"]
}Each entry becomes a declared but empty language on the draft — a recorded intention to translate, not a translation. No copy is generated. Someone writes the French and the German in the dashboard, and until they do, the campaign cannot be estimated, quoted or prepared: an empty declared language is a deliberate gate, so a half-translated campaign can never be sent by accident.
message.language is the two-letter code of the copy you already wrote. Its only job is this de-duplication: without it, the language you supplied would be declared as one of the missing translations and the campaign would wait forever on a translation that already exists. That is why it is optional on its own and required whenever translateTo is used — and why it is not stored anywhere: it is a fact about your request, not about the campaign.
Errors
errorCode | Cause |
|---|---|
invalid-message | message was sent without text; a fallback was sent without text; more than 4 RCS buttons; translateTo without message.language; an unknown compliance value. |
unknown-sender | The sender id does not exist or belongs to another project. |
fallback-not-supported | A fallback was sent on a channel that reaches every contact. |
unknown-channel | channel is missing, or names a channel this endpoint cannot draft. |