> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbit.devotel.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Generate a campaign draft from a brief

> Turn a plain-English campaign brief into a typed, editable campaign draft with POST /campaigns/from-brief: per-channel copy, audience criteria, send time, and a fallback chain, grounded in what your brief actually asked for.

# Generate a campaign draft from a brief

Authoring a multi-channel campaign means filling a dozen slots by hand: an SMS body under 160 characters, an email subject, preview text and body, a WhatsApp category, a fallback order, a send time. The **from-brief composer** collapses that into one plain-English sentence or two. You type what the campaign should do, `POST /campaigns/from-brief` returns a typed campaign draft, and the dashboard composer renders it into editable fields. Nothing is persisted and nothing is sent until you review the draft and commit it through the regular campaign creation flow.

This guide covers the request surface, the typed draft shape and why it is typed, the grounding pass that strips claims your brief never made, the limits and failure modes you can hit, and how this surface differs from the other AI authoring entry points.

## 1. What the from-brief composer does

Send a short natural-language brief, for example:

> Win back customers who have not ordered in 90 days. SMS first with email fallback, friendly tone, mention the 15% returning-customer discount.

The endpoint reads it and returns a structured draft carrying:

* **`name`**, **`description`**, **`goal`**: a short title, a one-to-two sentence summary for review, and the primary KPI the brief aims at ("reactivation", "CTR", "conversion").
* **`audience`**: a natural-language audience description plus the rationale for it. The description is written to feed `POST /contacts/segments/from-prompt`, which materialises the actual segment.
* **`channels`**: per-channel copy for `sms`, `rcs`, `whatsapp`, `email`, and `push`. Channels the brief does not need, or that you did not list as available, come back `null`.
* **`recommended_send_time`** and **`scheduled_at_iso`**: `immediate`, `optimal_per_recipient`, or `scheduled` with an RFC 3339 timestamp.
* **`fallback_chain`**: the failover sequence across channels, for example `["sms", "email"]`.
* **`ab_test`**: present only when your brief asks for an A/B test, with the two variants and the winner metric.
* **`warnings`**: anything you should know before launch, such as compliance notes, missing brand information, or a send time the composer could not resolve.

The response is a draft, not a campaign. The endpoint persists nothing: the dashboard composer renders every slot as an editable field, and you commit with `POST /campaigns` after review. Review-then-send is the contract on every AI authoring surface; generated output is a starting point, never a one-click launch.

## 2. Endpoint surface

```bash theme={null}
curl -X POST "https://api.orbit.devotel.io/api/v1/campaigns/from-brief" \
  -H "X-API-Key: $ORBIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "brief": "Win back customers who have not ordered in 90 days. SMS first with email fallback, friendly tone, mention the 15% returning-customer discount.",
    "available_channels": ["sms", "email"],
    "tone": "friendly",
    "region": "US"
  }'
```

| Field | Type | Required | Notes |
| - | - | - | - |
| `brief` | string | yes | Plain English, 8 to 4000 characters. |
| `available_channels` | string\[] | no | Subset of `sms`, `rcs`, `whatsapp`, `email`, `push`. The draft carries copy only for the channels you list; omit it and all five are eligible. Pass what you have actually provisioned so the draft never contains an RCS body you cannot send. |
| `tone` | string | no | Voice hint: "friendly", "formal", "playful". |
| `region` | string | no | Two-letter sender region. The draft honours that region's compliance norms and surfaces compliance warnings when you omit it. |
| `inside_journey` | boolean | no | Set `true` when the draft will run inside an existing journey: the composer then emits a single channel and an empty `fallback_chain`, because the journey already orchestrates failover. |
| `timezone` | string | no | Your IANA workspace timezone, for example `Europe/Berlin`. The dashboard forwards it automatically; direct API callers should pass it so relative-time briefs ("send tomorrow at 10:00") resolve against your local wall clock rather than UTC. |

The response envelope is `data.draft` (the typed shape from section 1) plus `data.model`, the model that served the request, and `data.usage`, the token accounting for the call, attributed to your tenant like any other AI spend.

## 3. Why the draft is a typed shape

The draft is a fixed JSON shape on purpose. Asked to "write a campaign", an open-ended model drifts into free-form prose that no form can render. A narrow, typed contract forces the output into exactly the slots the composer draws: a title, per-channel copy, a send time, a fallback order. Each slot has a type and a bound, so a well-formed response loads into the draft form with no re-typing, and a missing or off-shape slot shows up as a flagged field you edit, not as broken prose:

* SMS `body` is at most 160 characters, with no link tokens unless your brief asked for a link.
* RCS carries a `body` plus up to four quick-reply `suggestions`.
* WhatsApp carries a `template_intent`, a `body`, and a `category` restricted to Meta's vocabulary: `MARKETING`, `UTILITY`, or `AUTHENTICATION`.
* Email carries `subject`, `preview_text`, and a GitHub-flavored markdown `body_markdown`.
* Push carries a `title` of at most 50 characters and a `body` of at most 120.

Read `warnings` as part of the contract, not as noise: it is where the composer reports what it could not resolve, from an unresolvable send time to a compliance caveat for your region.

## 4. Grounding: what the composer refuses to invent

When the model writes copy it can tack on promotional claims your brief never made: urgency ("Limited time offer", "act now"), scarcity ("last chance", "while supplies last"), a discount you did not author ("50% off"), or a guarantee ("risk-free"). Unauthored claims are a carrier and consumer-protection liability you never signed off on, so before the draft reaches you the endpoint runs a deterministic grounding pass over every generated copy field, checked against your own brief:

* Any sentence that introduces a claim class your brief does not support is removed; the rest of the copy is kept. "Your order history qualifies you for the returning-customer rate. Limited time offer!" keeps the first sentence and loses the second when the brief never said "limited time".
* An RCS quick-reply label that is entirely an ungrounded claim is dropped from `suggestions` rather than rendered as an empty chip.
* A copy field that collapses to empty was entirely fabricated; the composer shows it as an empty, editable slot instead.

Reading a partially-grounded response:

* **A body minus one sentence** carried a fabricated claim. What remains is yours to edit, and if you genuinely want that claim, add it to the brief and regenerate: a claim present in the brief is grounded by definition and survives the pass.
* **An empty slot** means every sentence the model wrote for it was ungrounded. Treat it as "the model had nothing authorised to say here" and write the slot yourself.
* **Grounding checks claim classes, not facts.** It will strip "act now" but it cannot verify a statistic or a price. The review pass before launch is still yours.

## 5. Limits and failure modes

* **Draft-only output.** The endpoint never persists a campaign and never sends anything. A draft becomes a send only after you commit it with `POST /campaigns` and launch it through the normal lifecycle. The draft also makes no promise about deliverability pre-checks: run it through the same pre-launch checks as any authored content.
* **Send times are guarded.** A `scheduled_at_iso` in the past (or unparseable) is downgraded to `recommended_send_time: "immediate"`, the timestamp is dropped, and a warning is appended telling you to set the send time in the schedule step. Relative expressions ("tomorrow at 10:00", "next Monday") resolve against your workspace timezone when `timezone` is present, and against UTC when it is not, so direct API callers with local-time briefs should always pass it.
* **Brief too short.** Fewer than 8 characters is rejected with a 422 validation error before any generation runs.
* **Parse failure.** If the model returns something that is not the typed shape, you get `422 AI_PARSE_FAILED` with a preview of the raw output. Re-run with a clearer brief, or fall back to `POST /campaigns` with explicit fields.
* **Timeout and provider failure.** Generation runs under a time budget: a slow provider returns `504 LLM_TIMEOUT`, a transient upstream failure returns `502 LLM_UPSTREAM_ERROR`. Both are retryable; budget your retries rather than looping.
* **Spend and rate gates.** The endpoint sits on the same AI budget and rate limiting as the other generation surfaces. A spend cap, rate limit, or insufficient balance returns the same structured 402 / 429 / 403 / 503 envelope those surfaces return, with the reason and any retry window, and a `503` also covers a workspace with no AI provider configured.
* **Model routing.** Requests are served by your platform's configured model bucket; no model name is fixed in this flow, and `data.model` on the response names the model that served each request.

## 6. Worked briefs

Three briefs of increasing specificity, each with the draft shape they produce. Field values are illustrative of the shape; your copy will differ.

### Product launch, minimal brief

```json theme={null}
{
  "brief": "Announce our new loyalty wallet to all contacts, SMS with email fallback."
}
```

```json theme={null}
{
  "name": "Loyalty wallet launch",
  "description": "Single-blast announcement of the loyalty wallet, SMS first with email failover.",
  "goal": "CTR",
  "audience": {
    "description": "All contacts in the workspace.",
    "rationale": "The brief asks for an announcement to everyone; no behavioural filter."
  },
  "channels": {
    "sms": { "body": "Our loyalty wallet is live. Tap your profile in the app to open it and start earning on every order." },
    "rcs": null,
    "whatsapp": null,
    "email": {
      "subject": "Your loyalty wallet is live",
      "preview_text": "Open it in the app and start earning on every order.",
      "body_markdown": "## Your loyalty wallet is live\n\nOpen your profile in the app to activate the wallet. Every order now earns a balance you can redeem at checkout."
    },
    "push": null
  },
  "recommended_send_time": "immediate",
  "scheduled_at_iso": null,
  "fallback_chain": ["sms", "email"],
  "ab_test": null,
  "warnings": []
}
```

### Win-back with an authored offer and a send time

```json theme={null}
{
  "brief": "Win back customers who have not ordered in 90 days. Offer their 15% returning-customer discount. SMS first, email fallback, send tomorrow at 10:00.",
  "available_channels": ["sms", "email"],
  "region": "US",
  "timezone": "America/New_York"
}
```

```json theme={null}
{
  "name": "90-day win-back",
  "description": "Reactivate lapsed customers with their returning-customer discount, SMS first.",
  "goal": "reactivation",
  "audience": {
    "description": "Contacts whose most recent order is at least 90 days old.",
    "rationale": "The brief defines lapsed as 90 days without an order."
  },
  "channels": {
    "sms": { "body": "We saved your 15% returning-customer discount for your next order. Redeem it in the app this week." },
    "rcs": null,
    "whatsapp": null,
    "email": {
      "subject": "Your 15% discount is waiting",
      "preview_text": "A little something for coming back.",
      "body_markdown": "## We kept your discount\n\nYour 15% returning-customer discount is active on your account. Apply it at checkout on any order."
    },
    "push": null
  },
  "recommended_send_time": "scheduled",
  "scheduled_at_iso": "2026-10-16T14:00:00Z",
  "fallback_chain": ["sms", "email"],
  "ab_test": null,
  "warnings": []
}
```

The 15% discount survives grounding because the brief authored it. Add "ends tonight" to neither and the composer will strip it from the copy if the model invents it.

### Nurture series with an A/B test

```json theme={null}
{
  "brief": "Three-step nurture series for trial signups: welcome on day 0, tips on day 3, upgrade nudge on day 7, email only, A/B test two subject lines on the welcome email judged by open rate.",
  "available_channels": ["email"],
  "tone": "helpful"
}
```

```json theme={null}
{
  "name": "Trial nurture series",
  "description": "Three-touch email nurture for trial signups with a subject-line A/B test on the welcome step.",
  "goal": "conversion",
  "audience": {
    "description": "Contacts who started a trial, from the day they signed up.",
    "rationale": "The series is anchored to trial signup, so the segment keys off the signup event."
  },
  "channels": {
    "sms": null,
    "rcs": null,
    "whatsapp": null,
    "email": {
      "subject": "Welcome: three ways to get more from your trial",
      "preview_text": "Start here, then we will show you the rest.",
      "body_markdown": "## Welcome aboard\n\nHere are three ways to get value from your trial this week. We will follow up with practical tips and a look at what a full plan adds."
    },
    "push": null
  },
  "recommended_send_time": "optimal_per_recipient",
  "scheduled_at_iso": null,
  "fallback_chain": ["email"],
  "ab_test": {
    "variant_a": { "channel": "email", "field": "subject", "value": "Welcome: three ways to get more from your trial" },
    "variant_b": { "channel": "email", "field": "subject", "value": "Your trial, mapped out for the first week" },
    "winner_metric": "open_rate"
  },
  "warnings": [
    "The brief describes a three-step series; this draft carries the welcome step copy. Model the day-3 and day-7 touches as journey steps when you commit, or generate the orchestration with the journey builder."
  ]
}
```

Note the honest split of labour: from-brief drafts copy, while multi-step pacing (day 0 / 3 / 7 waits) is journey territory, and the draft says so in `warnings` rather than silently flattening the series into one blast.

## 7. How this differs from the adjacent surfaces

| Surface | Input | Output | Use it when |
| - | - | - | - |
| `POST /campaigns/from-brief` (this guide) | A campaign brief | A typed single-blast campaign draft: per-channel copy, audience criteria, send time, fallback chain | You want draft content for one campaign, ready to edit in the composer |
| [`POST /campaigns/journey/from-prompt`](/guides/campaign-journey-from-prompt) | A journey description | A `journeyDefinition` graph: nodes, edges, waits, branches | You want multi-step orchestration, not copy |
| [`POST /agents/from-prompt`](/guides/agent-from-prompt) | An agent description | A conversational AI agent draft | You want a bot, not a campaign |
| [Template-variable catalogue](/guides/campaigns-template-variable-catalogue) | Reference | The `{{first_name}}`-style merge fields any body can use | You personalise copy, authored or generated |

The boundaries in one line: from-brief drafts **copy** for one campaign, journey-from-prompt drafts the **orchestration** a series of sends runs on, and agents-from-prompt drafts a **conversational agent**. A journey whose steps need copy can take a from-brief draft's channel content into each step after review.

## See also

* [Create a campaign with the wizard](/guides/campaign-create-wizard): the commit path every draft, generated or hand-built, goes through.
* [Author a campaign journey from a natural-language prompt](/guides/campaign-journey-from-prompt): the journey-level sibling of this surface.
* [Template-variable catalogue](/guides/campaigns-template-variable-catalogue): the merge fields available inside generated copy.
* [LLM spend and the cost attribution model](/concepts/llm-spend-cost-attribution-model): how generation calls like this one are metered and attributed.
* [Campaigns API reference](/api-reference/endpoints/campaigns): full request and response shapes for every endpoint cited here.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.