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

# Create a campaign

`POST /v1/projectId/{projectId}/campaigns`

## Request

- Base URL: `https://api.bluefox.email`
- URL: `https://api.bluefox.email/v1/projectId/{projectId}/campaigns`
- Auth: HTTP bearer

subscriberListId and segmentId (if given) must belong to this same project - a valid ID from a different project 404s. Status "scheduled" requires a valid, parseable scheduledTo (a 400 otherwise), plus the project having usable sending credentials and enough account credit for the recipient count; missing any of these fails the whole create with a 400/405 rather than creating a draft.

## Path parameters

- `projectId` string, required

## Request body

- CampaignInput
  - `name` string, required
  - `subject` string, required
  - `previewText` string: Optional, but meaningfully affects open rates - if the user hasn't given you one, ask what they'd like it to say rather than leaving it blank.
  - `timeZone` string, required: IANA time zone, e.g. "America/New_York". Stored with the campaign and shown next to the scheduled time in the app; it does not shift scheduledTo, which must be an absolute instant.
  - `subscriberListId` string, required: Required. If the user hasn't told you which list to send to, call GET /v1/projectId/{projectId}/subscriber-lists first and ask them to choose from the real list names - don't ask them for a raw ID.
  - `segmentId` string: Optional - narrows the subscriberListId down further. If the user mentions targeting a specific segment by name but doesn't give an ID, call GET /v1/projectId/{projectId}/segments first and ask them to choose from the real names.
  - `status` 'draft' | 'archive' | 'scheduled': Set to "scheduled" together with a valid scheduledTo to schedule sending. Status "scheduled" without a parseable scheduledTo is rejected with a 400.
  - `scheduledTo` string, date-time: Absolute ISO 8601 date-time (with a UTC offset or trailing "Z"). Required when status is "scheduled".
  - `excludeUnengaged` boolean
  - `webVersionEnabled` boolean: Turns on the public web version ("view in browser" page) of the campaign. Defaults to false. The page only works while this is true and the status is "sent" or "archive".
  - `senderIdentity` string: A sender identity _id. If the user hasn't told you which one to use, call GET /v1/projectId/{projectId}/sender-identities first and ask them to choose from the real email addresses returned - don't ask them for a raw ID.
  - `replyTo` string
  - `type` 'html' | 'text': Omit to keep using the visual (Chamaileon) editor document. Set to "html" or "text" to author document as a raw Handlebars template string instead (see the document field).
  - `document` unknown
  - `feeds` object[]: RSS/Atom/JSON feeds to pull into this email - see variableName on each feed for how to reference it from document.
    - `url` string, required
    - `feedType` 'rss-xml' | 'json', required: "rss-xml" covers both RSS and Atom XML feeds - which one it actually is gets auto-detected from the feed content itself, which changes the array key inside the template (see variableName below).
    - `variableName` string, required: No spaces allowed. This becomes a top-level Handlebars variable in the document (see the `document` field on the parent email for the templating syntax). The feed's parsed items are NOT directly at `{{variableName}}` - they're nested under a feed-shape-dependent array key you loop over with the custom `each` helper: - feedType "rss-xml" that turns out to be RSS: `{{#each <variableName>.item}}...{{/each}}` (array key is the literal word "item", singular) - feedType "rss-xml" that turns out to be Atom: `{{#each <variableName>.entry}}...{{/each}}` (array key is "entry") - feedType "json": the array key is whatever field the source JSON actually uses for its list (e.g. "items", "articles") - inspect the feed's JSON to find it, it is not fixed. Inside the loop, each item's own fields are whatever the feed provides (e.g. RSS gives `{{this.title}}`, `{{this.link}}`, `{{this.pubDate}}`) - there is no fixed schema, it mirrors the feed's actual content one-to-one.
    - `maxItems` integer: Informational only - NOT enforced when the email is sent. To actually limit how many items render, pass limit (and optionally skip) as hash arguments on the each tag itself, e.g. `{{#each news.item limit=5 skip=0}}`.
    - `required` boolean
    - `availableFields` string[]: Informational only, for the app UI - not enforced or used when rendering.

## Response `201`

Created

- object
  - `status` integer
  - `result` Campaign
    - `_id` string
    - `name` string
    - `status` 'draft' | 'archive' | 'sent' | 'scheduled' | 'being-sent' | 'error'
    - `subject` string
    - `previewText` string
    - `timeZone` string
    - `subscriberListId` string
    - `segmentId` string, nullable
    - `scheduledTo` string, date-time
    - `senderIdentity` string
    - `replyTo` string
    - `type` 'chamaileon' | 'html' | 'text'
    - `excludeUnengaged` boolean
    - `webVersionEnabled` boolean
    - `feeds` object[]
      - `url` string, required
      - `feedType` 'rss-xml' | 'json', required: "rss-xml" covers both RSS and Atom XML feeds - which one it actually is gets auto-detected from the feed content itself, which changes the array key inside the template (see variableName below).
      - `variableName` string, required: No spaces allowed. This becomes a top-level Handlebars variable in the document (see the `document` field on the parent email for the templating syntax). The feed's parsed items are NOT directly at `{{variableName}}` - they're nested under a feed-shape-dependent array key you loop over with the custom `each` helper: - feedType "rss-xml" that turns out to be RSS: `{{#each <variableName>.item}}...{{/each}}` (array key is the literal word "item", singular) - feedType "rss-xml" that turns out to be Atom: `{{#each <variableName>.entry}}...{{/each}}` (array key is "entry") - feedType "json": the array key is whatever field the source JSON actually uses for its list (e.g. "items", "articles") - inspect the feed's JSON to find it, it is not fixed. Inside the loop, each item's own fields are whatever the feed provides (e.g. RSS gives `{{this.title}}`, `{{this.link}}`, `{{this.pubDate}}`) - there is no fixed schema, it mirrors the feed's actual content one-to-one.
      - `maxItems` integer: Informational only - NOT enforced when the email is sent. To actually limit how many items render, pass limit (and optionally skip) as hash arguments on the each tag itself, e.g. `{{#each news.item limit=5 skip=0}}`.
      - `required` boolean
      - `availableFields` string[]: Informational only, for the app UI - not enforced or used when rendering.
    - `errorMessage` string
    - `createdAt` string, date-time
    - `updatedAt` string, date-time

## Other responses

- `400`: Invalid request body or query - every violation found (missing/invalid/unexpected fields) is reported in one response, semicolon-separated, not just the first one hit.
- `403`: Missing or invalid API key

---

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