WarmblyDocs
Endpoint reference

Campaigns

Create campaigns and sequences, manage senders, A/B variants, attachments, ramp and tracking settings, preflight checks, and start or stop sends.

Campaigns are the cold outreach unit in Warmbly. A campaign holds sending rules, a schedule, a sender pool, and an ordered list of sequence steps (email or action nodes). These endpoints cover campaign CRUD, the advanced outreach overrides, per-step A/B variants, attachments, the explicit sender pool, preflight and test sends, start and stop, activity logs, campaign-scoped tracking-domain verification, the nested sequence editor, the template preview helper, and the AI writing assistant.

All errors follow the shared {error, message, code, request_id} envelope documented in error codes. Authentication and the scope model are covered in authentication and permissions.

List campaigns

GET /campaigns

Search and page through the organization's campaigns. Scope READ_CAMPAIGNS · Org permission view_campaigns.

ParameterInTypeDescription
qquerystringFree-text filter on campaign name. Optional.
folderquerystringRestrict to a single folder id. Optional.
cursorquerystringOpaque cursor from the previous page's pagination.next_cursor. Optional.
limitquerystringPage size. Optional.

Response

A data plus pagination envelope. next_cursor is the campaign id to resume from (null on the last page), and total is the unfiltered count.

{
  "data": [
    {
      "id": "8f1d6b2e-2b7a-4c9e-9a1f-0e6d4c3b2a10",
      "user_id": "a2c4...",
      "organization_id": "11111111-2222-3333-4444-555555555555",
      "name": "Q3 outbound",
      "description": "",
      "status": "active",
      "stop_on_reply": true,
      "open_tracking": true,
      "link_tracking": true,
      "text_only": false,
      "daily_limit": 50,
      "unsubscribe_header": true,
      "risky_emails": false,
      "cc": [],
      "bcc": [],
      "start_date": null,
      "end_date": null,
      "timezone": "UTC",
      "days": 62,
      "start_time": "09:00",
      "end_time": "17:00",
      "schedule_windows": [[],[{"start":540,"end":1020}],[],[],[],[],[]],
      "email_tags": ["sales"],
      "folders": [],
      "contact_order_by": "created_at",
      "contact_order_dir": "asc",
      "sender_strategy": "tags",
      "rotation_mode": "round_robin",
      "ramp_enabled": false,
      "ramp_start": 0,
      "ramp_increment": 0,
      "ramp_ceiling": 0,
      "ramp_level": 0,
      "esp_match_mode": "off",
      "max_new_leads_per_day": 0,
      "prioritize_new_leads": false,
      "tracking_domain": "",
      "tracking_domain_verified": false,
      "updated_at": "2026-06-10T12:00:00Z",
      "created_at": "2026-06-01T09:00:00Z"
    }
  ],
  "pagination": {
    "total": 12,
    "next_cursor": "c1_b3BhcXVlLWN1cnNvcg",
    "has_more": true
  }
}

Create a campaign

POST /campaigns

Create a campaign. Only name is required, every other field is optional and applied only when sent (the wizard sends everything at once, a simple modal can send just {name, description} and get sane defaults). Scope WRITE_CAMPAIGNS · Org permission manage_campaigns.

Request body

FieldTypeRequiredDescription
namestringyesCampaign name.
descriptionstringnoFree-text description.
stop_on_replybooleannoStop sending to a contact once they reply.
open_trackingbooleannoInsert the open pixel.
link_trackingbooleannoRewrite links through the tracking ticket service.
text_onlybooleannoSend plain text only.
daily_limitintegernoPer-campaign daily send cap.
unsubscribe_headerbooleannoAdd the RFC 8058 one-click unsubscribe header.
risky_emailsbooleannoAllow sending to risky/unverified addresses.
ccstring[]noStatic CC list.
bccstring[]noStatic BCC list.
start_datestring (RFC 3339)noEarliest send time.
end_datestring (RFC 3339)noLatest send time.
timezonestringnoIANA timezone for the schedule.
daysinteger (0-127)noLegacy weekday bitmask (superseded by schedule_windows).
start_timestringnoLegacy daily start (HH:MM).
end_timestringnoLegacy daily end (HH:MM).
email_tag_idsstring[]noMailbox tag ids that resolve the sender pool (tags strategy).
folder_idsstring[]noFolder ids to file the campaign under.
sender_strategystringnotags (default) or explicit.
rotation_modestringnoHow volume spreads across the chosen mailboxes.
sendersobject[]noExplicit-strategy mailbox pool (see sender input below).
ramp_enabledbooleannoEnable per-campaign daily ramp-up.
ramp_startintegernoRamp starting volume.
ramp_incrementintegernoDaily ramp increment.
ramp_ceilingintegernoRamp ceiling (never raises above the per-mailbox cap).
esp_match_modestringnooff, prefer, or strict.
max_new_leads_per_dayintegernoNew-lead throttle, 0 is unlimited.
prioritize_new_leadsbooleannoPrefer new leads in each send window.
tracking_domainstringnoCampaign-scoped tracking domain (honored only once verified).
sequencesobject[]noInitial sequence steps in order (see create sequence input below).
variantsobject[]noA/B variants for the first step (same shape as create A/B variant).
advanced_overridesobjectnoAdvanced outreach overrides, see advanced settings.

schedule_windows may also be supplied as a 7-element array (indexed by time.Weekday, Sunday = 0) of {start, end} minute intervals. When non-empty it supersedes days/start_time/end_time.

{
  "name": "Q3 outbound",
  "description": "Founders in fintech",
  "stop_on_reply": true,
  "open_tracking": true,
  "link_tracking": true,
  "daily_limit": 40,
  "unsubscribe_header": true,
  "timezone": "America/New_York",
  "email_tag_ids": ["3b0a...", "9d2c..."],
  "sender_strategy": "tags",
  "rotation_mode": "round_robin",
  "ramp_enabled": true,
  "ramp_start": 10,
  "ramp_increment": 2,
  "ramp_ceiling": 40,
  "sequences": [
    { "name": "Step 1", "subject": "Quick question, {{first_name}}", "body_plain": "Hi {{first_name}}...", "wait_after": 0 }
  ]
}

Response

The created Campaign object (same shape as one element of the list data array).

Get a campaign

GET /campaigns/:id

Fetch a single campaign by id. Scope READ_CAMPAIGNS · Org permission view_campaigns.

ParameterInTypeDescription
idpathuuidCampaign id.

Response

A Campaign object (see the list shape).

Update a campaign

PATCH /campaigns/:id

Patch any subset of campaign fields. Omitted fields are left unchanged. The explicit sender list is edited through replace senders, only the sender_strategy/rotation_mode toggles ride this PATCH. Scope WRITE_CAMPAIGNS · Org permission manage_campaigns.

ParameterInTypeDescription
idpathuuidCampaign id.

Request body

Every field is optional. Scalar fields use nullable pointers, so any field you send is applied. Notable fields: name, description, status, stop_on_reply, open_tracking, link_tracking, text_only, daily_limit, unsubscribe_header, risky_emails, cc, bcc, start_date, end_date, timezone, days, start_time, end_time, schedule_windows, email_tags, folders, contact_order_by, contact_order_dir, contact_order_field, sender_strategy, rotation_mode, ramp_enabled, ramp_start, ramp_increment, ramp_ceiling, esp_match_mode, max_new_leads_per_day, prioritize_new_leads, tracking_domain.

{
  "name": "Q3 outbound (renamed)",
  "daily_limit": 35,
  "stop_on_reply": true,
  "schedule_windows": [[],[{"start":540,"end":1020}],[{"start":540,"end":1020}],[],[],[],[]]
}

Response

The updated Campaign object.

Delete a campaign

DELETE /campaigns/:id

Permanently delete a campaign. Scope WRITE_CAMPAIGNS · Org permission manage_campaigns.

ParameterInTypeDescription
idpathuuidCampaign id.

Response

204 No Content with an empty body.

Get advanced settings

GET /campaigns/:id/advanced

Return the campaign's advanced outreach overrides (bounce pipeline, task reliability, A/B testing, reply intent, send-time optimization, preflight, dashboard). Scope READ_CAMPAIGNS · Org permission view_campaigns.

ParameterInTypeDescription
idpathuuidCampaign id.

Response

A CampaignAdvancedSettings object: the campaign id, the overrides block, and updated_at.

{
  "campaign_id": "8f1d6b2e-2b7a-4c9e-9a1f-0e6d4c3b2a10",
  "overrides": {
    "bounce_pipeline": {
      "enabled": true,
      "auto_suppress_on_bounce": true,
      "auto_suppress_on_complaint": true,
      "auto_suppress_on_unsubscribe": true,
      "auto_pause_campaign_on_spike": true,
      "pause_bounce_rate_threshold": 8,
      "pause_complaint_rate_threshold": 1.5
    },
    "task_reliability": { "enabled": true, "dlq_enabled": true, "max_attempts": 5, "execution_window_seconds": 300 },
    "ab_testing": { "enabled": true, "default_winning_rule": "reply_rate", "auto_promote_winner": false, "min_sample_size": 30 },
    "reply_intent": {
      "enabled": true,
      "positive_keywords": ["interested", "pricing"],
      "negative_keywords": ["not interested", "unsubscribe"],
      "out_of_office_keywords": ["out of office", "vacation"],
      "question_keywords": ["?", "how", "price"],
      "auto_create_crm_task": true,
      "auto_pause_on_negative": false,
      "auto_suppress_on_unsubscribe_keyword": true
    },
    "send_time_optimization": {
      "enabled": true,
      "use_contact_timezone": true,
      "default_contact_timezone": "UTC",
      "preferred_hours": [9, 10, 11, 14, 15, 16],
      "weekend_weight_multiplier": 0.5
    },
    "preflight": {
      "enabled": true,
      "check_tracking_domain": true,
      "check_unsubscribe_header": true,
      "check_ab_variant_configured": false,
      "check_daily_limit": true,
      "check_schedule_window": true
    },
    "dashboard": { "enabled": true, "show_suppression_log": true, "show_intent_summary": true, "show_dlq_stats": true }
  },
  "updated_at": "2026-06-10T12:00:00Z"
}

Update advanced settings

PATCH /campaigns/:id/advanced

Replace the campaign's advanced overrides. Scope WRITE_CAMPAIGNS · Org permission manage_settings.

ParameterInTypeDescription
idpathuuidCampaign id.

Request body

FieldTypeRequiredDescription
settingsobjectyesA full AdvancedOutreachSettings block (same shape as overrides above).
{
  "settings": {
    "bounce_pipeline": { "enabled": true, "auto_suppress_on_bounce": true, "auto_suppress_on_complaint": true, "auto_suppress_on_unsubscribe": true, "auto_pause_campaign_on_spike": true, "pause_bounce_rate_threshold": 8, "pause_complaint_rate_threshold": 1.5 },
    "task_reliability": { "enabled": true, "dlq_enabled": true, "max_attempts": 5, "execution_window_seconds": 300 },
    "ab_testing": { "enabled": true, "default_winning_rule": "reply_rate", "auto_promote_winner": false, "min_sample_size": 30 },
    "reply_intent": { "enabled": true, "positive_keywords": [], "negative_keywords": [], "out_of_office_keywords": [], "question_keywords": [], "auto_create_crm_task": true, "auto_pause_on_negative": false, "auto_suppress_on_unsubscribe_keyword": true },
    "send_time_optimization": { "enabled": true, "use_contact_timezone": true, "default_contact_timezone": "UTC", "preferred_hours": [9, 14], "weekend_weight_multiplier": 0.5 },
    "preflight": { "enabled": true, "check_tracking_domain": true, "check_unsubscribe_header": true, "check_ab_variant_configured": false, "check_daily_limit": true, "check_schedule_window": true },
    "dashboard": { "enabled": true, "show_suppression_log": true, "show_intent_summary": true, "show_dlq_stats": true }
  }
}

Response

204 No Content with an empty body.

List A/B variants

GET /campaigns/:id/ab-variants

List the campaign's A/B variants. A variant scoped to a step_id applies to one step, a null sequence id is campaign-level. Scope READ_CAMPAIGNS · Org permission view_campaigns.

The Original is a weighted arm

For a step-scoped test the step's own email is the Original (control) arm. By default it carries an even share, but you can give it an explicit share by creating one is_control: true variant on that step_id whose weight is the Original's share. That row's subject/body are ignored; when the control wins, the step's own content is sent. Control rows are excluded from the A/B analysis variant list.

ParameterInTypeDescription
idpathuuidCampaign id.

Response

A data array of CampaignABVariant objects (no pagination wrapper).

{
  "data": [
    {
      "id": "c1a2...",
      "campaign_id": "8f1d6b2e-2b7a-4c9e-9a1f-0e6d4c3b2a10",
      "step_id": "7e3b...",
      "name": "Subject B",
      "weight": 50,
      "subject": "Worth a look, {{first_name}}?",
      "body_html": "<p>Hi {{first_name}}...</p>",
      "body_plain": "Hi {{first_name}}...",
      "is_control": false,
      "is_active": true,
      "created_at": "2026-06-02T10:00:00Z",
      "updated_at": "2026-06-02T10:00:00Z"
    }
  ]
}

Create an A/B variant

POST /campaigns/:id/ab-variants

Add a variant to the campaign (or to one step via step_id). Scope WRITE_CAMPAIGNS · Org permission manage_settings.

ParameterInTypeDescription
idpathuuidCampaign id.

Request body

FieldTypeRequiredDescription
namestringyesVariant label.
step_iduuidnoStep to scope the variant to, omit for campaign-level.
weightintegernoRelative selection weight (1-100). Shares are these weights normalized across the active arms, so two arms at equal weight split evenly.
subjectstringnoVariant subject template.
body_htmlstringnoVariant HTML body.
body_plainstringnoVariant plain-text body.
is_controlbooleannoMark this as the step's control arm. For a step-scoped test, create one is_control row to set the Original's share; its weight is the Original's share and its content is ignored (the step's own email is sent when the control wins).
is_activebooleannoWhether the variant participates in the split.
metadataobjectnoFree-form metadata.
{
  "name": "Subject B",
  "step_id": "7e3b...",
  "weight": 50,
  "subject": "Worth a look, {{first_name}}?",
  "body_plain": "Hi {{first_name}}...",
  "is_control": false,
  "is_active": true
}

Response

201 Created with the created CampaignABVariant object (see the list variants shape).

Update an A/B variant

PATCH /campaigns/:id/ab-variants/:variantId

Patch a variant. Omitted fields are unchanged. Scope WRITE_CAMPAIGNS · Org permission manage_settings.

ParameterInTypeDescription
idpathuuidCampaign id.
variantIdpathuuidVariant id.

Request body

All fields optional: name, weight, subject, body_html, body_plain, is_control, is_active, metadata.

{ "weight": 70, "is_active": true }

Response

The updated CampaignABVariant object.

Delete an A/B variant

DELETE /campaigns/:id/ab-variants/:variantId

Remove a variant. Scope WRITE_CAMPAIGNS · Org permission manage_settings.

ParameterInTypeDescription
idpathuuidCampaign id.
variantIdpathuuidVariant id.

Response

204 No Content with an empty body.

Get A/B analysis

GET /campaigns/:id/ab-analysis

Return per-variant engagement stats plus the computed winner for the campaign. Scope READ_ANALYTICS · Org permission view_analytics.

ParameterInTypeDescription
idpathuuidCampaign id.

Response

An ABWinnerAnalysis object.

{
  "campaign_id": "8f1d6b2e-2b7a-4c9e-9a1f-0e6d4c3b2a10",
  "variants": [
    {
      "variant_id": "c1a2...",
      "variant_name": "Subject A",
      "total_sent": 120,
      "opened": 78,
      "clicked": 21,
      "replied": 9,
      "bounced": 2,
      "open_rate": 65.0,
      "click_rate": 17.5,
      "reply_rate": 7.5,
      "bounce_rate": 1.7
    }
  ],
  "winner_id": "c1a2...",
  "winner_name": "Subject A",
  "winning_rule": "reply_rate",
  "confidence": "low"
}

List attachments

GET /campaigns/:id/attachments

List the campaign's attachments. Each entry carries a short-lived presigned download url. Scope READ_CAMPAIGNS · Org permission view_campaigns.

ParameterInTypeDescription
idpathuuidCampaign id.

Response

A data array of attachment objects (no pagination wrapper).

{
  "data": [
    {
      "id": "a9f0...",
      "campaign_id": "8f1d6b2e-2b7a-4c9e-9a1f-0e6d4c3b2a10",
      "step_id": null,
      "filename": "one-pager.pdf",
      "size": 248192,
      "mime_type": "application/pdf",
      "url": "https://storage.warmbly.com/...signed...",
      "created_at": "2026-06-05T08:00:00Z"
    }
  ]
}

Upload an attachment

POST /campaigns/:id/attachments

Upload a file to attach to the campaign (or one step). Sent as multipart/form-data, not JSON. Scope WRITE_CAMPAIGNS · Org permission manage_campaigns.

ParameterInTypeDescription
idpathuuidCampaign id.
fileform (multipart)fileRequired. The file to upload (max 15 MB). Executable and script types are rejected.
step_idform (multipart)uuidOptional. Scope the attachment to one sequence step.

Response

201 Created with the created attachment object (same shape as one element of list attachments).

Delete an attachment

DELETE /campaigns/:id/attachments/:attachmentId

Delete a campaign attachment and its stored object. Scope WRITE_CAMPAIGNS · Org permission manage_campaigns.

ParameterInTypeDescription
idpathuuidCampaign id.
attachmentIdpathuuidAttachment id.

Response

204 No Content with an empty body.

Run preflight

POST /campaigns/:id/preflight

Run the campaign's preflight validation checks (tracking domain, unsubscribe header, daily limit, schedule window, A/B configuration, and more) and return a scored report. No mail is sent. Scope SEND_CAMPAIGNS · Org permission send_campaigns.

ParameterInTypeDescription
idpathuuidCampaign id.

Response

A PreflightReport object.

{
  "id": "f2b1...",
  "organization_id": "11111111-2222-3333-4444-555555555555",
  "campaign_id": "8f1d6b2e-2b7a-4c9e-9a1f-0e6d4c3b2a10",
  "passed": false,
  "score": 80,
  "checks": [
    {
      "key": "tracking_domain",
      "passed": false,
      "severity": "warning",
      "message": "Tracking domain is not verified.",
      "remediation": "Verify the campaign tracking domain before sending."
    }
  ],
  "recommendations": ["Verify your tracking domain to improve link attribution."],
  "created_at": "2026-06-10T12:00:00Z"
}

Send a test email

POST /campaigns/:id/test-email

Send a one-off preview of a sequence step to a chosen recipient through a chosen mailbox. Defaults to the first step when step_id is omitted. Scope SEND_CAMPAIGNS · Org permission send_campaigns.

ParameterInTypeDescription
idpathuuidCampaign id.

Request body

FieldTypeRequiredDescription
account_iduuidyesSending mailbox id.
recipientstring (email)yesWhere to send the test.
step_iduuidnoStep to render and send, defaults to the first step.
{
  "account_id": "5c7d...",
  "recipient": "[email protected]",
  "step_id": "7e3b..."
}

Response

{
  "message": "test email sent",
  "recipient": "[email protected]",
  "subject": "Quick question, Alex",
  "account_id": "5c7d..."
}

Start a campaign

POST /campaigns/:id/start

Start (activate) the campaign so it begins sending real mail. Scope SEND_CAMPAIGNS · Org permission send_campaigns.

ParameterInTypeDescription
idpathuuidCampaign id.

Response

{ "status": "started" }

Stop a campaign

POST /campaigns/:id/stop

Stop (pause) an active campaign. Scope SEND_CAMPAIGNS · Org permission send_campaigns.

ParameterInTypeDescription
idpathuuidCampaign id.

Response

{ "status": "stopped" }

Get campaign logs

GET /campaigns/:id/logs

Page through the campaign's activity log (status changes, send events, errors). Scope READ_CAMPAIGNS · Org permission view_campaigns.

ParameterInTypeDescription
idpathuuidCampaign id.
cursorquerystringOpaque cursor from the previous page. Optional.
limitqueryintegerPage size, 1 to 100 (default 50). Optional.

Response

A data plus pagination envelope. Here pagination carries only next_cursor (a string, null on the last page) and has_more.

{
  "data": [
    {
      "id": "9b2c...",
      "campaign_id": "8f1d6b2e-2b7a-4c9e-9a1f-0e6d4c3b2a10",
      "event_type": "campaign_started",
      "message": "Campaign started",
      "metadata": {},
      "created_at": "2026-06-10T12:00:00Z"
    }
  ],
  "pagination": {
    "next_cursor": "9b2c...",
    "has_more": true
  }
}

List campaign senders

GET /campaigns/:id/senders

Return the campaign's explicit sender pool (used when sender_strategy is explicit). Scope READ_CAMPAIGNS · Org permission view_campaigns.

ParameterInTypeDescription
idpathuuidCampaign id.

Response

A data array of sender objects (no pagination wrapper).

{
  "data": [
    {
      "email_account_id": "5c7d...",
      "weight": 1,
      "last_sent_at": "2026-06-10T11:55:00Z",
      "enabled": true
    }
  ]
}

Replace senders

PUT /campaigns/:id/senders

Atomically replace the campaign's explicit sender pool with the supplied list. Scope WRITE_CAMPAIGNS · Org permission manage_campaigns.

ParameterInTypeDescription
idpathuuidCampaign id.

Request body

FieldTypeRequiredDescription
sendersobject[]yesThe full new sender pool. Each item: email_account_id (uuid, required), weight (integer, optional), enabled (boolean, optional).
{
  "senders": [
    { "email_account_id": "5c7d...", "weight": 2, "enabled": true },
    { "email_account_id": "6d8e...", "weight": 1, "enabled": true }
  ]
}

Response

A data array of the resulting sender objects (same shape as list senders).

Verify campaign tracking domain

POST /campaigns/:id/tracking-domain/verify

Resolve the campaign-scoped tracking domain's CNAME and flip tracking_domain_verified to true on success. Scope WRITE_CAMPAIGNS · Org permission manage_campaigns.

ParameterInTypeDescription
idpathuuidCampaign id.

Response

A TrackingDomainStatus object.

{
  "tracking_domain": "track.acme.com",
  "tracking_domain_verified": true,
  "tracking_domain_verified_at": "2026-06-10T12:00:00Z"
}

List sequences

GET /campaigns/:id/steps

Return the campaign's sequence steps in order. Scope READ_CAMPAIGNS · Org permission view_campaigns.

ParameterInTypeDescription
idpathuuidCampaign id.

Response

A bare array of Sequence objects (no envelope).

[
  {
    "id": "7e3b...",
    "name": "Step 1",
    "subject": "Quick question, {{first_name}}",
    "body_plain": "Hi {{first_name}}...",
    "body_html": "<p>Hi {{first_name}}...</p>",
    "body_sync": true,
    "body_code": false,
    "wait_after": 0,
    "position": 0,
    "kind": "email",
    "updated_at": "2026-06-02T10:00:00Z",
    "created_at": "2026-06-02T10:00:00Z"
  }
]

Create a sequence

POST /campaigns/:id/steps

Append a new empty sequence step to the campaign. The step is created with defaults, then edited with PATCH. Scope WRITE_CAMPAIGNS · Org permission manage_campaigns.

ParameterInTypeDescription
idpathuuidCampaign id.

This endpoint takes no request body.

Response

The created Sequence object (see the list sequences shape).

Update a sequence

PATCH /campaigns/:id/steps/:sid

Patch a sequence step: its copy, spacing, node kind, branching tree, or action config. Omitted fields are unchanged. Scope WRITE_CAMPAIGNS · Org permission manage_campaigns.

ParameterInTypeDescription
idpathuuidCampaign id.
sidpathuuidSequence (step) id.

Request body

All fields optional.

FieldTypeRequiredDescription
namestringnoStep label.
subjectstringnoSubject template.
body_plainstringnoPlain-text body template.
body_htmlstringnoHTML body template.
body_syncbooleannoKeep plain and HTML bodies in sync.
body_codebooleannoTreat the body as raw code (no auto-formatting).
wait_afterintegernoMinutes to wait after this step before the next (the spacing model, there is no standalone wait node for email steps).
conditionsobjectnoBranching tree ({branches: [...]}). Send {} or empty branches to clear branching and fall back to linear progression.
kindstringnoemail (default), action, or wait.
actionobjectnoTyped config for non-email nodes. type is the switch (wait, add_tag, remove_tag, unsubscribe, notify, create_task, create_deal, move_deal_stage, run_automation, end), the remaining fields are type-scoped.
{
  "name": "Step 2",
  "subject": "Following up, {{first_name}}",
  "body_plain": "Just bumping this...",
  "wait_after": 2880,
  "conditions": {
    "branches": [
      {
        "branch_id": "b1",
        "target_step_id": null,
        "conditions": [{ "field": "replied", "operator": "ever", "value": null }]
      }
    ]
  }
}

Response

The updated Sequence object.

Delete a sequence

DELETE /campaigns/:id/steps/:sid

Delete a sequence step. Scope WRITE_CAMPAIGNS · Org permission manage_campaigns.

ParameterInTypeDescription
idpathuuidCampaign id.
sidpathuuidSequence (step) id.

Response

200 OK with an empty body.

Preview a template

POST /campaign-template-preview

Render subject and body templates against a sample (or supplied) contact exactly as the send path would, and report parse errors plus any unresolved {{...}} tokens. No side effects, no campaign id. Scope READ_CAMPAIGNS · Org permission view_campaigns.

Request body

FieldTypeRequiredDescription
subjectstringnoSubject template.
body_htmlstringnoHTML body template.
body_plainstringnoPlain-text body template.
contactobjectnoOverride fields on the built-in sample contact: first_name, last_name, email, company, phone, and a custom_fields map of string to string.
{
  "subject": "Hi {{first_name}} at {{company}}",
  "body_plain": "Hey {{first_name}}, I saw {{company}} is hiring. {{unknown_token}}",
  "contact": { "first_name": "Sam", "company": "Globex" }
}

Response

A TemplatePreview object. errors lists template parse errors that would block sending, unresolved lists literal tokens left after render. Both are omitted when empty.

{
  "subject": "Hi Sam at Globex",
  "body_html": "",
  "body_plain": "Hey Sam, I saw Globex is hiring. {{unknown_token}}",
  "unresolved": ["{{unknown_token}}"]
}

Generate copy with the writing assistant

POST /generation/write

Generate outreach copy with the AI writing assistant. Gated to paid and free-trial organizations, and each call consumes one AI credit (refunded if the provider call fails). Supports Idempotency-Key so a retried request is not double-charged. Scope WRITE_CAMPAIGNS · Org permission manage_campaigns.

Request body

FieldTypeRequiredDescription
promptstringyesThe instruction to generate from (max 8000 characters).
tonestringnoDesired tone (for example friendly, direct).
{ "prompt": "Write a 3-line cold intro to a fintech founder about our deliverability tooling.", "tone": "direct" }

Response

{
  "text": "Hi {{first_name}},\n\nNoticed {{company}} is scaling outbound...\n\nWorth a quick chat?",
  "credits_remaining": 248,
  "model": "claude-..."
}

When the organization is out of credits the endpoint returns 402 with code: "insufficient_credits" and the standard envelope. A depleted balance is checked before any provider call, so no completion is ever burned on a 402.

On this page