Campaigns
Pull delivery and engagement analytics for your campaigns. List every campaign you've sent, then open any one for a full report — totals, rates, an engagement timeseries, and per-domain / bounce-reason breakdowns.
A campaign is simply every email that shares the same campaignId. You set that ID when sending — see campaignId on email.send, email.bulk, templates.send, and templates.bulkSend. One campaign can span many sends. Campaign IDs are case-insensitive (stored and matched in uppercase), so spring-launch and SPRING-LAUNCH are the same campaign.
All examples assume you've set the API key in the constructor. For multi-tenant usage, pass { apiKey } as the last argument to any method. See Configuration.
List Campaigns
List your campaigns with headline send stats, newest first by default. Supports search, sorting, and pagination.
const { campaigns, total } = await outbound.campaigns.list({
limit: 20,
sortBy: 'recent',
});
console.log(`${total} campaigns`);
for (const c of campaigns) {
console.log(`${c.campaignId} — ${c.status} — ${c.recipients} recipients (${c.deliveryProgress} delivered)`);
}Parameters
| Param | Type | Default | Description |
|---|---|---|---|
page | number | 1 | Page number (1-based) |
limit | number | 20 | Results per page (max 100) |
search | string | — | Case-insensitive substring match on the campaign ID |
sortBy | 'recent' | 'oldest' | 'recipients' | 'name' | 'recent' | Sort order |
startDate | string | — | ISO date — only campaigns with sends on/after this time |
endDate | string | — | ISO date — only campaigns with sends on/before this time |
Response
{
page: 1,
limit: 20,
total: 7,
campaigns: [
{
campaignId: 'SPRING-LAUNCH',
status: 'completed',
jobs: 3,
recipients: 12000,
sent: 11980,
failed: 20,
deliveryProgress: '99.8%',
firstSent: '2026-03-01T09:00:00.000Z',
lastSent: '2026-03-03T14:30:00.000Z'
}
// ...
]
}Response Fields
| Field | Type | Description |
|---|---|---|
page | number | Current page |
limit | number | Page size |
total | number | Total number of campaigns matching the filters |
campaigns[].campaignId | string | The campaign ID (uppercase) |
campaigns[].status | 'active' | 'completed' | 'partial' | 'failed' | Derived from the campaign's jobs |
campaigns[].jobs | number | Number of sends (jobs) in the campaign |
campaigns[].recipients | number | Total recipients across all jobs |
campaigns[].sent | number | Recipients SES accepted |
campaigns[].failed | number | Recipients that failed to send |
campaigns[].deliveryProgress | string | Sent / recipients, e.g. "99.8%" |
campaigns[].firstSent | string | null | ISO date of the first send |
campaigns[].lastSent | string | null | ISO date of the most recent send |
Possible Errors
| Error | Status | Cause |
|---|---|---|
AuthenticationError | 401 | Invalid or missing API key |
BadRequestError | 400 | Invalid date range |
Get Campaign Analytics
Get the full report for a single campaign. By default it covers the campaign's entire lifetime; pass startDate / endDate to narrow the window.
const report = await outbound.campaigns.analytics('spring-launch');
console.log(`Sent: ${report.totals.sent}`);
console.log(`Delivered: ${report.totals.delivered} (${report.rates.deliveryRate})`);
console.log(`Open rate: ${report.rates.openRate} · Click rate: ${report.rates.clickRate}`);TIP
campaignId is case-insensitive. If the campaign has no sends, the call throws NotFoundError (404).
Parameters
| Param | Type | Default | Description |
|---|---|---|---|
campaignId | string | — | Required. The campaign ID (first positional argument) |
startDate | string | lifetime start | ISO date — lower bound of the report window |
endDate | string | lifetime end | ISO date — upper bound of the report window |
tz | string | 'UTC' | IANA timezone for timeseries bucketing, e.g. 'America/New_York' |
Response
Rates are returned as pre-formatted percentage strings (e.g. "98.50%"); counts are numbers.
{
campaignId: 'SPRING-LAUNCH',
period: {
startDate: '2026-03-01T09:00:00.000Z',
endDate: '2026-03-03T14:30:00.000Z',
granularity: 'day',
tz: 'UTC'
},
firstSent: '2026-03-01T09:00:00.000Z',
lastSent: '2026-03-03T14:30:00.000Z',
jobs: 3,
totals: {
recipients: 12000,
sent: 11980,
inTransit: 0,
delivered: 11900,
opened: 6200,
clicked: 1800,
bounced: 60,
complained: 8,
failed: 20,
queued: 0,
processing: 0
},
rates: {
deliveryRate: '99.33%',
openRate: '52.10%',
clickRate: '15.13%',
bounceRate: '0.50%',
complaintRate: '0.07%'
},
timing: {
avgDeliverySeconds: 4,
avgOpenSeconds: 5400
},
domains: [
{ domain: 'gmail.com', sent: 7000, delivered: 6980, deliveryRate: '99.71%' },
{ domain: 'outlook.com', sent: 3000, delivered: 2960, deliveryRate: '98.67%' }
],
bounceReasons: [
{ label: 'Mailbox full', value: 32 },
{ label: 'Invalid address', value: 28 }
],
timeseries: [
{
bucket: '2026-03-01T00:00:00.000Z',
total: 4000, delivered: 3980, opened: 2100, clicked: 600,
bounced: 20, complained: 3, failed: 7
}
// ... one entry per day/hour
]
}Response Fields
| Field | Type | Description |
|---|---|---|
campaignId | string | The campaign ID |
period | object | Effective window: startDate, endDate, granularity ('hour'/'day'), tz |
firstSent / lastSent | string | ISO dates bounding the campaign's actual activity |
jobs | number | Number of sends in the campaign |
totals.recipients | number | All recipient rows (incl. suppressed/queued) |
totals.sent | number | Emails SES attempted (the "Sent" figure) |
totals.inTransit | number | Accepted by SES, no delivery confirmation yet |
totals.delivered | number | Confirmed delivered (cumulative: delivered + opened + clicked) |
totals.opened | number | Confirmed opened (cumulative: opened + clicked) |
totals.clicked | number | Confirmed clicked |
totals.bounced | number | Bounced |
totals.complained | number | Reported as spam (complaints) |
totals.failed / queued / processing | number | Non-billable lifecycle counts |
rates.deliveryRate | string | Delivered / sent |
rates.openRate | string | Opened / delivered |
rates.clickRate | string | Clicked / delivered |
rates.bounceRate | string | Bounced / sent |
rates.complaintRate | string | Complaints / sent |
timing.avgDeliverySeconds | number | null | Avg seconds from accepted → delivered |
timing.avgOpenSeconds | number | null | Avg seconds from accepted → first open |
domains[] | array | Top recipient domains: domain, sent, delivered, deliveryRate |
bounceReasons[] | array | Bounce breakdown: label, value |
timeseries[] | array | Per-bucket counts (bucket is an ISO instant) |
Cumulative counts
Because each email stores only its latest status, delivered/opened/clicked are reconstructed cumulatively (a clicked email also counts as opened and delivered). Rates use conventional denominators: open/click are a % of delivered; bounce/complaint are a % of sent.
Possible Errors
| Error | Status | Cause |
|---|---|---|
AuthenticationError | 401 | Invalid or missing API key |
NotFoundError | 404 | No campaign with that ID (no sends) |
BadRequestError | 400 | Invalid date range (reversed, or > 366 days) |
Practical Examples
Rank your campaigns by open rate
const { campaigns } = await outbound.campaigns.list({ limit: 100 });
const reports = await Promise.all(
campaigns.map((c) => outbound.campaigns.analytics(c.campaignId)),
);
const ranked = reports
.map((r) => ({ id: r.campaignId, openRate: parseFloat(r.rates.openRate) }))
.sort((a, b) => b.openRate - a.openRate);
console.table(ranked);Alert on a high bounce-rate campaign
const report = await outbound.campaigns.analytics('spring-launch');
const bounceRate = parseFloat(report.rates.bounceRate);
if (bounceRate > 5) {
console.warn(
`Campaign ${report.campaignId} bounced at ${report.rates.bounceRate} — top reason: ${report.bounceReasons[0]?.label}`,
);
}Export a campaign's daily timeseries to CSV
const report = await outbound.campaigns.analytics('spring-launch', { tz: 'America/New_York' });
const rows = [
['date', 'delivered', 'opened', 'clicked', 'bounced'],
...report.timeseries.map((p) => [p.bucket, p.delivered, p.opened, p.clicked, p.bounced]),
];
const csv = rows.map((r) => r.join(',')).join('\n');
console.log(csv);