---
title: "Create a campaign"
method: POST
path: "/campaigns"
tags: ["Campaigns"]
---

# Create a campaign

`POST /campaigns`

## Request

- Base URL: `https://warmysender.com/api/v1`
- URL: `https://warmysender.com/api/v1/campaigns`
- Auth: HTTP bearer

Creates a campaign in draft status with one or more steps, assigned mailboxes, and (optionally) enrolled prospects. Start it later with the start endpoint. Email, LinkedIn, WhatsApp, Instagram and mixed (multichannel) campaigns are all created here; mailboxes are only required when the campaign sends email. Creating a campaign never sends anything — every message is paced later by the scheduler within that account's safe limits.

Required scope: `campaigns:write`.

Requires a unique `Idempotency-Key` request header so retries never duplicate.

## Headers

- `Idempotency-Key` string, required

## Request body

- object
  - `name` string, required: Campaign name.
  - `description` string: Optional description.
  - `channel` 'email' | 'linkedin' | 'whatsapp' | 'instagram' | 'multichannel': Campaign channel. A single-channel campaign may only carry steps of that channel (plus condition steps); mix channels only under "multichannel".
  - `linkedinAccountId` string, uuid: LinkedIn account id (for linkedin/multichannel).
  - `instagramAccountId` string: Instagram account that sends this campaign. Required when the campaign carries Instagram steps. Needs an active Instagram seat.
  - `whatsappAccountId` string: WhatsApp number that sends this campaign. Required when the campaign carries WhatsApp steps. Needs an active WhatsApp seat.
  - `timezone` string: Timezone the sending window and days are read in, as a name like "Europe/Paris" or "America/New_York". Leave it out to use your workspace timezone (Settings → Workspace), which is UTC unless you changed it.
  - `dailySendLimit` integer: Daily send cap for the campaign.
  - `sendingWindowStart` integer: Start hour (0–23) of the daily sending window.
  - `sendingWindowEnd` integer: End hour (0–23) of the daily sending window.
  - `scheduleDays` integer[]: Days of the week this campaign may send on. Numbers use 0=Sunday, 1=Monday, 2=Tuesday, 3=Wednesday, 4=Thursday, 5=Friday, 6=Saturday (Sunday is 0, not Monday). Day names such as "mon" or "monday" are accepted too and are the safest choice. Order and repeats do not matter. Leave it out for Monday–Friday [1,2,3,4,5]; an empty list is refused because a campaign with no sending days could never send.
  - `stopOnReply` boolean: Stop a prospect's sequence when they reply.
  - `stopOnBounce` boolean: Stop on bounce.
  - `stopOnUnsubscribe` boolean: Stop on unsubscribe.
  - `trackOpens` boolean: Track opens.
  - `trackClicks` boolean: Track clicks.
  - `opt_out_style` 'link' | 'reply', nullable: This applies to THIS campaign only — useful when one campaign writes to people in a different language from the rest of your outreach. Leave it out and the campaign keeps using your workspace setting; send null (or an empty string) to go back to following it. The choice itself works the same way it does for your workspace: How people opt out of your cold email. "link": The footer carries an Unsubscribe link — on your own verified tracking domain when you have one, otherwise on a shared WarmySender domain. "reply": The footer reads “Reply "unsubscribe" to opt out” and points back at the sending mailbox — no third-party link at all. Either way, a reply asking to unsubscribe is honored automatically and that person is never contacted again. Writing your own sentence? Put the word in quotes — like Reply “stop” — and a reply of just that word is honored too.
  - `opt_out_text` string, nullable: This applies to THIS campaign only — useful when one campaign writes to people in a different language from the rest of your outreach. Leave it out and the campaign keeps using your workspace setting; send null (or an empty string) to go back to following it. The wording rules are the same as your workspace setting: The sentence recipients click or reply to in order to opt out — written in your own words, 4–120 characters, one line. Send an empty string to go back to the default for your opt-out style ("Unsubscribe" for link, "Reply "unsubscribe" to opt out" for reply). Put {{opt_out}} inside an email or signature to place the sentence yourself.
  - `company_address` string, nullable: The postal address shown in the footer of THIS campaign's emails — useful when you run campaigns for several companies from one workspace. Leave it out and the campaign keeps using your workspace address; send null (or an empty string) to go back to it.
  - `startDate` string, date-time: Optional scheduled start.
  - `endDate` string, date-time: Optional end date.
  - `steps` object[], required: Ordered steps. Each: { stepIndex, type (email|linkedin|instagram|whatsapp|condition), subject, bodyHtml, bodyText, delayDays, delayHours, isActive, … }. See the steps[] fields below for what an email body may contain and how to reuse a saved template. Condition steps additionally take the four condition fields below.
  - `steps[].subject` string: Subject line of an email step. Merge tags and spintax work here. Optional when the step reuses a template that carries one.
  - `steps[].preheader` string: Preview text shown after the subject line in the inbox.
  - `steps[].bodyHtml` string: HTML email body. Use simple HTML: <p>, <br>, <strong>, <em>, <u>, <a href>, <ul>, <ol>, <li>, <h2>, <h3>, <blockquote>, <hr>, <img src>. Inline styles are supported for font-family, font-size, color, background-color, line-height, margin-left and text-align — the same formatting the WarmySender editor produces. Plain text is fine too: send bodyText (or plain body_html without tags) and line breaks are formatted automatically. Scripts and event handlers are removed. Merge tags like {{first_name | default: 'there'}} and spintax {a|b|c} work in both. Keep formatting light: plain-looking emails get more replies.
  - `steps[].bodyText` string: Plain-text version of the email. Optional — when you send only HTML, the plain-text alternative is written for you. Merge tags like {{first_name | default: 'there'}} and spintax {a|b|c} work here too.
  - `steps[].templateId` string: Reuse one of your saved email templates for this step: its subject, preview text and body are copied in. Anything you also set on the step overrides the template's version. An unknown id (or one from another workspace) fails the whole request with not_found.
  - `steps[].quotePreviousMessages` boolean: Quote the earlier email underneath a threaded follow-up, the way a hand-written reply does. Default true; only applies to follow-up steps that reply in the same thread.
  - `steps[].instagramActionType` 'follow' | 'view_profile' | 'like_post' | 'comment_post' | 'send_dm' | 'wait' | 'wait_reply' | 'condition_replied': Action for a step of type "instagram".
  - `steps[].instagramDmTemplate` string: DM body for a send_dm step (supports {{firstName}} etc).
  - `steps[].instagramCommentTemplate` string: Comment body for a comment_post step.
  - `steps[].whatsappActionType` 'send_message' | 'wait' | 'wait_reply' | 'condition_replied': Action for a step of type "whatsapp". WhatsApp outreach is reply-first: send_message is the only outbound action.
  - `steps[].whatsappMessageTemplate` string: Message body for a send_message step (supports {{firstName}} etc).
  - `steps[].conditionType` 'email_opened' | 'email_replied' | 'email_clicked' | 'email_bounced' | 'linkedin_accepted' | 'linkedin_replied' | 'has_linkedin_profile' | 'timeout': Required on a step of type "condition": the signal the branch evaluates. Any other value is rejected with 400 validation_error.
  - `steps[].conditionTrueNext` integer: Required on a condition step: stepIndex to jump to when the condition is met. Use -1 to end the sequence on the YES branch. Must point forward.
  - `steps[].conditionFalseNext` integer, nullable: stepIndex to jump to when the condition is NOT met. Omit (or use -1) to end the sequence on the NO branch — a first-class outcome, not an error. Must point forward.
  - `steps[].conditionTimeoutDays` integer: How long the condition waits before the NO branch is taken. Defaults to 3 days when omitted.
  - `mailboxIds` string[], required: Mailbox ids that send this campaign.
  - `prospectIds` string[]: Prospect ids to enroll on creation.

## Response `201`

Success

## Other responses

- `400`: `validation_error` — Body / step validation failed — including a conditionType outside the allowed list.; `unsupported_field` — A step supplied "conditions" (multi-condition AND/OR logic). Author those in the campaign builder; the API returns them read-only. Also returned when the opt-out setting was sent as optOutStyle / optOutText — use opt_out_style / opt_out_text, the same names your workspace settings use. Nothing was created.; `campaign_step_validation_error` — A step failed sequence-integrity checks (e.g. a branch pointing backward or at a step that does not exist).
- `401`: `unauthorized` — Missing or invalid API key.
- `403`: `subscription_blocked` — Your plan doesn't allow this action.; `insufficient_scope` — The API key lacks the scope this endpoint requires.
- `429`: `rate_limited` — Too many requests — slow down and retry after the Retry-After delay.

---

[API](https://skmtc.dev/warmysender/apis/warmysender-public-api.md) · [All operations](https://skmtc.dev/warmysender/apis/warmysender-public-api/llms.txt) · [OpenAPI document](https://skmtc.dev/warmysender/apis/warmysender-public-api/revisions/eb5c7f10a1cd?raw)
