# 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.

**Language:** en
**Audience:** developer
**TLDR:** message.text is the copy and is required whenever message is sent; allowUnicode is SMS-only and suggestions (max 4, displayText max 25) are RCS-only. Use {{short:url}} in the text for tracked short links, but never around a suggestion URL — those are shortened by the platform. fallback is RCS-only and its text is required. translateTo declares empty languages, needs message.language, and is a paid feature.
**Search keywords:** message, text, copy, template, short link, shortener, unsubscribe, sender, unicode, GSM, RCS, suggestions, buttons, quick reply, fallback, translation, multi-language, translateTo
**Related pages:** /developers/product-api/campaigns/creating-a-draft, /developers/product-api/campaigns/overview, /developers/product-api/campaigns/audience
**Docs index (every page):** https://docs.instasent.com/llms.txt
**This zone's index:** https://docs.instasent.com/developers/product-api/llms-full.txt
**This page:** https://docs.instasent.com/developers/product-api/campaigns/message/ (HTML) · https://docs.instasent.com/developers/product-api/campaigns/message.md (Markdown)

`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

```json
{
  "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.

```json
{
  "message": {
    "text": "Summer sale starts today: 30% off everything.\nShop now: {{short:https://example.com/sale}}"
  }
}
```

> **Warning**: **Do not wrap a suggestion URL in `{{short:...}}`.** The rule applies to the message *text*. URLs inside RCS `suggestions` are shortened and tracked by the platform on their own when the message is prepared — wrapping them yourself shortens an already-shortened link.

## 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.

- `allowUnicode` — `boolean`, default: `false`
  **SMS 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.

```json
{
  "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.

- `suggestions` — `array`
  **RCS only.** The buttons shown under the message, in order, up to 4.
  
  - `type` — `string`, required
    `url` opens a link; `dialer` starts a phone call.
  - `displayText` — `string`, required
    The label the recipient sees, up to 25 characters.
  - `url` — `string`
    Required when `type` is `url`. A plain absolute URL — not wrapped in a template variable.
  - `phoneNumber` — `string`
    Required when `type` is `dialer`. The number to call, in E.164 format.

```json
{
  "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 `url` button with no `url`, 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.

- `fallback` — `object`
  **RCS 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.
  
  - `text` — `string`, required
    The SMS body. Required whenever `fallback` is present.
  - `sender` — `string`
    Id of the SMS sender for this leg. Must belong to the project.
  - `allowUnicode` — `boolean`
    Allow non-GSM characters in the fallback SMS.

```json
{
  "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:

#### 1. The explicit one

`fallback.sender`, when you send it.

#### 2. 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.

#### 3. 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:

```json
{
  "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.

> **Warning**: Multi-language campaigns are a **paid feature**. A request that declares languages without a subscription that includes it returns `409 Conflict` and creates nothing at all — there is no partial draft to clean up.

## 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.                                                                                                       |

## What's next

- [Dates and scheduling](/developers/product-api/campaigns/scheduling) - Where the campaign sits on the calendar — and why nothing is scheduled here.
- [Audience targeting](/developers/product-api/campaigns/audience) - Who receives what you just wrote.
- [Creating a draft](/developers/product-api/campaigns/creating-a-draft) - Back to the endpoint contract, limits and error codes.

---

This is one page of the Instasent documentation. For the complete machine-readable index of every guide and API reference, fetch https://docs.instasent.com/llms.txt — start there for full context.
