All docs

GTM Campaigns

Create and manage AI Studio campaigns, documents, and sequence step catalogs

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.

EndpointMethodDescription
/campaignsGETList campaigns
/campaignsPOSTCreate a campaign
/campaigns/ideasGETList saved campaign ideas
/campaigns/{campaign_id}GETGet a campaign with completed documents
/campaigns/{campaign_id}PATCHUpdate campaign content fields
/campaigns/{campaign_id}/documentsGETList documents for a campaign
/campaigns/{campaign_id}/documentsPOSTCreate a document
/campaigns/{campaign_id}/documents/{document_id}GETGet full document content
/campaigns/{campaign_id}/documents/{document_id}PUTUpdate document content
/campaigns/{campaign_id}/documents/{document_id}DELETEDelete a document
/campaigns/{campaign_id}/sequenceGETGet sequence + step catalog docs
/campaigns/{campaign_id}/sequencePUTReplace sequence step ordering
/campaigns/{campaign_id}/sequence/stepsPOSTAppend a step
/campaigns/{campaign_id}/sequence/steps/{step_id}PUTUpdate a step definition
/campaigns/{campaign_id}/sequence/steps/{step_id}DELETERemove a step
/campaigns/{campaign_id}/ad-imagesGETList generated ad images
/campaigns/{campaign_id}/ad-images/{image_id}/hidePOSTMark an ad image hidden
/campaigns/{campaign_id}/launchPOSTLaunch campaign (real outreach)
/campaigns/{campaign_id}/metricsGETGet campaign metrics
/campaigns/{campaign_id}/fullGETFull snapshot (campaign + audience + docs + sequence)
/campaigns/{campaign_id}/fullPATCHDeep bulk update in a single transaction
/campaigns/{campaign_id}/audiencePUTAttach 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

ParameterTypeDefaultDescription
pageinteger1Page number
limitinteger501-200

Example

cURL

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

FieldTypeRequiredDescription
namestringYesCampaign name (max 255 chars)
categorystringNo (default "Outbound")Campaign category (max 100 chars)
briefstringNoCampaign brief (uncapped)
core_conceptstringNoOne-line concept
primary_hookstringNoPrimary hook text
target_personastringNoTarget persona (max 200 chars)
goalstringNoCampaign goal (max 255 chars)
audience_list_idstringNoAttach a list at create time
target_channelsstring[]No (default ["email"])Channels to target
secondary_hooksstring[]NoAdditional hook lines
auto_generate_documentsbooleanNo (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

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

ParameterTypeDefaultDescription
favoritedboolean-Filter to favorited ideas only
limitinteger501-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

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

StatusMeaning
404Campaign not found

Update Campaign

PATCH /campaigns/{campaign_id}

Update content fields on a campaign.

Request Body

FieldTypeDescription
namestringCampaign name
briefstringCampaign brief
core_conceptstringCore concept
primary_hookstringPrimary hook text
target_personastringTarget persona
categorystringCampaign category
goalstringCampaign goal

Errors

StatusMeaning
400No fields provided
404Campaign 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

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

FieldTypeRequiredDescription
file_typestringYesDocument type (e.g. campaign_brief, sequence, etc.)
display_namestringYesHuman-readable name
contentstringNoDocument content (default "")
folder_pathstringNoOptional folder path (e.g. "/briefs")
statusstringNo (default "draft")"draft" or "completed"
structured_dataobjectNoOptional 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

FieldTypeDescription
contentstringNew content (defaults to existing if empty)
statusstringOptional status override
structured_dataobjectOptional structured payload stored in metadata

Errors

StatusMeaning
404Document 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

StatusMeaning
404Campaign 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

FieldTypeRequiredDescription
stepsobject[]YesOrdered step references

Each step item:

FieldTypeDescription
step_idstringStep ID (must exist in catalog)
dayintegerDay offset from sequence start
conditionstringOptional condition (no_reply, etc.)
stop_on_replybooleanDefault true

Errors

StatusMeaning
400step_id not in catalog
404Sequence 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:

ChannelValid modes
emailemail
phonecall, voicemail
smssms
socialconnect_note, dm, comment, reply
voice_aicall

Request Body

FieldTypeRequiredDescription
namestringYesStep name
channelstringYesChannel type — see Channel → Mode Matrix above
modestringYesMode for the chosen channel — see Channel → Mode Matrix above
dayintegerYesDay offset for sequence placement
platformstringNoSub-channel platform (e.g. "linkedin")
angle_idstringNo (default "angle_1")Messaging angle reference
cta_typestringNo (default "soft_ask")CTA type
personalization_levelstringNo (default "medium")Level of personalization
constraintsobjectNoConstraint dict
conditionstringNoSequence condition
stop_on_replybooleanNo (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

FieldTypeDescription
namestringStep name
channelstringChannel
modestringMode
platformstringPlatform
angle_idstringMessaging angle
cta_typestringCTA type
personalization_levelstringLevel
constraintsobjectConstraint dict
do_not_send_rulesstring[]Rules that should suppress sending
dayintegerSequence placement (updates the sequence doc, not the catalog)
conditionstringSequence placement condition
stop_on_replybooleanSequence placement override

day, condition, and stop_on_reply modify the sequence document; everything else modifies the catalog entry.

Errors

StatusMeaning
404Step 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

StatusMeaning
404Step not found

Launch Campaign

POST /campaigns/{campaign_id}/launch

Query Parameters

ParameterTypeDefaultDescription
validatebooleanWhen 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.

FieldTypeDescription
sender_mailbox_idsinteger[]Explicit sender mailbox IDs from GET /mailboxes. Each must be connection_status="active" and non-archived. Missing IDs return 400.
disable_auto_pickbooleanDefault 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_namestringSince 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_namestringSince 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_idstringSince 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_idintegerSince 0.9.7. Integer from GET /event-types. Stamped onto the sequence so the booking-link rewrite kicks in during send.

Sender selection precedence:

  1. If sender_mailbox_ids is provided → attach exactly those mailboxes.
  2. Else if disable_auto_pick is false (default) → bridge auto-picks the first active email mailbox (ORDER BY id ASC).
  3. Else → legacy empty channels.

Example

cURL

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": [
      { "mailbox_id": 42, "email": "[email protected]", "channel_type": "email", "auto_picked": false }
    ],
    "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

ParameterTypeDescription
campaign_idstringCampaign ID

Example

cURL

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

ParameterTypeDescription
campaign_idstringCampaign ID
image_idstringAd image ID

Example

cURL

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

ParameterTypeDescription
campaign_idstringCampaign ID

Query Parameters

ParameterTypeDefaultDescription
daysinteger30Rolling window in days (1–365)

Example

cURL

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

ParameterTypeDescription
campaign_idstringCampaign ID

Example

cURL

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

ParameterTypeDescription
campaign_idstringCampaign ID

Request Body

All fields are optional.

FieldTypeDescription
namestringCampaign name (max 255 chars)
briefstringCampaign brief
core_conceptstringCore concept
primary_hookstringPrimary hook
target_personastringTarget persona (max 200 chars)
categorystringCampaign category (max 100 chars)
goalstringCampaign goal (max 255 chars)
audience_list_idstringProvide a list ID (or "") to set the audience. Use detach_audience to clear it.
detach_audiencebooleanWhen true, explicitly sets audience_list_id to null
documentsobject[]List of document patch/create objects (see below)
sequence_stepsobject[]Replaces the full sequence ordering
linked_sequence_idsstring[]Link existing sequences (sets status="linked")
unlink_sequence_idsstring[]Unlink sequences. Only deletes status="linked" rows; audit rows are untouched.

documents item fields

FieldTypeDescription
idstringWhen present, updates an existing document. When absent, creates a new document (then file_type and display_name are required).
file_typestringDocument type (required for create)
display_namestringHuman-readable name (required for create)
folder_pathstringOptional folder path
statusstring"draft" or "completed"
contentstringDocument content
structured_dataobjectOptional structured payload

sequence_steps item fields

FieldTypeDescription
step_idstringStep ID (must exist in catalog)
dayintegerDay offset from sequence start
conditionstringOptional condition string
stop_on_replybooleanStop enrollment on reply

Example

cURL

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

ParameterTypeDescription
campaign_idstringCampaign ID

Request Body

FieldTypeDescription
list_idstringAudience list ID to attach. Pass null to detach the current audience.

Example

cURL

# Attach an audience
curl -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 audience
curl -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 }'
Build with graph8