GTM Campaigns
GTM Campaigns are graph8’s AI-Studio campaign objects: a brief, a sequence (ordered step references), and a step catalog (the actual step definitions). Each campaign owns a set of generated documents (campaign brief, sequence doc, ad copy, landing page copy, etc.).
These endpoints are the org-scoped flavor used by Studio, MCP, and Claude Desktop GTM mode. The same surface is also exposed under /repos/{repo_id}/campaigns for spine-installed CLI users; see the CLI reference for that variant.
| Endpoint | Method | Description |
|---|---|---|
/campaigns | GET | List campaigns |
/campaigns | POST | Create a campaign |
/campaigns/ideas | GET | List saved campaign ideas |
/campaigns/{campaign_id} | GET | Get a campaign with completed documents |
/campaigns/{campaign_id} | PATCH | Update campaign content fields |
/campaigns/{campaign_id}/documents | GET | List documents for a campaign |
/campaigns/{campaign_id}/documents | POST | Create a document |
/campaigns/{campaign_id}/documents/{document_id} | GET | Get full document content |
/campaigns/{campaign_id}/documents/{document_id} | PUT | Update document content |
/campaigns/{campaign_id}/documents/{document_id} | DELETE | Delete a document |
/campaigns/{campaign_id}/sequence | GET | Get sequence + step catalog docs |
/campaigns/{campaign_id}/sequence | PUT | Replace sequence step ordering |
/campaigns/{campaign_id}/sequence/steps | POST | Append a step |
/campaigns/{campaign_id}/sequence/steps/{step_id} | PUT | Update a step definition |
/campaigns/{campaign_id}/sequence/steps/{step_id} | DELETE | Remove a step |
/campaigns/{campaign_id}/ad-images | GET | List generated ad images |
/campaigns/{campaign_id}/ad-images/{image_id}/hide | POST | Mark an ad image hidden |
/campaigns/{campaign_id}/launch | POST | Launch campaign (real outreach) |
/campaigns/{campaign_id}/metrics | GET | Get campaign metrics |
/campaigns/{campaign_id}/full | GET | Full snapshot (campaign + audience + docs + sequence) |
/campaigns/{campaign_id}/full | PATCH | Deep bulk update in a single transaction |
/campaigns/{campaign_id}/audience | PUT | Attach or detach audience list |
For machine-readable schemas see the interactive API docs.
List Campaigns
GET /campaigns
Returns all non-archived campaigns for the org, paginated.
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
page | integer | 1 | Page number |
limit | integer | 50 | 1-200 |
Example
curl "https://be.graph8.com/api/v1/campaigns?limit=10" \ -H "Authorization: Bearer $API_KEY"Response
{ "data": [ { "id": "cmp_abc", "name": "Q2 Outbound - VP Marketing", "slug": "q2-outbound-vp-marketing", "status": "draft", "category": "Outbound", "goal": "Book discovery calls", "target_persona": "VP Marketing at mid-market SaaS", "created_at": "2026-04-01T10:00:00Z" } ]}Create Campaign
POST /campaigns
Create a new campaign. Slug is auto-generated from name.
Campaign field constraints: name ≤255, category ≤100, target_persona ≤200, goal ≤255.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Campaign name (max 255 chars) |
category | string | No (default "Outbound") | Campaign category (max 100 chars) |
brief | string | No | Campaign brief (uncapped) |
core_concept | string | No | One-line concept |
primary_hook | string | No | Primary hook text |
target_persona | string | No | Target persona (max 200 chars) |
goal | string | No | Campaign goal (max 255 chars) |
audience_list_id | string | No | Attach a list at create time |
target_channels | string[] | No (default ["email"]) | Channels to target |
secondary_hooks | string[] | No | Additional hook lines |
auto_generate_documents | boolean | No (default false) | When true, triggers full Studio bootstrap: creates paired campaign ideas row, metadata, sequence and step catalog stubs, and dispatches Celery doc generation. Sets status="copy_in_progress". The MCP tool always sets this true; the launcher requires it. |
Example
curl -X POST "https://be.graph8.com/api/v1/campaigns" \ -H "Authorization: Bearer $API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Q2 Outbound - VP Marketing", "category": "Outbound", "primary_hook": "Cut SDR ramp time in half", "goal": "Book discovery calls", "audience_list_id": "12345", "target_channels": ["email"], "auto_generate_documents": true }'Response
{ "data": { "id": "cmp_abc", "name": "Q2 Outbound - VP Marketing", "slug": "q2-outbound-vp-marketing", "status": "copy_in_progress", "category": "Outbound", "audience_list_id": "12345", "campaign_idea_id": "idea_001", "target_channels": ["email"], "secondary_hooks": [], "generation_status": "in_progress", "total_documents": 15 }}generation_status is "in_progress" when auto_generate_documents was true, "skipped" when it was false, or "failed_to_dispatch" if the Celery dispatch failed.
status is "copy_in_progress" when auto-generation was requested, otherwise "draft".
List Campaign Ideas
GET /campaigns/ideas
Returns saved/AI-generated campaign ideas (pre-conversion). Use these to seed POST /campaigns.
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
favorited | boolean | - | Filter to favorited ideas only |
limit | integer | 50 | 1-200 |
Response
{ "data": [ { "id": "idea_001", "rank": 1, "campaign_name": "Sales Velocity Diagnostic", "campaign_category": "Outbound", "campaign_focus": "Mid-market RevOps", "core_concept": "...", "why_this_works": "...", "primary_hook": "Find your stuck deals in 5 minutes", "secondary_hooks": ["..."], "target_channels": ["email", "linkedin"], "required_assets": ["landing page", "demo video"], "execution_complexity": "medium", "expected_impact": "high", "proof_points": ["..."], "framework_inspiration": "Challenger", "source": "studio_generated", "favorited": true, "converted_to_campaign": false, "converted_campaign_id": null, "created_at": "2026-04-01T10:00:00Z", "updated_at": "2026-04-01T10:00:00Z" } ]}Get Campaign
GET /campaigns/{campaign_id}
Returns campaign details plus a thumbnail list of completed documents.
Example
curl "https://be.graph8.com/api/v1/campaigns/cmp_abc" \ -H "Authorization: Bearer $API_KEY"Response
{ "data": { "id": "cmp_abc", "name": "Q2 Outbound - VP Marketing", "slug": "q2-outbound-vp-marketing", "status": "draft", "category": "Outbound", "goal": "Book discovery calls", "target_persona": "VP Marketing at mid-market SaaS", "brief": "...", "core_concept": "...", "primary_hook": "Cut SDR ramp time in half", "channels": ["email", "linkedin"], "documents": [ {"id": "doc_001", "name": "Campaign Brief", "type": "campaign_brief"}, {"id": "doc_002", "name": "Sequence", "type": "sequence"} ], "created_at": "2026-04-01T10:00:00Z", "updated_at": "2026-04-10T14:30:00Z" }}Errors
| Status | Meaning |
|---|---|
404 | Campaign not found |
Update Campaign
PATCH /campaigns/{campaign_id}
Update content fields on a campaign.
Request Body
| Field | Type | Description |
|---|---|---|
name | string | Campaign name |
brief | string | Campaign brief |
core_concept | string | Core concept |
primary_hook | string | Primary hook text |
target_persona | string | Target persona |
category | string | Campaign category |
goal | string | Campaign goal |
Errors
| Status | Meaning |
|---|---|
400 | No fields provided |
404 | Campaign not found |
List Campaign Documents
GET /campaigns/{campaign_id}/documents
Returns metadata for every document associated with the campaign (no content payload).
Response
{ "data": { "campaign_id": "cmp_abc", "documents": [ { "id": "doc_001", "campaign_id": "cmp_abc", "display_name": "Campaign Brief", "name": "Campaign Brief", "file_type": "campaign_brief", "type": "campaign_brief", "folder_path": "/brief", "status": "completed", "version": 2, "current_version": 2, "created_at": "2026-04-01T10:00:00Z", "updated_at": "2026-04-05T11:00:00Z" } ], "count": 1 }}Get Campaign Document
GET /campaigns/{campaign_id}/documents/{document_id}
Returns full document content.
Example
curl "https://be.graph8.com/api/v1/campaigns/cmp_abc/documents/doc_001" \ -H "Authorization: Bearer $API_KEY"Response
{ "data": { "id": "doc_001", "campaign_id": "cmp_abc", "name": "Campaign Brief", "type": "campaign_brief", "status": "completed", "version": 2, "content": "# Campaign Brief\n\nGoal: ...", "created_at": "2026-04-01T10:00:00Z", "updated_at": "2026-04-05T11:00:00Z" }}Create Campaign Document
POST /campaigns/{campaign_id}/documents
Create a new document attached to the campaign.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
file_type | string | Yes | Document type (e.g. campaign_brief, sequence, etc.) |
display_name | string | Yes | Human-readable name |
content | string | No | Document content (default "") |
folder_path | string | No | Optional folder path (e.g. "/briefs") |
status | string | No (default "draft") | "draft" or "completed" |
structured_data | object | No | Optional structured payload stored in metadata |
Response 201 Created
Returns the same document shape as GET .../documents/{document_id}.
Update Campaign Document
PUT /campaigns/{campaign_id}/documents/{document_id}
Update content. Bumps version and current_version by 1, marks completed=true and generating=false in metadata.
Request Body
| Field | Type | Description |
|---|---|---|
content | string | New content (defaults to existing if empty) |
status | string | Optional status override |
structured_data | object | Optional structured payload stored in metadata |
Errors
| Status | Meaning |
|---|---|
404 | Document not found (or its parent campaign is archived) |
Delete Campaign Document
DELETE /campaigns/{campaign_id}/documents/{document_id}
Hard delete. Returns 404 if the document or its parent campaign is missing.
Response
{ "data": { "status": "deleted", "document_id": "doc_001" } }Get Sequence
GET /campaigns/{campaign_id}/sequence
Returns both the sequence document (ordered list of step references) and the step catalog document (step definitions keyed by step_id).
Response
{ "data": { "campaign_id": "cmp_abc", "sequence": { "steps": [ {"step_id": "step_1", "day": 0, "condition": null, "stop_on_reply": true}, {"step_id": "step_2", "day": 3, "condition": "no_reply", "stop_on_reply": true} ] }, "step_catalog": { "steps": { "step_1": {"name": "Intro email", "channel": "email", "mode": "send", ...}, "step_2": {"name": "LinkedIn follow-up", "channel": "linkedin", "mode": "send", ...} } }, "sequence_doc_id": "doc_seq_001", "catalog_doc_id": "doc_cat_001" }}Errors
| Status | Meaning |
|---|---|
404 | Campaign not found |
Update Sequence
PUT /campaigns/{campaign_id}/sequence
Replace the ordered list of step references. Each step_id must exist in the step catalog.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
steps | object[] | Yes | Ordered step references |
Each step item:
| Field | Type | Description |
|---|---|---|
step_id | string | Step ID (must exist in catalog) |
day | integer | Day offset from sequence start |
condition | string | Optional condition (no_reply, etc.) |
stop_on_reply | boolean | Default true |
Errors
| Status | Meaning |
|---|---|
400 | step_id not in catalog |
404 | Sequence document not found |
Create Sequence Step
POST /campaigns/{campaign_id}/sequence/steps
Create a new step and append it to the sequence in one call. The endpoint mints a fresh step_id (step_N).
Channel → Mode Matrix
The channel field determines which mode values are valid:
| Channel | Valid modes |
|---|---|
email | email |
phone | call, voicemail |
sms | sms |
social | connect_note, dm, comment, reply |
voice_ai | call |
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Step name |
channel | string | Yes | Channel type — see Channel → Mode Matrix above |
mode | string | Yes | Mode for the chosen channel — see Channel → Mode Matrix above |
day | integer | Yes | Day offset for sequence placement |
platform | string | No | Sub-channel platform (e.g. "linkedin") |
angle_id | string | No (default "angle_1") | Messaging angle reference |
cta_type | string | No (default "soft_ask") | CTA type |
personalization_level | string | No (default "medium") | Level of personalization |
constraints | object | No | Constraint dict |
condition | string | No | Sequence condition |
stop_on_reply | boolean | No (default true) | Stop the contact’s enrollment on reply |
Response 201 Created
{ "data": { "status": "created", "step_id": "step_3", "step": { "step_id": "step_3", "name": "Final follow-up", "channel": "email", "mode": "send", "platform": null, "angle_id": "angle_1", "cta_type": "soft_ask", "personalization_level": "medium", "constraints": {} }, "day": 7 }}Update Sequence Step
PUT /campaigns/{campaign_id}/sequence/steps/{step_id}
Update a step definition in the catalog and (optionally) its placement in the sequence. All fields are optional - send only what you want to change.
Request Body
| Field | Type | Description |
|---|---|---|
name | string | Step name |
channel | string | Channel |
mode | string | Mode |
platform | string | Platform |
angle_id | string | Messaging angle |
cta_type | string | CTA type |
personalization_level | string | Level |
constraints | object | Constraint dict |
do_not_send_rules | string[] | Rules that should suppress sending |
day | integer | Sequence placement (updates the sequence doc, not the catalog) |
condition | string | Sequence placement condition |
stop_on_reply | boolean | Sequence placement override |
day, condition, and stop_on_reply modify the sequence document; everything else modifies the catalog entry.
Errors
| Status | Meaning |
|---|---|
404 | Step or step catalog not found |
Delete Sequence Step
DELETE /campaigns/{campaign_id}/sequence/steps/{step_id}
Remove a step from the catalog and any references in the sequence.
Response
{ "data": { "status": "deleted", "step_id": "step_3" } }Errors
| Status | Meaning |
|---|---|
404 | Step not found |
Launch Campaign
POST /campaigns/{campaign_id}/launch
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
validate | boolean | — | When true, validates the campaign without actually launching |
Request Body
All fields are optional. An empty body is valid and keeps the 0.9.6 auto-pick contract.
| Field | Type | Description |
|---|---|---|
sender_mailbox_ids | integer[] | Explicit sender mailbox IDs from GET /mailboxes. Each must be connection_status="active" and non-archived. Missing IDs return 400. |
disable_auto_pick | boolean | Default false. When true, forces legacy empty-channels behavior — the sequence sits at status="scheduling" until a channel is attached via the Sequencer UI. |
textual_agent_name | string | Since 0.9.7. Agent name from GET /voice/dialer/agents (the unified Agents+Twins picker). Stamped onto the sequence. Used for email and social steps. |
voice_agent_name | string | Since 0.9.7. Same picker; for phone and SMS steps. Set equal to textual_agent_name for the common “one agent, all channels” case. |
schedule_id | string | Since 0.9.7. UUID from GET /schedules. Stamped onto the sequence (send window + timezone). When omitted the sequencer falls back to its org-default send window. |
appointment_id | integer | Since 0.9.7. Integer from GET /event-types. Stamped onto the sequence so the booking-link rewrite kicks in during send. |
Sender selection precedence:
- If
sender_mailbox_idsis provided → attach exactly those mailboxes. - Else if
disable_auto_pickisfalse(default) → bridge auto-picks the first active email mailbox (ORDER BY id ASC). - Else → legacy empty channels.
Example
curl -X POST "https://be.graph8.com/api/v1/campaigns/cmp_abc/launch" \ -H "Authorization: Bearer $API_KEY" \ -H "Content-Type: application/json" \ -d '{ "sender_mailbox_ids": [42, 17], "textual_agent_name": "my-agent", "voice_agent_name": "my-agent", "schedule_id": "sched_uuid", "appointment_id": 1 }'Response
{ "data": { "status": "launched", "campaign_id": "cmp_abc", "sequence_id": "seq_001", "sequence_name": "Q2 Outbound - VP Marketing", "steps_created": 3, "link_id": "link_001", "warnings": [], "channels_attached": [ ], "agent_assigned": { "textual_agent_name": "my-agent", "voice_agent_name": "my-agent" }, "schedule_id": "sched_uuid", "appointment_id": 1, "message": "Campaign launched successfully." }}channels_attached lists the sender mailboxes the bridge attached. auto_picked: true when the bridge selected a mailbox automatically.
Get Ad Images
GET /campaigns/{campaign_id}/ad-images
Returns generated ad images for the campaign.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
campaign_id | string | Campaign ID |
Example
curl "https://be.graph8.com/api/v1/campaigns/cmp_abc/ad-images" \ -H "Authorization: Bearer $API_KEY"Hide Ad Image
POST /campaigns/{campaign_id}/ad-images/{image_id}/hide
Marks an ad image as hidden. No request body required.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
campaign_id | string | Campaign ID |
image_id | string | Ad image ID |
Example
curl -X POST "https://be.graph8.com/api/v1/campaigns/cmp_abc/ad-images/img_001/hide" \ -H "Authorization: Bearer $API_KEY"Get Campaign Metrics
GET /campaigns/{campaign_id}/metrics
Returns engagement metrics for the campaign over a rolling time window.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
campaign_id | string | Campaign ID |
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
days | integer | 30 | Rolling window in days (1–365) |
Example
curl "https://be.graph8.com/api/v1/campaigns/cmp_abc/metrics?days=30" \ -H "Authorization: Bearer $API_KEY"Get Campaign Full Snapshot
GET /campaigns/{campaign_id}/full
Returns a full snapshot of the campaign including audience, all documents, and the sequence.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
campaign_id | string | Campaign ID |
Example
curl "https://be.graph8.com/api/v1/campaigns/cmp_abc/full" \ -H "Authorization: Bearer $API_KEY"Bulk Update Campaign
PATCH /campaigns/{campaign_id}/full
Deep bulk update executed in a single transaction. Updates campaign fields, patches or creates documents, reorders sequence steps, and links or unlinks existing sequences.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
campaign_id | string | Campaign ID |
Request Body
All fields are optional.
| Field | Type | Description |
|---|---|---|
name | string | Campaign name (max 255 chars) |
brief | string | Campaign brief |
core_concept | string | Core concept |
primary_hook | string | Primary hook |
target_persona | string | Target persona (max 200 chars) |
category | string | Campaign category (max 100 chars) |
goal | string | Campaign goal (max 255 chars) |
audience_list_id | string | Provide a list ID (or "") to set the audience. Use detach_audience to clear it. |
detach_audience | boolean | When true, explicitly sets audience_list_id to null |
documents | object[] | List of document patch/create objects (see below) |
sequence_steps | object[] | Replaces the full sequence ordering |
linked_sequence_ids | string[] | Link existing sequences (sets status="linked") |
unlink_sequence_ids | string[] | Unlink sequences. Only deletes status="linked" rows; audit rows are untouched. |
documents item fields
| Field | Type | Description |
|---|---|---|
id | string | When present, updates an existing document. When absent, creates a new document (then file_type and display_name are required). |
file_type | string | Document type (required for create) |
display_name | string | Human-readable name (required for create) |
folder_path | string | Optional folder path |
status | string | "draft" or "completed" |
content | string | Document content |
structured_data | object | Optional structured payload |
sequence_steps item fields
| Field | Type | Description |
|---|---|---|
step_id | string | Step ID (must exist in catalog) |
day | integer | Day offset from sequence start |
condition | string | Optional condition string |
stop_on_reply | boolean | Stop enrollment on reply |
Example
curl -X PATCH "https://be.graph8.com/api/v1/campaigns/cmp_abc/full" \ -H "Authorization: Bearer $API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Q3 Outbound - VP Engineering", "brief": "Updated brief text", "audience_list_id": "12345", "documents": [ { "id": "doc_uuid", "status": "completed", "content": "Updated brief content" }, { "file_type": "landing_page", "display_name": "Landing Page v2", "content": "New landing page copy", "structured_data": {} } ], "sequence_steps": [ { "step_id": "step_1", "day": 1, "condition": null, "stop_on_reply": true }, { "step_id": "step_2", "day": 3, "condition": null, "stop_on_reply": true } ], "linked_sequence_ids": ["seq_existing_001"], "unlink_sequence_ids": ["seq_old_002"] }'Attach or Detach Audience
PUT /campaigns/{campaign_id}/audience
Attach an audience list to the campaign, or detach the current one by passing null.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
campaign_id | string | Campaign ID |
Request Body
| Field | Type | Description |
|---|---|---|
list_id | string | Audience list ID to attach. Pass null to detach the current audience. |
Example
# Attach an audiencecurl -X PUT "https://be.graph8.com/api/v1/campaigns/cmp_abc/audience" \ -H "Authorization: Bearer $API_KEY" \ -H "Content-Type: application/json" \ -d '{ "list_id": "12345" }'
# Detach the audiencecurl -X PUT "https://be.graph8.com/api/v1/campaigns/cmp_abc/audience" \ -H "Authorization: Bearer $API_KEY" \ -H "Content-Type: application/json" \ -d '{ "list_id": null }'