> ## Documentation Index
> Fetch the complete documentation index at: https://docs.fuseai.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Campaigns

> Create a campaign from a list, review and edit its sequence steps, and approve it to start sending.

A campaign turns a [list](/guides/upload-your-contacts) into an outbound sequence. The API mirrors how campaigns work in the app: you create a **draft**, Fuse initializes it asynchronously, you review and edit the sequence steps, and nothing sends until you explicitly **approve** it. That last part is deliberate — a campaign contacts real people, so the API never activates one as a side effect of creating it.

The full flow is four calls:

1. `POST /business/campaigns` — create the draft.
2. `GET /business/campaigns/{campaignId}` — poll until initialization completes.
3. `GET /business/campaigns/{campaignId}/steps` — review (and optionally `PATCH`) the sequence.
4. `POST /business/campaigns/{campaignId}/approve` — activate it.

<Info>
  Campaign endpoints require a key with the `campaigns` scope. See [scopes](/concepts/scopes).
</Info>

<Note>
  Sending accounts (email and LinkedIn) are connected in the Fuse app, not via the API. Connect at least one before you approve — approval fails with `CAMPAIGN_NOT_READY` if no sending account is available.
</Note>

## Manual vs. AI-generated

Every campaign is one of two types, set by `campaignType` at creation:

* **`manual`** (the default) — you write the subject and body of every step yourself.
* **`ai-generated`** — Fuse writes the copy during initialization, guided by your prompt, your website, and optional context like a [knowledge hub](/guides/knowledge-hubs) or a writing style.

The type decides which request fields are valid and which step fields you edit later: manual steps have a `subject` and `body` you own; AI steps are driven by a `topic`. AI-only fields on a manual campaign are rejected, not ignored — the request fails so you find out immediately rather than wondering why your prompt had no effect.

<Steps>
  <Step title="Create the draft">
    Provide a `campaignName`, exactly one of `listId` or `listIds` (up to 10 lists), and a `channels` array — one entry per sequence step, in order.

    <CodeGroup>
      ```bash Manual campaign theme={null}
      curl -X POST https://api.tryfuse.ai/api/v1/business/campaigns \
        -u "af_YOUR_KEY:" \
        -H "Content-Type: application/json" \
        -d '{
          "campaignName": "Q3 fintech outreach",
          "campaignType": "manual",
          "listId": "68a1f20b9c41d20014b3e901",
          "channels": ["email", "email"],
          "campaignStartDate": "2026-08-03T09:00:00.000Z",
          "timezone": "America/New_York",
          "emailDays": ["Monday", "Tuesday", "Wednesday", "Thursday"],
          "excludeContacted": "last_3_months"
        }'
      ```

      ```bash AI-generated campaign theme={null}
      curl -X POST https://api.tryfuse.ai/api/v1/business/campaigns \
        -u "af_YOUR_KEY:" \
        -H "Content-Type: application/json" \
        -d '{
          "campaignName": "Q3 fintech outreach",
          "campaignType": "ai-generated",
          "listIds": ["68a1f20b9c41d20014b3e901"],
          "channels": ["email", "email", "email"],
          "userPrompt": "Book intro calls with finance leaders evaluating spend-management tools. Friendly, concise, no jargon.",
          "websiteUrls": ["https://fuseai.com"],
          "ctaLink": "https://fuseai.com/demo",
          "knowledgeHubId": "68a1f5c09c41d20014b3eb11"
        }'
      ```
    </CodeGroup>

    ```json theme={null}
    {
      "campaignId": "68a1f6309c41d20014b3ec21",
      "status": "initializing"
    }
    ```

    A `201` means the draft was accepted, not that it is ready — initialization runs in the background, which is what the next step polls for.

    **Core fields:**

    | Field                | Notes                                                                     |
    | -------------------- | ------------------------------------------------------------------------- |
    | `campaignName`       | Required. 1–200 characters.                                               |
    | `campaignType`       | `"manual"` (default) or `"ai-generated"`.                                 |
    | `listId` / `listIds` | Exactly one of the two. `listIds` accepts 1–10 list ids.                  |
    | `channels`           | The sequence's step channels, in order — one entry per step.              |
    | `templateId`         | Start the campaign from an existing template instead of a blank sequence. |

    **Scheduling (all optional):**

    | Field               | Notes                                                                           |
    | ------------------- | ------------------------------------------------------------------------------- |
    | `campaignStartDate` | ISO 8601 timestamp for when sending may begin.                                  |
    | `timezone`          | Timezone the schedule is evaluated in. Defaults to `UTC`.                       |
    | `emailDays`         | Weekday names sending is allowed on, e.g. `["Monday", "Tuesday"]`. 1–7 entries. |

    **Exclusions (all optional):**

    | Field                    | Notes                                                                                                                                 |
    | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------- |
    | `excludeLists`           | Contact lists whose members are excluded. Up to 10.                                                                                   |
    | `excludeCompanyLists`    | Company lists whose employees are excluded. Up to 20.                                                                                 |
    | `excludeContacted`       | Skip people you have already contacted: `none` (default), `ever`, `last_1_month`, `last_3_months`, `last_6_months`, or `last_1_year`. |
    | `excludeEngagedContacts` | Boolean, default `false`. Skip contacts who have already engaged with you.                                                            |

    **AI-only fields** — valid only when `campaignType` is `"ai-generated"`; a manual campaign rejects them:

    | Field            | Notes                                                                             |
    | ---------------- | --------------------------------------------------------------------------------- |
    | `userPrompt`     | What the campaign should achieve and how it should sound. Up to 5,000 characters. |
    | `websiteUrls`    | Pages the AI reads for context about your product. Up to 10 URLs.                 |
    | `additionalInfo` | Extra free-form context. Up to 10,000 characters.                                 |
    | `ctaLink`        | The link the generated copy drives readers toward.                                |
    | `writingStyleId` | A saved writing style to match.                                                   |
    | `knowledgeHubId` | A [knowledge hub](/guides/knowledge-hubs) the AI draws product facts from.        |

    The endpoint accepts a few additional toggles (unsubscribe link, open-rate tracking, and others) — see the [API reference](/api-reference) for the full request schema.

    <Accordion title="Errors you can hit at create">
      All errors use the standard [error envelope](/concepts/errors).

      | Code                              | What it means                                                           |
      | --------------------------------- | ----------------------------------------------------------------------- |
      | `LIST_NOT_FOUND`                  | A `listId`/`listIds` entry does not exist in your workspace.            |
      | `INVALID_CAMPAIGN_CHANNELS`       | The `channels` array is not a valid sequence.                           |
      | `LIST_INCOMPATIBLE_WITH_CHANNELS` | The list is empty, or nobody in it is reachable on the chosen channels. |
      | `TEMPLATE_NOT_FOUND`              | The `templateId` does not exist.                                        |
      | `WRITING_STYLE_NOT_FOUND`         | The `writingStyleId` does not exist.                                    |
      | `KNOWLEDGE_HUB_NOT_FOUND`         | The `knowledgeHubId` does not exist.                                    |
      | `EXCLUDE_COMPANY_LISTS_INVALID`   | An `excludeCompanyLists` entry is missing or is not a company list.     |
    </Accordion>
  </Step>

  <Step title="Poll until initialization completes">
    Initialization is asynchronous — for AI campaigns it includes generating the copy for every step, which takes time. Poll the campaign until `initializationStatus` is `"completed"`:

    ```bash theme={null}
    curl https://api.tryfuse.ai/api/v1/business/campaigns/68a1f6309c41d20014b3ec21 \
      -u "af_YOUR_KEY:"
    ```

    ```json theme={null}
    {
      "campaign": {
        "id": "68a1f6309c41d20014b3ec21",
        "name": "Q3 fintech outreach",
        "type": "ai",
        "status": "initializing",
        "initializationStatus": "completed",
        "initializationError": null,
        "listIds": [
          "68a1f20b9c41d20014b3e901"
        ]
      }
    }
    ```

    `status` and `initializationStatus` answer different questions: `status` is the campaign's lifecycle state (it stays pre-active until you approve), while `initializationStatus` tells you whether the draft is ready to review. When AI generation fails, `initializationError` carries readable status text explaining what went wrong.
  </Step>

  <Step title="Review the sequence">
    Once initialized, fetch the steps and read what will actually go out:

    ```bash theme={null}
    curl https://api.tryfuse.ai/api/v1/business/campaigns/68a1f6309c41d20014b3ec21/steps \
      -u "af_YOUR_KEY:"
    ```

    ```json theme={null}
    {
      "steps": [
        {
          "id": "68a1f6409c41d20014b3ec31",
          "channel": "email",
          "sequenceNumber": 1,
          "daysBetween": 0,
          "topic": "Intro to Fuse for finance teams",
          "subject": "Quick question about {{companyName}}",
          "body": "Hi {{firstName}}, ..."
        }
      ]
    }
    ```

    `sequenceNumber` orders the steps and `daysBetween` is the wait, in days, after the previous step. If anything needs changing, edit it now — see [Editing steps](#editing-steps) below. Approval is the point of no return, so this is the moment to check the copy, the merge tokens, and (for AI campaigns) that every step has a `topic`.
  </Step>

  <Step title="Approve">
    Approval is the explicit go signal. It validates that the campaign is complete and then activates it:

    ```bash theme={null}
    curl -X POST https://api.tryfuse.ai/api/v1/business/campaigns/68a1f6309c41d20014b3ec21/approve \
      -u "af_YOUR_KEY:"
    ```

    ```json theme={null}
    {
      "campaignId": "68a1f6309c41d20014b3ec21",
      "status": "active"
    }
    ```

    The `202` means activation was accepted; sending follows your schedule (`campaignStartDate`, `emailDays`, `timezone`). If approval fails instead, see [Troubleshooting approval](#troubleshooting-approval).
  </Step>
</Steps>

## Editing steps

`PATCH /business/campaigns/{campaignId}/steps/{stepId}` edits one step and returns `{ "stepId": "..." }`. Which fields you set depends on the campaign type: on manual steps you write the `subject` and `body`; on AI steps you set the `topic` and let generation handle the copy. Either way you can adjust `daysBetween` (0–365).

<CodeGroup>
  ```bash Manual step theme={null}
  curl -X PATCH https://api.tryfuse.ai/api/v1/business/campaigns/68a1f6309c41d20014b3ec21/steps/68a1f6409c41d20014b3ec31 \
    -u "af_YOUR_KEY:" \
    -H "Content-Type: application/json" \
    -d '{
      "subject": "Quick question about {{companyName}}",
      "body": "Hi {{firstName}},\n\nSaw your team is hiring in finance — worth a quick chat about {Column: Pain point}?",
      "daysBetween": 3
    }'
  ```

  ```bash AI step theme={null}
  curl -X PATCH https://api.tryfuse.ai/api/v1/business/campaigns/68a1f6309c41d20014b3ec21/steps/68a1f6409c41d20014b3ec31 \
    -u "af_YOUR_KEY:" \
    -H "Content-Type: application/json" \
    -d '{ "topic": "Follow up on the ROI angle with a customer example" }'
  ```
</CodeGroup>

```json theme={null}
{ "stepId": "68a1f6409c41d20014b3ec31" }
```

Limits: `subject` up to 500 characters, `body` up to 50,000, `topic` up to 1,000.

### Merge tokens

Step content supports two kinds of tokens:

* **Contact tokens** — `{{firstName}}`, `{{companyName}}`, and similar, filled from the contact's own fields.
* **Column tokens** — `{Column: <name>}`, filled from the list's [custom columns](/guides/custom-columns). `{Column: Pain point}` renders the value of the "Pain point" column for that contact.

Column tokens have one behavior worth planning around: if the referenced column has no value for a contact yet, that contact's send is **held** until the value exists. This is deliberate — Fuse never sends a half-rendered email — and it is what makes the "enrich a custom column, then send" workflow safe. It also means a campaign can look stalled when it is actually waiting on column data, so fill your columns before approving, or expect sends to trickle out as values land.

<Warning>
  **The topic gotcha.** An AI campaign created from a `templateId` whose template was captured from a *manual* campaign inherits each step's `subject` and `body` — but no `topic`. Approval requires a topic on every AI-generated step, so this combination fails with `422 CAMPAIGN_NOT_READY` until you `PATCH` a `topic` onto each step.
</Warning>

## Troubleshooting approval

Approval failures come back in the standard [error envelope](/concepts/errors):

```json theme={null}
{
  "error": {
    "code": "CAMPAIGN_NOT_READY",
    "message": "The campaign is missing content required for approval.",
    "param": null,
    "doc_url": "https://fuseai.com/api/errors/campaign_not_ready",
    "request_id": "req_8fK2mQ4x"
  }
}
```

| Status | Code                      | What it means                                                                                                                             |
| ------ | ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| `422`  | `CAMPAIGN_NOT_READY`      | The campaign is incomplete: AI steps missing topics, manual steps missing subjects or bodies, or no sending account connected in the app. |
| `409`  | `CAMPAIGN_NOT_APPROVABLE` | The campaign is in the wrong state — for example, it is already active.                                                                   |
| `404`  | `CAMPAIGN_NOT_FOUND`      | The campaign id does not exist in your workspace.                                                                                         |

`CAMPAIGN_NOT_READY` is the one you fix and retry: add the missing content via [step edits](#editing-steps), or connect a sending account in the app, then call approve again. `CAMPAIGN_NOT_APPROVABLE` means retrying will not help — the campaign has already moved past the draft state.

## Where to next

<CardGroup cols={2}>
  <Card title="Knowledge hubs" icon="book" href="/guides/knowledge-hubs">
    Give AI campaigns a curated source of product facts.
  </Card>

  <Card title="Custom columns" icon="table-columns" href="/guides/custom-columns">
    Attach per-contact data your steps can merge with column tokens.
  </Card>

  <Card title="Build a list from search" icon="magnifying-glass" href="/guides/build-a-list-from-search">
    Fill a campaign-ready list from prospect search.
  </Card>

  <Card title="Upload your contacts" icon="upload" href="/guides/upload-your-contacts">
    Bring your own CRM contacts into a list first.
  </Card>
</CardGroup>
