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.
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.
Create a campaign draft.
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.
Before you start
Get a Product API token with campaign write access
The endpoint requires the
PROJECT_CAMPAIGN_WRITEscope. Scopes are chosen when the token is minted, under Project settings → API tokens — see Authentication.Know your project UID
Every call is scoped to one project, which owns the audience, the segments and the senders the campaign will use.
Export credentials
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. 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 -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."
}
}'A fuller one — a segment, an explicit sender, a date and a tracked link:
{
"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.
{
"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 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. | Pass fallback.sender, or configure an SMS sender on the RCS sender. |
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.
channelstringrequiredsms or rcs. Immutable once the draft exists — a campaign cannot change channel afterwards. See Message content.
titlestringrequiredThe campaign name shown in the dashboard, 3–64 characters. Internal: recipients never see it.
emojistringAn emoji for the campaign card in the dashboard, 1–2 characters.
descriptionstringAn internal note about the campaign, up to 1000 characters.
datestringWhere 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.
timestringTime 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.
senderstringdefault: the project's default for the channelId of the sender to use. Must belong to this project. See Choosing a sender.
compliancestringdefault: the project's own policyConsent policy applied when the audience is resolved: basic, opt-out or opt-in. See Consent policy.
audienceobjectdefault: everyoneWho the campaign targets, as segment UIDs and inline filters. See Audience targeting.
includearraydefault: ["_all"]Segments and/or filters to target.
excludearraySegments and/or filters to remove from the target, applied after include.
messageobjectThe campaign copy, in one language. Omit it to create an empty draft for someone to write in the dashboard. See Message content.
textstringrequiredThe message body. Required whenever message is present.
languagestringTwo-letter lowercase code of the language this copy is written in. Required only with translateTo.
allowUnicodebooleanSMS only. Allow non-GSM characters. Ignored for RCS.
suggestionsarrayRCS only. Tappable buttons under the message, up to 4. Ignored for SMS.
fallbackobjectRCS only. An SMS for the contacts RCS cannot reach. See Falling back to SMS.
translateToarrayOther languages this campaign should eventually go out in, declared as empty. A paid feature. See 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 |
An SMS body has no fixed maximum here — length drives cost instead. See 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:
{
"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. |
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. |
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. |
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. |
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:
{
"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.