Skip to content

Run a campaign

A campaign writes a sequence of emails to a list of leads, from mailboxes you choose. A lead is a contact on a campaign. Start with a call that changes nothing: ask what a campaign still needs before it can launch.

Check before you launch

import { Colding } from "colding";

const colding = new Colding("cd_your_key");
const check = await colding.campaigns.checkLaunch({
  campaignId: "33333333-3333-4333-8333-333333333333",
});
console.log(check.blockers);

checkLaunch takes a campaign's ID, sends nothing and changes nothing. It needs the read permission.

Example for a new draft with no steps and no mailbox. It prints two blockers.
[
  {
    "code": "no_steps"
  },
  {
    "code": "no_mailboxes"
  }
]

blockers stop a launch until you fix them. warnings never do: they say what to know first, such as no leads yet, an unsubscribe line turned off or a failed payment. The answer also holds revision, which you pass to launch, and counts of the leads, steps and mailboxes.

A blocker is one of these:

no_steps
The sequence has no step.
step_empty
A step has no message. position says which one, and 0 is the first.
variant_empty
A live version of a step has no message.
no_mailboxes
No connected mailbox is on the campaign's list.
no_confirmed_mailboxes
Every mailbox on the list has an address nobody has confirmed yet. An owner confirms each one in the console.
list_invalid_share_high
Too many leads have an address known to be invalid. Take them out, as the leads section shows.
workspace_posture
We limited this workspace's email, so a launch would send nothing.
campaign_limit
The workspace already runs as many campaigns as its plan allows. A paused campaign doesn't count. The blocker carries limit, running and upgradeTo.

See how to launch

Create a campaign

create takes a name and makes a draft with no steps, no leads and no mailboxes. Nothing is sent until a launch. It needs the operate permission.

const campaign = await colding.campaigns.create({
  name: "October introductions",
});
console.log(campaign.id, campaign.status);

kind is sequence, the default, or one_time. You can't change it later. A sequence has up to 50 steps, so it can follow up. A one-time campaign has one step. It sends that step once to the leads it holds at launch, and it finishes when they are done. It never grows by itself.

A new campaign sends on weekdays, in the workspace's time zone, at most 50 emails a day. The schedule section shows how to change that.

A key an owner limited to some campaigns can't create one, and can't edit, pause, launch or delete any other: it answers 403 with details.scope set to keyCampaign. Unless an owner gave it a list of mailboxes and agent inboxes to read, it can still read every campaign. A key with that list reads no campaign: they answer 403 with details.scope set to keyReads.

campaigns.list reads your campaigns, newest first. Give it q to keep only those whose name contains that text. Case doesn't matter, but accents do: cafe doesn't find Café.

const found = await colding.campaigns.list({ q: "october" });
console.log(found.items.map((campaign) => campaign.name));

Write the steps

A step is one email. addStep puts it at the end of the sequence, or earlier if you give a position, where 0 is first. It needs operate.

await colding.campaigns.addStep({
  campaignId: campaign.id,
  subject: "A question about {{ company }}",
  bodyHtml: "<p>Hi {{ first_name }}, are you the right person to ask?</p>",
});
await colding.campaigns.addStep({
  campaignId: campaign.id,
  bodyHtml: "<p>Just checking that my first email reached you.</p>",
  waitDays: 3,
  threadReply: true,
});

subject and bodyHtml are the words. The plain-text part is made from the HTML unless you write bodyText. Both are Liquid templates: the example fills in first_name and company from each lead's contact. Spintax such as {Hi|Hello} is drawn after the template. A template that doesn't parse answers 400 templateInvalid, and nothing is saved.

waitDays is how many days after the step before it goes: 3 if you leave it out, at most 60. The first step has none before it, so its waitDays is not read.

threadReply: true sends the step as a reply in the lead's thread, under that thread's subject. Left out, a blank or repeated subject replies, and a new subject starts a new thread.

A one-time campaign keeps one step. A second one answers 409.

On a running campaign, a new step or an edit to a step also needs the send permission. The launch section says why.

campaigns.updateStep changes what a step says or when it goes: send only what changes. A new position moves the step there, and the others close up around it. The template rules above hold here too. campaigns.removeStep deletes a step and cannot be undone: the steps after it move up one place. Both need operate. Deleting a step only takes something out, so on a running campaign it keeps the approval and needs no send.

const edited = await colding.campaigns.updateStep({
  campaignId: campaign.id,
  stepId: followUp.id,
  waitDays: 5,
});
console.log(edited.waitDays);

await colding.campaigns.removeStep({ campaignId: campaign.id, stepId: oldStep.id });

campaigns.listTemplates lists six emails to start from, in one language: a first email, follow-ups and a last email. Each is written for its language, not translated, and has a subject, a body, the words you must fill in as blanks and the same advice as a preview. It needs read and changes nothing. language is en, pt-BR or es, and en when you leave it out.

const templates = await colding.campaigns.listTemplates({ language: "en" });
const template = templates.find((item) => item.id === "short-question");
if (!template) throw new Error("That template is gone.");
console.log(template.blanks);

const body = template.body
  .replace("[[who you help]]", "freelance designers")
  .replace("[[what you help them do]]", "get paid on time");
await colding.campaigns.addStep({
  campaignId: campaign.id,
  subject: template.subject,
  bodyText: body,
});

Words in double brackets, such as [[who you help]], are the blanks: the parts only you can write. A save that still holds one answers 400 stepBlank and names it, so a step can't be saved with a blank in it. The step keeps its own copy of the words. Nothing points back at the template, and a saved reply from savedReplies.get works the same way. Copy body in as bodyText. If the step already has bodyHtml, send bodyHtml: "" too, because most mail apps show the HTML part.

previewStep shows what a step says to one lead, built the way a send builds it: fields filled in, spintax at its first choice, then the mailbox's signature and the unsubscribe line. It sends and saves nothing.

const preview = await colding.campaigns.previewStep({
  campaignId: campaign.id,
  stepId: step.id,
});
console.log(preview.subject);
console.log(preview.advice.issues.map((issue) => issue.code));

The preview's advice lists what spam filters may notice in the step's own words, without the signature or the unsubscribe line. Each issue has a code, a count and the places that show it. It is advice: it never stops an edit, a launch or a send, and the launch check lists the same steps as a content_advice warning. When wordsChecked is false, the words are in a language we have no list for, or are too short to tell, so finding no spam word means nothing. The other checks still ran.

campaigns.testStep sends a step for real, so you can see it arrive. It goes to the address you give in to, or to your own when you leave it out. It needs the send permission. An address that isn't yours needs a fresh address check first, or recipientReviewed: true for one the check couldn't confirm.

const test = await colding.campaigns.testStep({
  campaignId: campaign.id,
  stepId: step.id,
  mailboxId: "22222222-2222-4222-8222-222222222222",
  to: "[email protected]",
});
const send = await colding.sends.get({ sendId: test.id });
console.log(send.status);

A test changes nothing in the campaign, and the campaign's daily limit does not apply. It does go through the suppression list, the mailbox's gap and daily cap, and the plan's daily sends. contactId picks whose fields fill the step in: the campaign's first lead when you leave it out. variantId sends one of the step's versions instead of the step. A step with no message answers 409 campaignStepEmpty. Read the outcome with sends.get.

Add leads

addLeads takes a selection: the contact IDs, or all: true with filters, the contacts to leave out in exclude and expectedCount, the number you confirmed. If a different number of contacts matches now, nothing is added and you get a 409. Read the count again and pass the new one.

const added = await colding.campaigns.addLeads({
  campaignId: campaign.id,
  selection: { ids: ["11111111-1111-4111-8111-111111111111"] },
});
console.log(added);

A contact who is already a lead is counted in alreadyIn and left as it was. A contact who unsubscribed, or is on the suppression list, can be added, but the campaign never writes to them.

Before a campaign sends, and when you add leads to a running one, it checks how many of the leads that can get mail have an address known to be invalid. Once at least 50 can, that share must stay under 4%, and the check warns from 2%. An add that would pass the limit adds nothing and answers 409 leadsInvalidShare. Only a known-invalid address counts. A lead nobody has checked doesn't, and this is not a forecast of bounces.

campaigns.removeLeads takes out up to 1000 leads by contact ID, or every known-invalid lead with invalid: true. The contacts stay.

campaigns.listLeads says where each address stands for this campaign. standing is eligible, review (a fresh check said risky or couldn't tell, and the campaign's sendToRisky or sendToUnknown switch for that is off), held (it waits for a fresh check) or excluded (never written to).

q looks for each word in the address, the name and the company, capitals ignored, and standing filters the list. Leads come latest added first, a page at a time, and reading them needs read. standingReason says why a lead stands where it does.

const waiting = await colding.campaigns.listLeads({
  campaignId: campaign.id,
  standing: "held",
});
console.log(waiting.items.map((lead) => lead.email));

const removed = await colding.campaigns.removeLeads({
  campaignId: campaign.id,
  invalid: true,
});
console.log(removed);

On a running campaign, adding leads also needs the send permission. The launch section says why.

Take leads from a segment

linkSegment makes a segment one of the campaign's audiences. The contacts it matches now become leads. You give the revision you read, and that is your approval of exactly that audience.

const segment = await colding.segments.get({
  segmentId: "55555555-5555-4555-8555-555555555555",
});
await colding.campaigns.linkSegment({
  campaignId: campaign.id,
  segmentId: segment.id,
  revision: segment.revision,
});

const links = await colding.campaigns.listSegments({ campaignId: campaign.id });
console.log(links.map((link) => link.state));

If the segment changed since, nothing is linked and it answers 409 segmentStale. Read it again, then link it at the new revision.

On a running sequence campaign, contacts who join the segment later become leads too, checked every 2 minutes. Linking a segment to a sequence campaign turns continuous on, so the campaign waits for new members instead of finishing. Set continuous to false to change that. A one-time campaign takes the members it has at launch and no more. A campaign links up to 10 segments.

campaigns.listSegments needs read and answers the linked segments, each with the approvedRevision someone approved, who approved it and a state:

covered
The segment is still at the approved revision, so contacts who join it become leads.
edited
The segment changed since. Link it again at its new revision to approve it.
held
New leads stopped. heldReason says why: list_gate, key_bounds, approver_lapsed or checks_unavailable. Link it again to lift the hold.

Each lead goes through the same checks as addLeads. Editing a segment never changes what a running campaign sends: it only holds that link's new leads until someone approves it again. A contact you removed by hand stays out.

unlinkSegment takes out the leads that link brought and nobody has emailed. A lead already emailed stays, and so does one you added by hand. A lead another linked segment still matches stays too.

Linking needs operate while the campaign is a draft, and send once it holds an approval. You become its approver.

Choose the mailboxes

setSenders replaces the list of mailboxes the campaign sends from with exactly the IDs you give, up to 100. Each must be a connected mailbox in this workspace, or it answers 404. A mailbox kept for transactional email answers 409, because campaigns send only from mailboxes for campaigns and messages (purpose: "cold"). An empty list means the campaign sends from none, never from all.

await colding.campaigns.setSenders({
  campaignId: campaign.id,
  mailboxIds: ["66666666-6666-4666-8666-666666666666"],
});

A lead keeps the mailbox its first email went from, so every step reaches the person from the same address. If that mailbox can't send for now, the lead waits for it. A mailbox whose address nobody confirmed is left out, and its leads go to the others.

To connect a mailbox, ask an owner with a handoff, as the first guide shows.

Set the schedule and pace

update changes a campaign's settings. Send only what changes. It needs operate. These are the ones that set the pace and whom it writes to:

await colding.campaigns.update({
  campaignId: campaign.id,
  dailyLimit: 40,
  schedule: {
    mon: [{ start: "09:00", end: "12:00" }],
    tue: [{ start: "09:00", end: "12:00" }],
    wed: [{ start: "09:00", end: "12:00" }],
    thu: [{ start: "09:00", end: "12:00" }],
    fri: [{ start: "09:00", end: "12:00" }],
    sat: [],
    sun: [],
  },
});
schedule
Every weekday, mon to sun, with up to 4 sending windows each, as HH:MM in the workspace's time zone. An empty list is a day off, and at least one day needs a window.
dailyLimit
Emails a day across all its mailboxes. New campaigns start at 50, and the most you can set is 5000.
rampStart
The first day's limit. After each day the campaign sent, it climbs by rampIncrement, up to dailyLimit. null turns the ramp off.
maxNewLeadsPerDay
Caps the leads that get their first email in a day. Follow-ups never count. null means no cap.
entryDelayMinutes
Makes a lead wait after it is added, up to 90 days.
newLeadsFirst
Sends first emails before follow-ups. Off, follow-ups go first.
continuous
Keeps the campaign running when every lead is done, and waits for leads you add. Off, it finishes. A campaign with no leads yet waits whatever its kind. A one-time campaign can't be continuous: continuous: true on one answers 409 oneTimeNotContinuous.
stopOnReply
A person's reply ends that lead's sequence.
sendToRiskysendToUnknown
sendToRisky and sendToUnknown let a first email go to an address a check called risky, or couldn't judge. Both are off by default, and those leads wait for review.
ccbcc
cc and bcc take up to 5 addresses each, which every email also goes to. The lead sees Cc and never Bcc. An email leaves out the lead's own address, and any address on the suppression list or known to be invalid. Copies count toward no daily limit here, but your email provider counts each one as another recipient.

Launch a campaign

A launch starts the campaign and approves exactly what it sends. It needs the send permission, and you give it the revision the check answered.

const check = await colding.campaigns.checkLaunch({
  campaignId: campaign.id,
});
if (check.blockers.length === 0) {
  const launched = await colding.campaigns.launch({
    campaignId: campaign.id,
    revision: check.revision,
  });
  console.log(launched.status);
}

The workspace owner must also have accepted the acceptable use policy. Until then, a launch, testStep and an edit that adds to what a running campaign sends answer 403 AUP_REQUIRED. The first guide says what to do.

If the campaign changed since the check, it answers 409 campaignChanged and nothing starts. If a blocker is left, it answers 409 campaignNotReady with the blockers in details. When the only blocker is campaign_limit, it answers 402 PLAN_GATE instead.

You become the campaign's approver, and its emails go out under you. If you lose the send permission, or the key is revoked, the campaign pauses as approval_lapsed. An owner can limit a key to some mailboxes, campaigns and streams, and to a number of emails. The campaign must fit inside those limits, or the launch answers 403 with details.scope.

Launching a running campaign at its current revision changes nothing. Launching a paused campaign resumes it. Launching a finished one reopens it, which is how you send to leads you added after it finished.

With the validate permission, a launch also starts address checks for the leads it hasn't written to yet that have no verdict or an old one. Pass checkLeads: false to skip only that launch's own checks.

After the launch, a change that only takes something out needs nothing more. Any other change needs the send permission and makes you the approver. For example:

  • A faster pace: a higher dailyLimit, maxNewLeadsPerDay, rampStart or rampIncrement, or a shorter entryDelayMinutes.
  • A changed schedule, textOnly or unsubscribeMode, tracking turned on, or stopOnReply turned off.
  • A new step, or a step's words, wait or order changed.
  • A mailbox added to the list.
  • A new Cc or Bcc address, or one moved from Bcc to Cc.
  • Adding leads, or linking a segment.
  • A new version of a step, a changed name, message or weight, a paused version resumed, or a winner picked.
  • Turning on sendToRisky or sendToUnknown.

These only take something out, so they keep the approval and need no send: a new name; a lower dailyLimit, rampStart, rampIncrement or new-lead cap, or setting a cap; a longer entry delay; stopOnReply turned on; tracking, sendToRisky or sendToUnknown turned off; newLeadsFirst or continuous changed either way; a Cc or Bcc address removed; a step, a mailbox, leads or a segment removed; a version paused or archived.

Pause, resume, see it finish and delete

A campaign's status is draft, active, paused or completed.

pause stops a running campaign now. Emails that haven't started to go out are canceled, and their leads get those steps again when it resumes. Settings, steps and leads stay. A draft or a finished campaign answers 409 campaignNotRunning. To resume, launch it again, as above. It needs operate.

const paused = await colding.campaigns.pause({
  campaignId: campaign.id,
});
console.log(paused.status, paused.pausedReason);

A running campaign can also pause itself. pausedReason says why, and is null when a person paused it:

no_mailboxes
No connected mailbox is left on its list.
no_confirmed_mailboxes
Every mailbox on its list has an address nobody confirmed. The workspace owner confirms one in the console.
domain_failing
Every mailbox on its list sends from a domain that fails its records.
approval_lapsed
Whoever launched it can no longer send, or it changed in a way nobody approved.
guardrail
At least 50 of its emails went out in 7 days, and 5% bounced or 0.1% were marked as spam. After this pause the count starts over.
list_quality
The share of leads with an address known to be invalid reached 4%. listGateBreach has the counts.
staff
Our team stopped it by hand. staffPauseReason says why.
workspace_closing
The workspace's owner asked to delete it. A launch is refused until they cancel the request.

Fix the cause, then launch again.

A sequence campaign finishes when every lead is done. With continuous on, it keeps running instead and waits for leads you add. A campaign with no leads yet waits too, whatever its kind, and waitingForLeadsSince says since when. A one-time campaign can't be continuous: it finishes when its leads are done.

campaigns.remove deletes a campaign with its steps, its leads and its list of mailboxes. It needs operate and cannot be undone. The contacts stay, and anyone who unsubscribed through the campaign stays unsubscribed. The emails it already sent stay in the send log, with campaignId set to null.

listLog reads what happened to a campaign, newest first: what people and keys changed, and what the campaign did on its own, such as a pause and its reason, a finish, a lead it left out or a copy it dropped. It keeps 90 days.

Give listLog kinds to read one group: changes (edits, steps, mailboxes, leads added or removed, a lead's pause and linked segments), status (launches, pauses, finishes and waiting for leads) or leads (a lead left out or moved, and a copy dropped). Leave it out to read everything.

const history = await colding.campaigns.listLog({
  campaignId: campaign.id,
  kinds: "status",
});
for (const entry of history.items) console.log(entry.at, entry.type);

Follow one lead

getLead says what the campaign does with one lead next, in next.state: due (the lead's own wait is over; the campaign's hours, limits and pauses still apply on top), waits or ended. It needs the read permission.

const lead = await colding.campaigns.getLead({
  campaignId: campaign.id,
  contactId,
});
console.log(lead.next);

await colding.campaigns.holdLead({
  campaignId: campaign.id,
  contactId,
  until: "2026-10-12T09:00:00-03:00",
});

await colding.campaigns.releaseLead({ campaignId: campaign.id, contactId });

When a lead waits, why is one of these:

step_on_its_way
An email is on its way.
step_wait
The next step waits its days after the one before.
entry_delay
A new lead waits the campaign's entry delay.
away
An away message said they are back later.
held
A person paused this lead.
new_leads_cap
Today's first emails are used up. It starts tomorrow.
address_check
Its address waits for a check, or for the campaign to allow a risky or couldn't-tell address.
mailbox_busy
The mailbox it hears from can't send now.

When a lead has ended, why is one of these:

sequence_done
It had every step it can reach.
replied
It answered, and the campaign stops on a reply.
replied_elsewhere
The person answered another of the workspace's campaigns, and this campaign stops on a reply. The lead's repliedElsewhere says which one.
suppressed
Its address is on the suppression list, or it unsubscribed.
invalid
A checking service or your contacts file said its address doesn't exist.
failed
Its next step couldn't be sent, so this lead stopped here.

A person's reply also ends their leads in the workspace's other campaigns, unless the owner turned that off in Settings. It is on by default. It reaches only the campaigns that exist when the reply arrives, so a campaign you create later is not touched, and it never ends a lead whose own campaign keeps writing after a reply. An away message, a reply that asks to unsubscribe and a reply from a Cc or Bcc copy do not do it. The lead's own results never count that reply, and its repliedAt stays null. listLog shows one lead_replied_elsewhere line for each lead it ended.

holdLead pauses one lead: nothing more goes to it from this campaign until until, a date and time with its offset that is still ahead of now (a past one answers 400 leadHoldEndPassed). Without until it lasts until campaigns.releaseLead. An email made for it that hasn't started to go out is canceled. This pause is apart from an away message's hold, and a step waits for whichever ends later. It needs operate, and it keeps the approval, because it changes nothing the campaign sends.

campaigns.releaseLead ends a lead's pause: its next step may go at once, if the step's own wait is over. An away message's hold stays. A lead nobody paused is left as it was. It needs operate.

A lead can wait on its address check though you know the address gets mail. Only a person can say that: the lead's sendAnyway, which listLeads and getLead answer, shows who did it and what it overrode, and no key can set it. To ask a person, create a handoff of kind send_lead_anyway, the campaignId and the lead's contactId, and say in note why you expect the address to get mail. The person opens the lead in the console and sends it anyway, or declines. The request then reads completed or cancelled. It answers 409 for an address on the suppression list, one found invalid, one whose check is still running and one the campaign already writes to. Nobody can send past the first two.

Try two versions of a step

addStepVariant adds another version of a step's message. The step's own words are the original, and a step holds up to 4 versions besides it, paused ones included. Archive one to make room. Each lead draws one version for the step, once, in proportion to the weights, and keeps it: the same lead always gets the same one. The original's weight is the step's weight, and a new version's is 50 unless you give one.

const variant = await colding.campaigns.addStepVariant({
  campaignId: campaign.id,
  stepId: step.id,
  subject: "Quick question for {{ first_name }}",
  bodyHtml: "<p>Are you the right person to ask about this?</p>",
});

await colding.campaigns.pickStepWinner({
  campaignId: campaign.id,
  stepId: step.id,
  variantId: variant.id,
});

On a step that replies in the thread, a version changes the message only. A subject on one answers 400 variantSubjectThreading. A paused version isn't drawn, and if every version is paused the original sends. A step keeps up to 20 archived versions for its results.

campaigns.updateStepVariant changes a version's name, words or weight, or pauses it with paused: true and resumes it with paused: false. Send only what changes. A resume needs a message in the body. A lead that already drew a version keeps it while it can send, and draws again when the version is paused. Any change except a pause alone adds to what the campaign sends, so on a running campaign it needs send, as the launch section says. campaigns.removeStepVariant archives a version: its numbers stay in the results and its name is free again. Leads that drew it draw again among the others, if they have a step left to send. You can't bring it back: add a new one. Both need operate.

await colding.campaigns.updateStepVariant({
  campaignId: campaign.id,
  stepId: step.id,
  variantId: "77777777-7777-4777-8777-777777777777",
  paused: true,
});

await colding.campaigns.removeStepVariant({
  campaignId: campaign.id,
  stepId: step.id,
  variantId: "88888888-8888-4888-8888-888888888888",
});

Nothing picks a winner for you. pickStepWinner takes a version's ID, or original. The chosen message becomes the step's, the step takes every lead again, and every version is archived. Their numbers stay in the results. The console says it is too early to tell until a version has emailed 100 people.

A new version, a changed name, message or weight, a paused version resumed and a winner all add to what a running campaign sends, so they need the send permission, as the launch section says. Pausing or archiving a version doesn't.

Read the results

get returns the campaign with its engagement, counted in people, not emails: emailed, opened, clicked and replied. A lead who opened five times counts once.

const { engagement } = await colding.campaigns.get({
  campaignId: campaign.id,
});
console.log(engagement.emailed, engagement.opened, engagement.replied);

Only what a person did counts. A mail scanner, an image proxy, anything too soon after the email left and several links followed from one network within seconds are left out. openedByMachinesOnly counts the leads whose emails only machines opened, which says why opened may look low.

An open is the email's image loading, so a person who reads with images off never counts. opened is a floor, not a read rate.

Opens and clicks count only while the campaign tracks them and one of the workspace's tracking domains is in use. Both are off by default. Until then a 0 means nothing was measured.

steps repeats the counts for each step, without replies, because a reply answers the campaign and not a step. Each step with versions also lists the counts for each one. links lists the pages people clicked through to, at most 10, over the last 90 days.

See which tracking domain is in use

Opens and clicks are measured through a tracking domain: a name such as t.acme.com with a CNAME record that points at our servers. trackingDomains.list lists the workspace's tracking domains, oldest first. It needs read. Each one has its hostname, the target its record points at, a status and inUse.

const domains = await colding.trackingDomains.list();
for (const domain of domains) console.log(domain.hostname, domain.status, domain.inUse);

const pending = domains.find((domain) => domain.status === "pending");
if (pending) {
  const checked = await colding.trackingDomains.check({ trackingDomainId: pending.id });
  console.log(checked.status, checked.lastCheck);
}

status is pending until the record is seen, verified once it points at the target, and failing when a verified domain's record is gone or points elsewhere. A campaign's emails carry the tracking image and tracked links only while a domain is inUse: it is verified, our end of the target answers (targetResolves is not false) and we have seen our end serve the name over HTTPS (httpsOkAt is set). That last part is ours to set up, never yours. While no domain is in use, emails go out without the image and with their links as written, and a 0 in the results means nothing was measured. Each email uses the tracking domain that sits under the domain it is sent from (t.acme.io for an email from [email protected]), or the oldest one in use when there is none.

trackingDomains.check looks at a domain's record now, instead of waiting for the hourly check, and answers the domain as it stands. lastCheck says what DNS answered: verified, no_record, wrong_target (with observed, where it leads) or dns_unanswered. A verified domain stops being used only when DNS answers that the record is gone or points elsewhere, never because DNS did not answer. Once the record verifies, we check within minutes that our end serves the name over HTTPS, then every hour. It needs operate.

A workspace can newly verify at most 10 hostnames in 30 days. Past that, a check that would verify answers 429 until the oldest one ages out, and nothing is verified. A check also counts against the workspace's limit on mailbox checks, per minute and per hour, and past that limit it answers 429 with Retry-After.

A key can't add or remove a tracking domain: that needs manage, which a key never holds, so a person does it in Settings. A workspace holds up to 10. While a domain is not verified, dnsHost names the DNS provider that holds its zone, or is null when DNS did not answer in time.

Events a campaign raises

A webhook or events.list gets these when a campaign does something. The webhooks guide shows how to receive them.

campaign.reply_received
Someone answered a campaign email, or their server answered for them.
campaign.paused
A running campaign was paused. The event says why, unless a person or a key paused it.
campaign.finished
Every lead got the whole sequence, so the campaign finished.
campaign.waiting_for_leads
A running campaign has nobody left to write to.

campaign.waiting_for_leads fires once each time a campaign changes into waiting. Its continuous field is true when the campaign keeps running for new leads, and false when it had no leads at all.

Every send.* event has a campaignId: the campaign that sent it, or null for an email that wasn't a campaign's. Events from before this field don't have it, so treat it as optional.

Read the webhooks guide