Skip to Content
🚀 Sendbox is live — this documentation is a work in progress.
API ReferenceCampaigns

Campaigns

Campaigns

MethodPathDescription
GET/campaignsList campaigns with stats. Query: q (name contains), folder (empty = no folder), status (comma list), page, limit
POST/campaignsCreate a campaign (name, sendWindowStartHour, sendWindowEndHour, sendDaysOfWeek, startAt, dailyNewLeadCap, folder)
GET/campaigns/:idGet one campaign
PATCH/campaigns/:idUpdate campaign settings
PATCH/campaigns/:id/statusSet status: ACTIVE, PAUSED, or ARCHIVED. Body: status, confirm? (see launch guard below)
DELETE/campaigns/:idDelete 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-previewWhat a delete would remove (owner/admin): leadsOnlyHere, leadsInOtherCampaigns, sends, replies
POST/campaigns/:id/duplicateCopy 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-settingsApply the same settings to up to 500 campaigns. Body: campaignIds, settings (same fields as PATCH .../settings). Returns updated, failed, notFound
GET/campaigns/:id/launch-checkEverything that would stop or weaken a launch (see below)
GET/campaigns/:id/statsSent/opened/clicked/replied/bounced counts (see Stats), plus variants: sent, opened, replied and rates per step and A/B variant
GET/campaigns/:id/timeseriesDaily-bucketed counts. Query: days (max 90)
GET/campaigns/sent-trendsEmails 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. null clears 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’s defaultTimezone day, or the UTC day if it has none. null = no cap.
  • folder: a free-text label to group campaigns; filter with GET /campaigns?folder=.

Stats

GET /campaigns, GET /campaigns/:id/stats and the CSV reports count the same way:

FieldCountsRate field, over
sentCountEmails sent (a 3-step sequence sends 3 per lead)
leadsContactedLeads who got at least one email
openedCount, clickedCountLeads, each counted onceopenedRate, clickedRate, over leadsContacted
repliedCount, positiveCountLeads, each counted oncerepliedRate, positiveRate, over leadsContacted
bouncedCountEmailsbouncedRate, 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:

CodeKindMeaning
NO_STEPS, NO_LEADS, NO_ELIGIBLE_MAILBOXblockerNothing could be sent
TEMPLATE_TOKENSblockerSome leads would receive a raw {{token}}
WARMING_MAILBOXESwarningPool mailboxes still in early warmup
MAILBOXES_CANNOT_SEND, LEADS_WITHOUT_MAILBOXwarningPart of the pool or leads can’t send yet
NO_TIMEZONE, SCHEDULED_STARTwarningNo 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

MethodPathDescription
POST/campaigns/:id/leadsAdd leads, auto-assigning a sending mailbox to each
GET/campaigns/:id/leadsPaginated list of a campaign’s leads. limit up to 1000
DELETE/campaigns/:id/leads/:leadIdRemove a lead from the campaign
POST/campaigns/:id/leads/removeRemove up to 5000 leads at once. Body: leadIds. Returns removed, notInCampaign
POST/campaigns/:id/leads/reassignRe-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

MethodPathDescription
GET/campaigns/:id/stepsList steps in order
POST/campaigns/:id/stepsCreate a step: stepOrder, subject (first step only), bodyTemplate, bodyHtmlTemplate?, delayHoursAfterPrevious?, variantGroup?
PATCH/campaigns/:id/steps/:stepIdUpdate a step
DELETE/campaigns/:id/steps/:stepIdDelete a step
PUT/campaigns/:id/stepsReplace 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-sendSend 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-wordsScan every step for spam-trigger words
GET/campaigns/:id/steps/:stepId/attachmentsList a step’s files and inline images
POST/campaigns/:id/steps/:stepId/attachmentsAdd 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/:attachmentIdRemove 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.

VariableFilled 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 (413 over that).
  • fromAttachmentId copies a file already in an inbox conversation, by the attachment id from GET /replies/thread/:leadId (for example a deck you sent by hand in a reply), so you don’t upload it again. 404 if no such file is in your workspace.
  • inline: true adds an image to show inside the body: PNG, JPEG, GIF or WebP, 1 MB at most. The response has a cid. Show it in bodyHtmlTemplate as <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, size and cid. Inline images also come with dataBase64.

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 abVariantOptimization on, 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 (stepOrder above 0) goes from the same mailbox as Re: + the subject of the first email the lead got, with In-Reply-To / References pointing at the earlier emails, so the lead sees one conversation. A follow-up’s own subject is not used, so it is optional; the first step needs one.

Mailbox pool

MethodPathDescription
GET/campaigns/:id/mailboxesList the pool and which mailboxes are actively sending
POST/campaigns/:id/mailboxes/selectConfigure the pool — mode: "manual" | "tag" | "random"
DELETE/campaigns/:id/mailboxes/:mailboxIdRemove one mailbox from the pool
PUT/campaigns/:id/mailboxesReplace the pool with exactly these mailboxes. Body: mailboxIds. Returns poolSize, added, removed, notFound, leadsOutsidePool
POST/campaigns/:id/mailboxes/removeRemove 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

MethodPathDescription
GET/campaigns/:id/copy-fatigueList flagged steps
POST/campaigns/:id/copy-fatigue/scanRun an on-demand scan
POST/campaigns/:id/copy-fatigue/:stepId/dismissClear a flag without changing the step
POST/campaigns/:id/copy-fatigue/:stepId/applyClear the flag and return the suggested rewrite (does not write it — follow up with PATCH .../steps/:stepId)

CRM

MethodPathDescription
GET/crm/pipelineEvery lead who has replied, grouped by sequence stage
GET/crm/replies?intent=Replies filtered by a specific intent

Check a step before sending

MethodPathDescription
POST/campaigns/:id/steps/previewExactly what one lead would receive for a step, with nothing sent
GET/campaigns/:id/variables-checkEvery 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

MethodPathDescription
GET/campaigns/:id/settingsAll settings of a campaign
PATCH/campaigns/:id/settingsChange any of them; only the fields you send change
GET/timezonesCountries 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.