Guides

Your First Campaign

This guide walks through creating a complete campaign: defining a sequence, adding leads with custom variables, and monitoring performance.

Prerequisites

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's first_name field, or empty string if missing
  • {{first_name|there}} — Renders first_name, or "there" if missing
  • {{company|your team}} — Renders company, 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