Campaigns
Campaigns
| Method | Path | Description |
|---|---|---|
GET | /campaigns | List campaigns with stats. Query: q (name contains), folder (empty = no folder), status (comma list), page, limit |
POST | /campaigns | Create a campaign (name, sendWindowStartHour, sendWindowEndHour, sendDaysOfWeek, startAt, dailyNewLeadCap, folder) |
GET | /campaigns/:id | Get one campaign |
PATCH | /campaigns/:id | Update campaign settings |
PATCH | /campaigns/:id/status | Set status: ACTIVE, PAUSED, or ARCHIVED. Body: status, confirm? (see launch guard below) |
DELETE | /campaigns/:id | Delete a campaign (owner/admin). Query: deleteLeads=true also deletes its leads that are in no other campaign. See Deleting a campaign |
GET | /campaigns/:id/delete-preview | What a delete would remove (owner/admin): leadsOnlyHere, leadsInOtherCampaigns, sends, replies |
POST | /campaigns/:id/duplicate | Copy a campaign with its steps, settings and (by default) mailbox pool. Body: name, folder?, includeMailboxPool? (default true). Leads are not copied; the copy is a DRAFT |
PATCH | /campaigns/bulk-settings | Apply the same settings to up to 500 campaigns. Body: campaignIds, settings (same fields as PATCH .../settings). Returns updated, failed, notFound |
GET | /campaigns/:id/launch-check | Everything that would stop or weaken a launch (see below) |
GET | /campaigns/:id/stats | Sent/opened/clicked/replied/bounced counts (see Stats), plus variants: sent, opened, replied and rates per step and A/B variant |
GET | /campaigns/:id/timeseries | Daily-bucketed counts. Query: days (max 90) |
GET | /campaigns/sent-trends | Emails sent per day for every campaign in one call: { dates, trends: { [campaignId]: number[] } }. Campaigns with no send in the window are left out. Query: days (max 90) |
Start date and daily cap
startAt: an ISO date-time with an offset (e.g.2026-10-13T09:00:00+05:30). Nothing is sent before it; after it, the normal send window and days apply.nullclears it.dailyNewLeadCap: the most leads that get their first email from this campaign per day (1 to 100000). Follow-ups are not counted and not limited by it. The day is the campaign’sdefaultTimezoneday, or the UTC day if it has none.null= no cap.folder: a free-text label to group campaigns; filter withGET /campaigns?folder=.
Stats
GET /campaigns, GET /campaigns/:id/stats and the CSV reports
count the same way:
| Field | Counts | Rate field, over |
|---|---|---|
sentCount | Emails sent (a 3-step sequence sends 3 per lead) | |
leadsContacted | Leads who got at least one email | |
openedCount, clickedCount | Leads, each counted once | openedRate, clickedRate, over leadsContacted |
repliedCount, positiveCount | Leads, each counted once | repliedRate, positiveRate, over leadsContacted |
bouncedCount | Emails | bouncedRate, over sentCount |
opensTracked and clicksTracked say whether the campaign tracks opens and clicks. When one is
false, its numbers mean nothing; the dashboard shows them blank.
Deleting a campaign
DELETE /campaigns/:id deletes the campaign with its steps, settings, mailbox pool and lead links.
It returns { deleted, id, leadsDeleted, leadsKept }.
- With
?deleteLeads=true, every lead of the campaign that is in no other campaign is deleted too, with its replies and tags. A lead that is also in another campaign is kept. - Sent emails are kept. They count for mailbox daily limits, the plan’s quota and bounce health.
- Suppression list entries are kept.
Call GET /campaigns/:id/delete-preview first to see the counts.
Only an owner or admin can delete. Through an API key the rules are stricter: only a DRAFT that
has never sent can be deleted (409 otherwise; archive it instead), and deleteLeads=true returns
403. Deleting a campaign with history is done in the dashboard.
Launch guard
GET /campaigns/:id/launch-check returns ready, blockers and warnings, each with a code
and a message:
| Code | Kind | Meaning |
|---|---|---|
NO_STEPS, NO_LEADS, NO_ELIGIBLE_MAILBOX | blocker | Nothing could be sent |
TEMPLATE_TOKENS | blocker | Some leads would receive a raw {{token}} |
WARMING_MAILBOXES | warning | Pool mailboxes still in early warmup |
MAILBOXES_CANNOT_SEND, LEADS_WITHOUT_MAILBOX | warning | Part of the pool or leads can’t send yet |
NO_TIMEZONE, SCHEDULED_START | warning | No timezone set; start date is in the future |
Setting a campaign to ACTIVE while it has warming mailboxes or template-token problems returns
409 with code: "LAUNCH_NEEDS_CONFIRMATION" and the launch report. Send the same request
again with confirm: true to start anyway.
Leads on a campaign
| Method | Path | Description |
|---|---|---|
POST | /campaigns/:id/leads | Add leads, auto-assigning a sending mailbox to each |
GET | /campaigns/:id/leads | Paginated list of a campaign’s leads. limit up to 1000 |
DELETE | /campaigns/:id/leads/:leadId | Remove a lead from the campaign |
POST | /campaigns/:id/leads/remove | Remove up to 5000 leads at once. Body: leadIds. Returns removed, notInCampaign |
POST | /campaigns/:id/leads/reassign | Re-balance leads across the current pool. Body: leadIds or all: true (up to 1000 per call; more: true means call again) |
POST .../leads returns a per-lead result: assigned, suppressed, no_eligible_mailbox,
seg_blocked, or error — a lead that can’t be assigned yet is still added, unassigned, so
nothing is silently dropped.
.../leads/reassign only moves leads that have not been emailed yet by this campaign (a lead
already in conversation keeps its mailbox, so the thread stays with one sender). It returns
reassigned, leftUnassigned, skippedAlreadyEmailed and more.
Sequence steps
| Method | Path | Description |
|---|---|---|
GET | /campaigns/:id/steps | List steps in order |
POST | /campaigns/:id/steps | Create a step: stepOrder, subject (first step only), bodyTemplate, bodyHtmlTemplate?, delayHoursAfterPrevious?, variantGroup? |
PATCH | /campaigns/:id/steps/:stepId | Update a step |
DELETE | /campaigns/:id/steps/:stepId | Delete a step |
PUT | /campaigns/:id/steps | Replace the whole sequence in one call. Body: steps[] (same fields as create). Steps are matched by stepOrder + variantGroup: matching ones are updated, new ones created, missing ones deleted. A step that has already sent cannot be deleted (409) |
POST | /campaigns/:id/steps/:stepId/test-send | Send one step, rendered for a real lead, to any address. Body: to, leadId?, mailboxId?. Nothing is recorded as a campaign send. 5 per minute |
POST | /campaigns/:id/check-spam-words | Scan every step for spam-trigger words |
GET | /campaigns/:id/steps/:stepId/attachments | List a step’s files and inline images |
POST | /campaigns/:id/steps/:stepId/attachments | Add a file. Body: filename, contentType, contentBase64, inline? (default false); or fromAttachmentId (and filename? to rename) to copy a file from an inbox conversation |
DELETE | /campaigns/:id/steps/:stepId/attachments/:attachmentId | Remove a file |
subject, bodyTemplate and bodyHtmlTemplate accept these variables:
Write them in double braces, spelled exactly as below. They are case-sensitive. A misspelt variable
is not replaced: it is sent as written, so {{sender_signature}} or {{Signature}} would reach
the lead as literal text.
| Variable | Filled with |
|---|---|
{{firstName}}, {{lastName}}, {{email}} | The lead’s name and email |
{{companyName}}, {{companyDomain}}, {{jobTitle}} | The lead’s company and role |
{{phoneNumber}}, {{timezone}} | The lead’s phone and timezone |
{{yourCustomField}} | Any custom field on the lead, by its exact name |
{{senderName}} | The sender name of the mailbox this lead is assigned to |
{{signature}} | The signature of the mailbox this lead is assigned to |
- A lead field that is empty for a lead becomes empty text, not the raw
{{token}}. - Signature: put
{{signature}}where you want it, and it goes exactly there (plain-text or HTML signature, each part of the email gets the right form). A step without{{signature}}still gets the signature, added at the bottom automatically. It is never added twice. If the mailbox has no signature, the variable’s line is removed cleanly. - Every lead is sent from its own assigned mailbox, so
{{senderName}}and{{signature}}are that mailbox’s, not one global value.
Step attachments
A step’s files are sent with every lead’s email for that step, and with test sends. Duplicating a campaign copies them.
- A step’s files and images can be 10 MB in all (
413over that). fromAttachmentIdcopies a file already in an inbox conversation, by the attachmentidfromGET /replies/thread/:leadId(for example a deck you sent by hand in a reply), so you don’t upload it again.404if no such file is in your workspace.inline: trueadds an image to show inside the body: PNG, JPEG, GIF or WebP, 1 MB at most. The response has acid. Show it inbodyHtmlTemplateas<img src="cid:THE_CID">.- An inline image is sent only when the body shows it. Saving a body that no longer shows it removes it.
- The list returns
id,filename,contentType,sizeandcid. Inline images also come withdataBase64.
A step with no bodyHtmlTemplate is sent as plain text, unless the campaign tracks opens (open
tracking needs an HTML part, so one is made from the text).
Attachments on a first cold email hurt inbox placement. Prefer a link in the first step, and keep files for follow-ups or replies.
A/B variants and follow-ups
Several steps with the same stepOrder and a different variantGroup are variants of one step.
- Each step picks its variant on its own. A lead that got variant A of step 1 can get variant
B of step 2. (With
abVariantOptimizationon, the pick leans towards the variant with the better reply rate; otherwise it is random.) If the follow-ups must match the first email, give the follow-up steps a single version. - Follow-ups are replies in the same thread. Every step after the first (
stepOrderabove 0) goes from the same mailbox asRe:+ the subject of the first email the lead got, withIn-Reply-To/Referencespointing at the earlier emails, so the lead sees one conversation. A follow-up’s ownsubjectis not used, so it is optional; the first step needs one.
Mailbox pool
| Method | Path | Description |
|---|---|---|
GET | /campaigns/:id/mailboxes | List the pool and which mailboxes are actively sending |
POST | /campaigns/:id/mailboxes/select | Configure the pool — mode: "manual" | "tag" | "random" |
DELETE | /campaigns/:id/mailboxes/:mailboxId | Remove one mailbox from the pool |
PUT | /campaigns/:id/mailboxes | Replace the pool with exactly these mailboxes. Body: mailboxIds. Returns poolSize, added, removed, notFound, leadsOutsidePool |
POST | /campaigns/:id/mailboxes/remove | Remove many mailboxes from the pool. Body: mailboxIds |
Changing the pool does not move leads already assigned. leadsOutsidePool tells you how many
leads are on a mailbox that left the pool; call POST .../leads/reassign to move the ones not
yet emailed.
Copy fatigue
| Method | Path | Description |
|---|---|---|
GET | /campaigns/:id/copy-fatigue | List flagged steps |
POST | /campaigns/:id/copy-fatigue/scan | Run an on-demand scan |
POST | /campaigns/:id/copy-fatigue/:stepId/dismiss | Clear a flag without changing the step |
POST | /campaigns/:id/copy-fatigue/:stepId/apply | Clear the flag and return the suggested rewrite (does not write it — follow up with PATCH .../steps/:stepId) |
CRM
| Method | Path | Description |
|---|---|---|
GET | /crm/pipeline | Every lead who has replied, grouped by sequence stage |
GET | /crm/replies?intent= | Replies filtered by a specific intent |
Check a step before sending
| Method | Path | Description |
|---|---|---|
POST | /campaigns/:id/steps/preview | Exactly what one lead would receive for a step, with nothing sent |
GET | /campaigns/:id/variables-check | Every step checked against every lead in the campaign (up to 5000) |
POST /campaigns/:id/steps/preview body: stepId (a saved step) or subject + bodyTemplate
(+ bodyHtmlTemplate) to check a draft; optional leadId (any lead in the workspace; default: the
campaign’s first lead) and mailboxId (default: that lead’s assigned mailbox). It runs the same
code as the real send, so subject, text and html are what the lead gets (the tracking pixel
and click redirects are left out). The response also has signature (at_token,
appended_at_bottom or none), variables (used, unknown = would be sent as literal text,
malformed, empty for this lead) and plain-English notes.
GET /campaigns/:id/variables-check returns, per step, the tokens used, unknown and empty
tokens with how many leads each affects, and malformed text; ok is true when no lead would
receive a raw {{token}}. Run it before starting a campaign.
Campaign settings and timezone
| Method | Path | Description |
|---|---|---|
GET | /campaigns/:id/settings | All settings of a campaign |
PATCH | /campaigns/:id/settings | Change any of them; only the fields you send change |
GET | /timezones | Countries with their timezones and today’s UTC offsets |
The send window (sendWindowStartHour / sendWindowEndHour on the campaign) is in each lead’s own
timezone when the lead has one, otherwise in defaultTimezone. Set defaultTimezone to an IANA
zone from /timezones (e.g. America/New_York, Asia/Kolkata); an unknown zone is rejected with
a 400. With no timezone at all, a lead can be emailed at any hour.