Skip to main content
A campaign is built in a fixed order, and every step is resumable. You can stop after any of them, come back tomorrow, and GET /send/campaigns/{campaign_id} will tell you exactly what’s still missing.
Before your first campaign, connect at least one sender. Nothing launches without one. See account setup.

The recipe

1

Create the campaign

A blank draft. Nothing sends until you launch it.
2

Add senders

Senders belong to the organization; here you pick which ones this campaign draws from. Add several and sends spread across them.
GET …/senders shows the current pool plus the org senders available to add.
3

Declare what you know about each person

The person schema is the People tab’s columns. Declare it before enrolling anyone, so their context has somewhere to land.
Pass {"fields": []} if you only need name, email, and company. Skipping this step entirely gets you 409 PERSON_SCHEMA_REQUIRED when you enroll.
4

Enroll people

From a list — this is where Leads and Send meet:
mapping connects list columns to person-schema fields. why_this_person is the one-line reason this prospect is worth contacting — it’s what the copy generator leans on, so point it at a real column or set default_why_this_person.You can also pass people inline instead of a list_id, and row_ids to take only specific rows. Either way it’s synchronous, up to 1,000 per call.
5

Write the template

A template is one or more variants (A/B arms), each a list of steps.
personalized (and subject_personalized) mark phrases that should be rewritten per recipient. A bare string is the phrase copied verbatim from the body; { text, name, instruction } also names the variable and steers generation. If a phrase doesn’t match the body exactly you get 409 TEMPLATE_SPANS_INVALID.Step channel is email, linkedin_message, linkedin_connect, linkedin_comment, linkedin_react, or manual. manual is persistable, not sendable — include it on a GET → PUT so UI-added steps aren’t dropped. Comment and react steps act on the person’s posts; pass posts when you enroll people inline, or the step waits (on_missing_post: hold) until a post exists.Up to 10 variants, 15 steps each. Add arms one at a time with POST …/templates/sequences rather than replacing the whole template; each keeps a variant_key that is never reused, so stats stay comparable over time.
Don’t want to write it yourself? POST /send/campaigns/draft takes a brief and an optional list_id and fills in the schema and templates for you. It never launches — you still review everything below.
6

Read what will actually go out

Templates are not what people receive. Generate the real per-person copy and read it before anything sends.
That’s one LLM call per person, so it returns a Job. When it finishes, read the results with GET …/examples.A message that isn’t right can be fixed in place with PATCH …/people/{sequence_id}/steps/{step_index}, and POST …/revert throws your edits away and re-renders from the template.
7

Set the schedule

Settings can be patched in any status, including while the campaign is running. Omitted fields are left alone.
8

Dry run, then launch

Check the gates without committing:
A dry run reports exactly what a real launch would check. Drop dry_run to go live.

While it’s running

GET …/people pages everyone with their current status, including bounced and unsubscribed, and takes a search filter. If you turned on require_message_approval, messages queue up until you release them:

Stopping things

stop and remove both accept {"dry_run": true} if you want to see the blast radius first.
Deleting a person is not the same as removing them. DELETE …/people/{sequence_id} erases them from the campaign, which re-opens deduplication — they can be enrolled again and contacted twice. After anything has been sent it requires force=true. Prefer remove.

What’s next

Account setup

Connect senders, buy domains, provision mailboxes.

Webhooks

Get replies, bounces, and opens pushed to you instead of polling.