Your First Campaign
This guide walks through creating a complete campaign: defining a sequence, adding leads with custom variables, and monitoring performance.
Prerequisites
- A connected mailbox (see Mailbox Setup)
- An API key (get one here)
Campaign lifecycle
Campaigns follow a state machine:
draft -> active -> paused -> active -> completed
\-> completed- Draft — Initial state. Add steps, leads, and configure settings.
- Active — Emails are being sent within the send window.
- Paused — Sending is halted. Can be resumed.
- Completed — All leads have finished the sequence.
Only draft and archived campaigns can be deleted.
Step 1: Create the campaign
import { ZendClient } from 'zend-sh'
const zend = new ZendClient({ apiKey: 'zk_live_...' })
const campaign = await zend.campaigns.create({
name: 'Q1 SaaS Outreach',
stop_on_reply: true,
send_window: {
timezone: 'America/New_York',
days: ['monday', 'tuesday', 'wednesday', 'thursday', 'friday'],
start_hour: 9,
end_hour: 17,
},
})The send_window is timezone-aware. Emails only send during the specified hours in the specified timezone, including DST transitions.
Step 2: Add email steps with variable substitution
Create your first email with Handlebars-style variables:
await zend.campaigns.steps.create(campaign.id, {
type: 'email',
position: 1,
subject: 'Quick question, {{first_name|there}}',
body: `Hi {{first_name|there}},
I noticed {{company}} is scaling their outbound efforts. We built zend.sh to handle the infrastructure — mailbox rotation, health monitoring, and deliverability.
The API takes about 5 minutes to set up. Would it make sense to chat this week?
Best,
Your Name`,
})Variable substitution syntax
Variables use double curly braces with an optional fallback after the pipe:
{{first_name}}— Renders the lead'sfirst_namefield, or empty string if missing{{first_name|there}}— Rendersfirst_name, or "there" if missing{{company|your team}}— Renderscompany, or "your team" if missing
The fallback syntax ensures your emails always read naturally, even when data is incomplete.
Step 3: Add delay steps
Add a 3-day delay between emails:
await zend.campaigns.steps.create(campaign.id, {
type: 'delay',
position: 2,
delay_days: 3,
})Then add a follow-up email:
await zend.campaigns.steps.create(campaign.id, {
type: 'email',
position: 3,
subject: 'Re: Quick question, {{first_name|there}}',
body: `Hi {{first_name|there}},
Just circling back on my note from earlier this week. Happy to share how other teams in {{company|your space}} are handling email infrastructure at scale.
Would a quick 15-minute call work?
Best,
Your Name`,
})Step 4: Enroll leads
Add leads with custom fields that map to your template variables:
await zend.campaigns.leads.add(campaign.id, {
leads: [
{
email: 'jane@acme.com',
first_name: 'Jane',
last_name: 'Smith',
company: 'Acme Corp',
},
{
email: 'bob@globex.com',
first_name: 'Bob',
company: 'Globex',
},
{
email: 'alice@initech.com',
first_name: 'Alice',
company: 'Initech',
},
],
})For larger lists, use the import endpoint which handles duplicates:
const result = await zend.campaigns.leads.import(campaign.id, {
leads: largeLeadArray,
})
console.log(`Imported: ${result.imported}, Skipped: ${result.skipped}`)Step 5: Start the campaign
await zend.campaigns.start(campaign.id)Once started, the campaign moves to active state. Emails begin sending within your configured send window. zend.sh automatically:
- Rotates across your connected mailboxes
- Enforces daily send limits per account
- Checks the suppression list before every send
- Appends unsubscribe headers to every email
- Randomizes send times within the window
Step 6: Monitor performance
Check campaign analytics:
const stats = await zend.analytics.campaign(campaign.id)
console.log(stats)Response:
{
"sent": 3,
"delivered": 3,
"bounced": 0,
"replied": 1,
"opened": 2,
"clicked": 0
}Check team-wide overview:
const overview = await zend.analytics.overview()Complete example
Here is the full script to create and launch a campaign:
import { ZendClient } from 'zend-sh'
const zend = new ZendClient({ apiKey: 'zk_live_...' })
// 1. Create campaign
const campaign = await zend.campaigns.create({
name: 'Q1 SaaS Outreach',
stop_on_reply: true,
send_window: {
timezone: 'America/New_York',
days: ['monday', 'tuesday', 'wednesday', 'thursday', 'friday'],
start_hour: 9,
end_hour: 17,
},
})
// 2. Add email step
await zend.campaigns.steps.create(campaign.id, {
type: 'email',
position: 1,
subject: 'Quick question, {{first_name|there}}',
body: 'Hi {{first_name|there}},\n\nI noticed {{company}} is growing fast...',
})
// 3. Add delay
await zend.campaigns.steps.create(campaign.id, {
type: 'delay',
position: 2,
delay_days: 3,
})
// 4. Add follow-up
await zend.campaigns.steps.create(campaign.id, {
type: 'email',
position: 3,
subject: 'Re: Quick question, {{first_name|there}}',
body: 'Hi {{first_name|there}},\n\nJust following up...',
})
// 5. Enroll leads
await zend.campaigns.leads.add(campaign.id, {
leads: [
{ email: 'jane@acme.com', first_name: 'Jane', company: 'Acme' },
{ email: 'bob@globex.com', first_name: 'Bob', company: 'Globex' },
],
})
// 6. Start
await zend.campaigns.start(campaign.id)
console.log(`Campaign ${campaign.id} is now active`)
// 7. Check analytics later
const stats = await zend.analytics.campaign(campaign.id)
console.log(stats)Next steps
- Webhooks — Get real-time notifications on sends, replies, and bounces
- MCP Server — Let AI agents manage campaigns
- API Reference — Complete endpoint documentation