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

# Create a draft campaign

`POST /business/campaigns`

## Request

- Base URL: `https://api.tryfuse.ai/api/v1`
- URL: `https://api.tryfuse.ai/api/v1/business/campaigns`
- Auth: HTTP basic

Creates a draft campaign in status `initializing`; nothing is sent until you call POST /business/campaigns/{campaignId}/approve. `campaignType: manual` (the default) scaffolds one empty step per entry in `channels` immediately (sequenceNumber 0, 1, 2 ...), so you can list them with GET .../steps and write subject/body with PATCH .../steps/{stepId}. `campaignType: ai-generated` returns immediately and runs prompt analysis and topic generation in the background: poll GET /business/campaigns/{campaignId} until `initializationStatus` is `completed` (or `failed`, with the reason in `initializationError`) before editing or approving. Passing `templateId` copies that template's steps and channels for either type. The campaign is owned by the token owner. No credits are charged by this call. Rate limit: 30 requests/minute and 1,000/day per token owner, shared by every campaign, scheduled-send, campaign-template and knowledge-hub endpoint.

Errors:
- `404` `LIST_NOT_FOUND` — A listId/listIds entry does not exist, is archived, or belongs to another team.
- `404` `TEMPLATE_NOT_FOUND` — templateId does not exist or is not visible to you.
- `404` `WRITING_STYLE_NOT_FOUND` — writingStyleId (ai-generated only) is not one of your writing styles.
- `404` `KNOWLEDGE_HUB_NOT_FOUND` — knowledgeHubId (ai-generated only) is not one of your non-archived knowledge hubs.
- `422` `INVALID_CAMPAIGN_CHANNELS` — More than one linkedin_connection step, or a linkedin_connection after a linkedin_message (also evaluated on the template's steps when templateId is given).
- `422` `LIST_INCOMPATIBLE_WITH_CHANNELS` — The source lists are empty, or no contact carries an email address (email/inmail) or a LinkedIn URL (LinkedIn channels) for the requested channels.
- `422` `EXCLUDE_COMPANY_LISTS_INVALID` — An excludeCompanyLists entry is not a non-archived company or people list owned by your team.
- `422` `VALIDATION_FAILED` — The body failed schema validation (unknown key, both or neither of listId/listIds, channels together with templateId, an AI-only field on a manual campaign, an unrecognised timezone, a non-http(s) ctaLink); `param` names the field.
- `429` `RATE_LIMITED` — More than 30 campaign requests in a minute, or 1,000 in a day, for this token owner (the bucket is shared by every /business/campaigns, /business/scheduled-sends, /business/campaign-templates and /business/knowledge-hubs call).
- `500` `UNEXPECTED_ERROR` — An unhandled failure; the detail stays in our logs and is never returned.

Requires one of the following token scopes: campaigns.

## Request body

- object
  - `campaignName` string, required: Display name of the campaign (1-200 characters, trimmed); also the stem of the CSV export filename.
  - `listId` string: Id of the single Fuse list the campaign sources contacts from; mutually exclusive with listIds (exactly one of the two is required). The list must belong to your team and not be archived, else 404 LIST_NOT_FOUND.
  - `listIds` string[]: Ids of 1-10 Fuse lists the campaign sources; mutually exclusive with listId. Every list must belong to your team and not be archived, else 404 LIST_NOT_FOUND. Take the ids from GET /business/lists with entityType=contactList — a campaign enrolls contacts. The lists are OR-joined into a deduplicated union: they are read in the order given and a contact that sits in two of them is enrolled once, up to the campaign's 100000-contact cap, after which the remaining lists are skipped.
  - `channels` string[]: Ordered channel scaffold: one empty sequence step (sequenceNumber 0, 1, 2 ...) is created per entry, 1-10 entries, default ["email"]. At most one linkedin_connection, and it must precede every linkedin_message (422 INVALID_CAMPAIGN_CHANNELS). Forbidden together with templateId, whose steps decide the channels.
  - `campaignStartDate` string: ISO 8601 date-time before which nothing is sent (default: now); evaluated in timezone together with emailDays.
  - `timezone` string: Time zone in which the daily sending window (09:00-17:00 local) and emailDays are evaluated; default UTC. Accepted values are IANA time zone names, for example America/New_York, Europe/Berlin, Asia/Kolkata or UTC. Do not send an abbreviation: EST is in the tz database but resolves to a fixed UTC-5 zone that never observes daylight saving, so name the region instead. A name the tz database does not know is rejected rather than silently evaluated as UTC.
  - `emailDays` string[]: Weekdays on which sends may go out; default Monday to Friday. One of exactly: Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday — capitalised English names, nothing else. The days are OR-joined: a send that comes due is pushed forward to the next day named in the set, so a weekday you leave out is never used. 1-7 unique names; naming all seven removes the weekday restriction.
  - `excludeLists` string[]: Ids of up to 10 Fuse people lists whose members are excluded from enrollment (a list that is also a source list is ignored here); default []. Take the ids from GET /business/lists with entityType=contactList. The lists are OR-joined: a contact is skipped when it appears in any one of them, matched on contact id, email address (case-insensitively) or LinkedIn URL. These ids are stored without an ownership check, so an id that is not one of your lists is accepted and silently excludes nobody — and a companyList here excludes nobody either, use excludeCompanyLists for that.
  - `excludeCompanyLists` string[]: Ids of up to 20 company or people lists owned by your team whose companies' contacts are skipped at enrollment; default []. Take the ids from GET /business/lists — either entityType: a companyList contributes its companies, a contactList contributes the companies its contacts work at. The lists are OR-joined: a contact is skipped when its company appears in any one of them, matched by company id and by the company's web domain (personal-email domains are never used). Unlike excludeLists these ids are checked, so one that is unknown, archived or not your team's answers 422 EXCLUDE_COMPANY_LISTS_INVALID.
  - `excludeContacted` 'none' | 'ever' | 'last_1_month' | 'last_3_months' | 'last_6_months' | 'last_1_year': Skips contacts your campaigns already reached: none keeps them, ever excludes anyone ever messaged, last_1_month / last_3_months / last_6_months / last_1_year exclude those messaged within that window; contacts with a pending send in an active campaign are always excluded. Default none.
  - `excludeEngagedContacts` boolean: When true, contacts who have already replied to one of your campaigns are excluded from enrollment. Default false.
  - `isDynamic` boolean: When true, contacts added to the source lists after activation are enrolled automatically by a background refresh. Default false.
  - `useLinkedinPosts` boolean: When true, each contact's recent LinkedIn posts are fetched at activation and fed to AI personalization (needs a LinkedIn URL on the contact). Default false.
  - `addUnsubscribeLink` boolean: When true, every email step appends a one-click unsubscribe footer (email channel only). Default true.
  - `openRateTrackingEnabledAt` string, nullable: ISO 8601 timestamp that switches on the open-tracking pixel for outgoing emails (it records when tracking was enabled); null (default) keeps open tracking off.
  - `sendOnlyToValidatedEmailsEnabledAt` string, nullable: ISO 8601 timestamp from which enrollment only accepts contacts whose email verified as valid (catch-all and never-verified uploaded addresses are skipped); null (default) also allows catch-all and never-verified uploaded emails. Applies to enrollment only.
  - `validateEmailsBeforeSendEnabledAt` string, nullable: ISO 8601 timestamp from which every email address is re-verified with ZeroBounce right before it is sent (and again once the last check is older than 14 days); addresses that come back invalid are skipped and marked invalid on the contact. Costs 2 credits per verified address. Null (default) sends without the check.
  - `dailyEmailLimit` integer, nullable: Maximum emails this campaign sends per calendar day in its timezone, across all its mailboxes and excluding replies. Mailbox limits and sharing with other campaigns still apply. Null (default) means no campaign-level cap.
  - `dailyLinkedinConnectionLimit` integer, nullable: Maximum LinkedIn connection requests this campaign sends per calendar day in its timezone, across all its LinkedIn accounts. Account limits still apply. Null (default) means no campaign-level cap.
  - `dailyLinkedinMessageLimit` integer, nullable: Maximum LinkedIn messages (InMails included) this campaign sends per calendar day in its timezone, across all its LinkedIn accounts. Account limits still apply. Null (default) means no campaign-level cap.
  - `delegates` string[]: User ids of teammates allowed to view and edit this campaign in the app besides you; default []. Take each id from the `id` of a GET /business/team/members row (a row whose id is null has no linked user account and cannot be a delegate). Access is OR-joined — any one of the listed users gets it — and there is no cap on how many you list. Nothing checks the ids against your team on the way in, and the app's delegate test is a plain membership check on this array that runs ahead of every team rule, so an id belonging to any real Fuse user — teammate or not — grants that user the access the owner has: opening the campaign, editing its settings and steps, activating it so it sends, and seeing it in their own campaign list. Only a well-formed id that matches no user grants nobody anything. The grant stops at the app: every campaign endpoint here resolves the token owner's own campaigns, so a delegate calling with their own key still gets 404.
  - `campaignType` 'ai-generated' | 'manual': manual (default): you write every step's subject/body and the draft is ready to author immediately. ai-generated: prompt analysis and topic generation run in the background; poll GET /business/campaigns/{campaignId} until initializationStatus is completed before editing or approving.
  - `templateId` string, nullable: Id of a saved campaign template (GET /business/campaign-templates) whose steps are copied into the new campaign; its channels replace the channels field, so the two cannot be combined. null means no template. An unknown or inaccessible template answers 404 TEMPLATE_NOT_FOUND.
  - `userPrompt` string, nullable: ai-generated only: free-text brief (up to 5000 characters) describing the offer and goal; the AI analyses it to derive the campaign topics. Forbidden for manual campaigns.
  - `ctaLink` string, nullable: The campaign's call-to-action link, honoured for either campaign type; null or an empty string means no link. Accepted values: one absolute URL whose scheme is http:// or https:// (up to 2000 characters); anything else, a bare domain or a mailto: link included, is rejected. The URL is inserted verbatim wherever a step's text uses {CTA Link} or one of its aliases {Calendar Link}, {cta_link}, {calendar_link}, {{calendarLink}}.
  - `writingStyleId` string, nullable: ai-generated only: id of one of your writing styles whose rules steer the generated copy; null or omitted uses your default style; an unknown id answers 404 WRITING_STYLE_NOT_FOUND. Forbidden for manual campaigns.
  - `knowledgeHubId` string: ai-generated only: id of the knowledge hub (GET /business/knowledge-hubs) that grounds the generated content; omitted means your default hub; an unknown id answers 404 KNOWLEDGE_HUB_NOT_FOUND. Forbidden for manual campaigns.

## Response `201`

Success

- object
  - `campaignId` string, required: Id of the new campaign; use it for every other campaign endpoint.
  - `status` 'initializing', required: A new campaign always starts as a draft in status initializing.

## Other responses

- `400`: Error
- `401`: Error
- `402`: Error
- `403`: Error
- `404`: Error
- `409`: Error
- `422`: Error
- `429`: Error
- `500`: Error

---

[API](https://skmtc.dev/fuseai/apis/fuse-business-api.md) · [All operations](https://skmtc.dev/fuseai/apis/fuse-business-api/llms.txt) · [OpenAPI document](https://skmtc.dev/fuseai/apis/fuse-business-api/revisions/50d95e0536f4?raw)
