Skip to content

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.

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

Terminal window
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

Terminal window
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

Terminal window
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

Terminal window
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

Terminal window
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": "sender@acme.com", "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

Terminal window
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

Terminal window
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

Terminal window
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

Terminal window
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

Terminal window
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

Terminal window
# 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 }'