# Creating a draft

The POST contract for campaign drafts: what the smallest useful request looks like, the full request body field by field, the warnings the response carries, the limits and the error codes an integration has to handle.

**Language:** en
**Audience:** developer
**TLDR:** POST /v1/project/{project}/campaign creates a DRAFT and nothing else, and there is no edit counterpart: re-posting creates a second campaign. Only channel and title are required; everything else defaults (the whole audience, the project's default sender, no date). The 201 returns the campaign under entity and non-fatal findings under metadata.warnings, where no-default-sender means the draft cannot be sent or even estimated until someone picks a sender.
**Search keywords:** create campaign, campaign draft, draft campaign, POST campaign, new campaign, campaign request body, campaign fields, campaign limits, campaign errors, errorCode, warnings, no-default-sender, PROJECT_CAMPAIGN_WRITE
**Related pages:** /developers/product-api/campaigns/overview, /developers/product-api/campaigns/audience, /developers/product-api/campaigns/message
**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/creating-a-draft/ (HTML) · https://docs.instasent.com/developers/product-api/campaigns/creating-a-draft.md (Markdown)

One call assembles a whole campaign: pick the channel, describe who it targets, write the copy, and put it on the calendar. What comes back is a **draft**, ready for a human to review and send from the dashboard.

[`POST /v1/project/{project}/campaign` - Create a campaign draft.](/developers/product-api/reference)

Nothing on this page arms, schedules or sends anything, and there is no update endpoint to follow it with. Both properties are explained in [Campaigns](/developers/product-api/campaigns/overview#drafts-only-and-no-edit-counterpart).

## Before you start

#### 1. Get a Product API token with campaign write access

The endpoint requires the `PROJECT_CAMPAIGN_WRITE` scope. Scopes are chosen when the token is minted, under **Project settings** → **API tokens** — see [Authentication](/developers/product-api/authentication).

#### 2. Know your project UID

Every call is scoped to one project, which owns the audience, the segments and the senders the campaign will use.

#### 3. Export credentials

```bash
export INSTASENT_PROJECT="proj_xxx"
export INSTASENT_TOKEN="eyJhbGciOi..."
export BASE="https://api.instasent.com/v1/project/$INSTASENT_PROJECT"
```

The endpoint sits in the **medium** rate-limit class, and its ceiling scales with the organization's subscription plan — see [Rate limits](/developers/product-api/rate-limits). Campaign creation is a low-frequency operation by nature; if you are anywhere near the ceiling, something is looping.

## Your first draft

The smallest useful request is a channel, a title and some copy. Everything else has a default: the audience becomes the whole audience, the sender becomes the project's default for the channel, and the campaign sits on the calendar undated.

#### curl

```bash
curl -X POST "$BASE/campaign" \
  -H "Authorization: Bearer $INSTASENT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "channel": "sms",
    "title": "Service notice",
    "message": {
      "text": "We are performing maintenance this Sunday. Service may be briefly unavailable."
    }
  }'
```

#### node

```js
const response = await fetch(`${BASE}/campaign`, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.INSTASENT_TOKEN}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    channel: "sms",
    title: "Service notice",
    message: {
      text: "We are performing maintenance this Sunday. Service may be briefly unavailable.",
    },
  }),
});

const { entity, metadata } = await response.json();
```

A fuller one — a segment, an explicit sender, a date and a tracked link:

```json
{
  "channel": "sms",
  "title": "Summer sale announcement",
  "emoji": "☀️",
  "date": "2026-06-15T09:30:00+02:00",
  "sender": "67bdfa983114d0062d733795",
  "compliance": "opt-out",
  "audience": {
    "include": ["vip-customers"],
    "exclude": ["recently-messaged"]
  },
  "message": {
    "text": "Summer sale starts today: 30% off everything.\nShop now: {{short:https://example.com/sale}}",
    "allowUnicode": false
  }
}
```

## The response

A successful call returns **`201 Created`** with the campaign under `entity` and, alongside it, anything the platform noticed while building the draft.

```json
{
  "entity": {
    "id": "68b0f2a4e5a6b7c8d9e0f1a2",
    "title": "Summer sale announcement",
    "status": "draft"
  },
  "metadata": {
    "warnings": []
  }
}
```

`entity` is the campaign as the campaign read endpoints return it — the example above is trimmed to the fields you are most likely to store. Keep the `id`: it is what the [campaign analytics](/developers/product-api/analytics/overview#campaign-report) endpoints take once the campaign has been sent.

### Warnings

`metadata.warnings` reports **non-fatal** findings. The draft was created; something in it is incomplete and a human has to close the gap before the campaign can go out.

| Warning              | What it means                                                                                              | What to do                                                                                                   |
| -------------------- | ---------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
| `no-default-sender`  | The project has no default sender for the channel, so the draft has none.                                  | The campaign cannot be sent until a sender is chosen in the dashboard. Pass `sender` explicitly to avoid it. |
| `no-fallback-sender` | Same, for the SMS leg of an [RCS fallback](/developers/product-api/campaigns/message#falling-back-to-sms). | Pass `fallback.sender`, or configure an SMS sender on the RCS sender.                                        |

> **Tip**: Treat a non-empty `warnings` array as a signal to surface in your own UI. A draft that arrives with `no-default-sender` looks complete and silently cannot be sent — telling the user at creation time is much cheaper than letting them discover it at the review step.
> 
> These two warnings are also the best predictor you get of a later failure: a draft missing a sender is not only unsendable, it is **unestimable**, and the `409` it earns at [estimate](/developers/product-api/campaigns/estimating-and-deleting#estimating-a-draft) was already announced here in the `201`.

## The request body

Only `channel` and `title` are required. The shape is deliberately flat: `channel` decides which of the channel-specific message fields apply, and fields belonging to the other channel are ignored.

- `channel` — `string`, required
  `sms` or `rcs`. Immutable once the draft exists — a campaign cannot change channel afterwards. See [Message content](/developers/product-api/campaigns/message).
- `title` — `string`, required
  The campaign name shown in the dashboard, 3–64 characters. Internal: recipients never see it.
- `emoji` — `string`
  An emoji for the campaign card in the dashboard, 1–2 characters.
- `description` — `string`
  An internal note about the campaign, up to 1000 characters.
- `date` — `string`
  Where the campaign sits on the dashboard calendar. Accepts a day, a day and time, or a full ISO 8601 datetime with an offset — **and nothing is scheduled by setting it**. See [Dates and scheduling](/developers/product-api/campaigns/scheduling).
- `time` — `string`
  Time of day for the calendar anchor, `HH:MM`, for callers holding the day and the time apart. Rejected alongside a `date` that already carries a time. See [Dates and scheduling](/developers/product-api/campaigns/scheduling).
- `sender` — `string`, default: `the project's default for the channel`
  Id of the sender to use. Must belong to this project. See [Choosing a sender](/developers/product-api/campaigns/message#choosing-a-sender).
- `compliance` — `string`, default: `the project's own policy`
  Consent policy applied when the audience is resolved: `basic`, `opt-out` or `opt-in`. See [Consent policy](/developers/product-api/campaigns/audience#consent-policy).
- `audience` — `object`, default: `everyone`
  Who the campaign targets, as segment UIDs and inline filters. See [Audience targeting](/developers/product-api/campaigns/audience).
  
  - `include` — `array`, default: `["_all"]`
    Segments and/or filters to target.
  - `exclude` — `array`
    Segments and/or filters to remove from the target, applied after `include`.
- `message` — `object`
  The campaign copy, in one language. Omit it to create an empty draft for someone to write in the dashboard. See [Message content](/developers/product-api/campaigns/message).
  
  - `text` — `string`, required
    The message body. Required whenever `message` is present.
  - `language` — `string`
    Two-letter lowercase code of the language this copy is written in. Required only with `translateTo`.
  - `allowUnicode` — `boolean`
    **SMS only.** Allow non-GSM characters. Ignored for RCS.
  - `suggestions` — `array`
    **RCS only.** Tappable buttons under the message, up to 4. Ignored for SMS.
- `fallback` — `object`
  **RCS only.** An SMS for the contacts RCS cannot reach. See [Falling back to SMS](/developers/product-api/campaigns/message#falling-back-to-sms).
- `translateTo` — `array`
  Other languages this campaign should eventually go out in, declared as empty. A paid feature. See [Declaring other languages](/developers/product-api/campaigns/message#declaring-other-languages).

## Limits

| Field                       | Limit                       |
| --------------------------- | --------------------------- |
| `title`                     | 3–64 characters             |
| `emoji`                     | 1–2 characters              |
| `description`               | up to 1000 characters       |
| `message.text` (RCS)        | up to 3072 characters       |
| `message.suggestions`       | up to 4 buttons             |
| `suggestions[].displayText` | up to 25 characters         |
| `date`                      | less than one year from now |
| unsent drafts per project   | 50                          |

> **Note**: The 3-character minimum on `title` is the one that catches integrations out: a campaign named after a short code or a two-letter market (`ES`, `Q3`) is rejected. Give it a human title and keep your own reference in `description`.

An SMS body has no fixed maximum here — length drives cost instead. See [SMS](/developers/product-api/campaigns/message#sms) for how characters map to billed parts.

## Errors

Validation failures come back as **`422 Unprocessable Entity`** with a stable machine-readable code, so a caller can react to the reason rather than parse a sentence:

```json
{
  "errors": {
    "fields": {
      "errorCode": ["unknown-segment"]
    }
  }
}
```

| `errorCode`               | What happened                                                                                                                                                            | What to do                                                                                                                                      |
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| `unknown-channel`         | `channel` is missing, or names a channel this endpoint cannot draft.                                                                                                     | Send `sms` or `rcs`.                                                                                                                            |
| `channel-not-available`   | The channel is not available on this project.                                                                                                                            | Check the project's channels before offering it as an option.                                                                                   |
| `unknown-segment`         | A segment UID in `audience` does not exist in this project, or an audience list is not a list of UIDs and filters.                                                       | Read the project's segments and use their UIDs. See [Audience targeting](/developers/product-api/campaigns/audience).                           |
| `invalid-audience-filter` | An inline audience filter is not a valid filter, has no conditions, or names an unknown operator or attribute.                                                           | Fix the filter — the message names the offending part. See [Writing the filter](/developers/product-api/campaigns/audience#writing-the-filter). |
| `unknown-sender`          | The `sender` id does not exist or belongs to another project.                                                                                                            | Use a sender id from this project, or omit the field to take the default.                                                                       |
| `fallback-not-supported`  | A `fallback` was sent on a channel that reaches every contact.                                                                                                           | Drop `fallback` on SMS campaigns.                                                                                                               |
| `invalid-message`         | The message could not be built: unknown consent policy, an RCS message with more than 4 buttons, a `fallback` with no copy, or `translateTo` without `message.language`. | See [Message content](/developers/product-api/campaigns/message).                                                                               |
| `invalid-date`            | The date or time was not understood, or a `time` was sent alongside a date that already carries one.                                                                     | See [Dates and scheduling](/developers/product-api/campaigns/scheduling).                                                                       |

Field-level validation errors — a `title` under 3 characters, a date beyond the one-year horizon — use the same `422` envelope keyed by the field name instead of `errorCode`:

```json
{
  "errors": {
    "fields": {
      "title": ["This value is too short."]
    }
  }
}
```

Two conditions produce a **`409 Conflict`**, and in both cases nothing is created. Multi-language campaigns are a paid feature, so a request that declares languages without the subscription to match is refused. And a project that already holds **50 campaigns sitting in `draft`** takes no more: the ceiling is checked before any other work, and it exists so an automated caller cannot fill a project with drafts in a loop. Only the raw `draft` status counts towards it, so a draft that has been estimated no longer does. Everything else — `401`, `403`, `429`, `5xx` — behaves as described in [Errors](/developers/product-api/errors).

## What's next

- [Audience targeting](/developers/product-api/campaigns/audience) - Segments, inline filters and how to count the reach before you create anything.
- [Message content](/developers/product-api/campaigns/message) - Copy, template variables, senders, RCS buttons, SMS fallback and languages.
- [Estimating and deleting](/developers/product-api/campaigns/estimating-and-deleting) - What you can do to the draft once it exists: price it, or remove it.

---

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.
