Most cold email platforms are built around a dashboard. You log in, click through a setup wizard, paste a CSV, and click send. That works well for small-scale, human-operated outbound. It breaks down when you need to launch campaigns programmatically — when the contacts, sequences, and timing are generated by another system, not entered by a person.
zend.sh is built for the programmatic case. Here's what that looks like in practice.
Provisioning: BYO or guided
Before you can send, you need inboxes. The two paths are:
BYO inboxes — you connect existing inboxes (Google Workspace, Microsoft 365, any IMAP/SMTP provider) via the API. This is the default path for teams that already manage their own mail infrastructure.
Guided Azure provisioning — for teams that want to provision fresh Microsoft 365 inboxes via API, zend.sh can provision them into your Azure tenant. This is an API and operator-driven process: you supply your Azure credentials, the API handles the subscription, tenant, and mailbox setup, and the result is a fully provisioned inbox linked to your sending account. There's no GUI wizard — provisioning happens entirely through API calls or the CLI.
Both paths produce the same API surface once provisioning is done.
The programmatic surface
Once you have inboxes, everything else is available over a consistent REST API. There are over 80 endpoints covering every aspect of the platform: inbox management, campaign setup, contact import, sequence configuration, sending health, deliverability monitoring, and analytics.
TypeScript SDK
import Zend from 'zend-sh';
const client = new Zend({ apiKey: process.env.ZEND_API_KEY });
// Create a campaign
const campaign = await client.campaigns.create({
name: 'Q3 outbound — fintech segment',
inboxes: ['inbox_01abc', 'inbox_02def'],
dailyLimit: 40,
warmup: true,
});
// Bulk-import contacts from a parsed CSV
const contacts = await client.contacts.bulkCreate({
campaignId: campaign.id,
contacts: rows.map((r) => ({
email: r.email,
firstName: r.first_name,
company: r.company,
variables: { role: r.title, industry: r.industry },
})),
});
console.log(`Imported ${contacts.created} contacts`);The TypeScript SDK is published on npm as zend-sh and ships with full types — no separate @types/ package needed.
Python SDK
import zend_sh
client = zend_sh.Zend(api_key=os.environ["ZEND_API_KEY"])
campaign = client.campaigns.create(
name="Q3 outbound — fintech segment",
inboxes=["inbox_01abc", "inbox_02def"],
daily_limit=40,
warmup=True,
)The Python SDK is on PyPI as zend-sh, with both sync and async interfaces.
CLI
The CLI (zend) gives you direct terminal access to the same API surface:
# Scale a running campaign up to a new daily limit
zend scale --campaign cmp_01abc --limit 60
# Import contacts from a CSV
zend contacts import --campaign cmp_01abc --file contacts.csv
# Get a JSON report of campaign health
zend status --campaign cmp_01abc --json | jq '.deliverability'
# Check inbox health across all active inboxes
zend inbox list --json | jq '.[] | select(.bounceRate > 0.02)'The --json flag on most commands makes the output machine-readable, so you can pipe it through jq, capture it in shell scripts, or feed it into CI pipelines.
The full command reference is at /cli.
Launching many campaigns from the terminal
The combination of the API + CLI makes it practical to build outbound pipelines that look more like data pipelines than email marketing:
#!/bin/bash
# Segment a lead list, create campaigns per segment, import contacts
for segment in fintech saas ecommerce; do
CAMPAIGN_ID=$(
zend campaigns create \
--name "Q3 outbound — $segment" \
--inbox-tags "warmed,us-east" \
--daily-limit 40 \
--json | jq -r '.id'
)
python3 scripts/filter_leads.py \
--segment "$segment" \
--output "/tmp/$segment.csv"
zend contacts import \
--campaign "$CAMPAIGN_ID" \
--file "/tmp/$segment.csv"
echo "Launched: $segment → $CAMPAIGN_ID"
doneThis is a straightforward shell script, but the same pattern works from a Python scheduler, a Node.js service, an Inngest function, or a GitHub Actions workflow. The API is just HTTP — any environment that can make HTTP requests can drive it.
A/B testing sequences programmatically
The API exposes full control over sequence configuration, including variant splits. You can configure A/B tests on subject lines, body copy, or send timing without touching a dashboard:
await client.sequences.create({
campaignId: campaign.id,
steps: [
{
delayDays: 0,
variants: [
{
weight: 0.5,
subject: 'Quick question about {{company}}',
body: '...',
},
{
weight: 0.5,
subject: 'Saw you were hiring {{role}}s',
body: '...',
},
],
},
{
delayDays: 3,
subject: 'Following up',
body: '...',
},
],
});Winner selection and follow-up sequence configuration are also available via the API, so you can automate the full experiment lifecycle without manual intervention.
Health monitoring and auto-pause
The platform monitors bounce rates, complaint rates, and blacklist status for every sending inbox continuously. When an inbox crosses the configured thresholds (default: >2% bounce, >0.3% complaint), it's paused automatically and you're notified via webhook.
This is deliberate: human reaction time is too slow to prevent reputation damage once a spike starts. The auto-pause fires faster than any dashboard alert-and-respond cycle.
Webhooks deliver real-time events for inbox health changes, campaign completions, bounce spikes, and reply detection. A typical setup pipes these into a Slack channel and a database, so the ops team has a running feed of what's happening without polling the API.
What's not here yet
This is an early access platform. There's no campaign builder UI, no visual sequence editor, no point-and-click import wizard. If you need those things today, zend.sh isn't the right fit. If you're building email infrastructure that another system drives, or you want the terminal-first workflow described above, that's exactly what this is for.
Full API reference: /docs/api-reference. SDK docs: /docs/sdk/typescript, /docs/sdk/python. CLI reference: /cli.