---
title: "Create Campaign"
method: POST
path: "/campaigns"
tags: ["GTM Campaigns"]
---

# Create Campaign

`POST /campaigns`

## Request

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

Create a new campaign from scratch — full Studio "Convert to Campaign" parity.

The original handler only inserted ``cb_campaigns``. That left two
Studio-side gaps:

1. Studio's Campaigns workbench (``frontend/features/studio/components/
   CampaignUnifiedTable.tsx``) filters ``cb_campaign_ideas`` WHERE
   ``converted_to_campaign=true`` and joins back to ``cb_campaigns`` via
   ``converted_campaign_id``. With no paired idea row, MCP-created
   campaigns were invisible in the workbench (only the direct
   ``/studio?campaignId=...`` URL worked).
2. No documents were generated, so the campaign sat in "Not Ready" state
   indefinitely. The launch flow silently produced empty sequences.

This handler now mirrors ``campaign_builder/routers/campaign_conversion.py
:convert_idea_to_campaign`` end-to-end:

* INSERT cb_campaigns (status=``copy_in_progress`` when generating, else ``draft``)
* INSERT paired cb_campaign_ideas row with ``source='mcp'``,
  ``converted_to_campaign=true``, ``converted_campaign_id=<new>``
* INSERT cb_campaign_metadata with ``total_documents`` calculated from the
  target channels
* Scaffold ``sequence`` + ``step_catalog`` stub docs (idempotent)
* Dispatch ``generate_campaign_files_from_idea.delay(...)`` to Celery

Backwards-compatible: original 7-field callers still work AND see
byte-for-byte identical behavior because ``auto_generate_documents``
defaults to ``False``. All Studio-parity behavior (paired idea row,
metadata row, Celery dispatch, status='copy_in_progress') is opt-in
via ``auto_generate_documents=True``. The MCP ``g8_gtm_create_campaign``
tool wrapper sets it to ``True`` so Daniel's GTM agent gets the fix
without affecting any other direct API caller.

## Headers

- `Authorization` string, nullable
- `X-Target-Org-Id` string, nullable

## Request body

- DeveloperApiInterfacesV1GtmCampaignsRouterCampaignCreateRequest
  - `name` string, required: Campaign name
  - `category` string: Campaign category
  - `brief` string, nullable: Campaign brief
  - `core_concept` string, nullable: Core concept
  - `primary_hook` string, nullable: Primary hook text
  - `target_persona` string, nullable: Target persona
  - `goal` string, nullable: Campaign goal
  - `audience_list_id` string, nullable: If provided, attaches this list ID to cb_campaigns.audience_list_id at create time.
  - `target_channels` string[], nullable: Channels the campaign will use, e.g. ['email'], ['email','linkedin']. Drives which copy documents the auto-generator creates. Defaults to ['email'].
  - `execution_targets` string[], nullable: Execution destinations: sequencer, dialer, or content_only.
  - `secondary_hooks` string[], nullable: Additional hook variations to seed the campaign idea with.
  - `auto_generate_documents` boolean: When True, dispatches the Celery campaign-content generator asynchronously after creation: the paired cb_campaign_ideas row is inserted (so the FE workbench sees the campaign), cb_campaign_metadata is created, the campaign starts in status='copy_in_progress', and flips to 'active' once docs are ready (~60-90s). Default is False to preserve byte-for-byte backwards compatibility with the original bare-bones draft behavior — the MCP tool wrapper sets True explicitly to opt into Studio parity.

## Response `200`

Successful Response

- ApiResponseDict
  - `data` object, required
  - `pagination` PaginationMeta
    - `page` integer, required: Current page number (1-indexed)
    - `limit` integer, required: Items per page
    - `total` integer, required: Total number of items
    - `has_next` boolean, required: Whether there are more pages
    - `next_cursor` string, nullable: Opaque cursor for the next page; null when there are no more items. Pass it back as the `cursor` query param to fetch the following page without computing page numbers.
    - `total_available` integer, nullable: Search endpoints only: how many records really match, when known. Can be larger than `total`, which is capped at the 10,000 results that can be paged. For company search this is a distinct-company estimate. Null when unknown; absent outside search.
    - `total_is_lower_bound` boolean, nullable: Search endpoints only: true when more records match than `total` shows (the result set is larger than the 10,000 results that can be paged). Absent outside search.
    - `notes` string[], nullable: Search endpoints only: plain-language notes about the result, e.g. that more records match than can be paged, or that a reversed `between` range was put in order.

## Other responses

- `422`: Validation Error

---

[API](https://skmtc.dev/graph8/apis/graph8-developer-api.md) · [All operations](https://skmtc.dev/graph8/apis/graph8-developer-api/llms.txt) · [OpenAPI document](https://skmtc.dev/graph8/apis/graph8-developer-api/revisions/2e7cfa0ca921?raw)
