Skip to content

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.

ts
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

ParamTypeDefaultDescription
pagenumber1Page number (1-based)
limitnumber20Results per page (max 100)
searchstringCase-insensitive substring match on the campaign ID
sortBy'recent' | 'oldest' | 'recipients' | 'name''recent'Sort order
startDatestringISO date — only campaigns with sends on/after this time
endDatestringISO date — only campaigns with sends on/before this time

Response

ts
{
  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

FieldTypeDescription
pagenumberCurrent page
limitnumberPage size
totalnumberTotal number of campaigns matching the filters
campaigns[].campaignIdstringThe campaign ID (uppercase)
campaigns[].status'active' | 'completed' | 'partial' | 'failed'Derived from the campaign's jobs
campaigns[].jobsnumberNumber of sends (jobs) in the campaign
campaigns[].recipientsnumberTotal recipients across all jobs
campaigns[].sentnumberRecipients SES accepted
campaigns[].failednumberRecipients that failed to send
campaigns[].deliveryProgressstringSent / recipients, e.g. "99.8%"
campaigns[].firstSentstring | nullISO date of the first send
campaigns[].lastSentstring | nullISO date of the most recent send

Possible Errors

ErrorStatusCause
AuthenticationError401Invalid or missing API key
BadRequestError400Invalid 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.

ts
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

ParamTypeDefaultDescription
campaignIdstringRequired. The campaign ID (first positional argument)
startDatestringlifetime startISO date — lower bound of the report window
endDatestringlifetime endISO date — upper bound of the report window
tzstring'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.

ts
{
  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

FieldTypeDescription
campaignIdstringThe campaign ID
periodobjectEffective window: startDate, endDate, granularity ('hour'/'day'), tz
firstSent / lastSentstringISO dates bounding the campaign's actual activity
jobsnumberNumber of sends in the campaign
totals.recipientsnumberAll recipient rows (incl. suppressed/queued)
totals.sentnumberEmails SES attempted (the "Sent" figure)
totals.inTransitnumberAccepted by SES, no delivery confirmation yet
totals.deliverednumberConfirmed delivered (cumulative: delivered + opened + clicked)
totals.openednumberConfirmed opened (cumulative: opened + clicked)
totals.clickednumberConfirmed clicked
totals.bouncednumberBounced
totals.complainednumberReported as spam (complaints)
totals.failed / queued / processingnumberNon-billable lifecycle counts
rates.deliveryRatestringDelivered / sent
rates.openRatestringOpened / delivered
rates.clickRatestringClicked / delivered
rates.bounceRatestringBounced / sent
rates.complaintRatestringComplaints / sent
timing.avgDeliverySecondsnumber | nullAvg seconds from accepted → delivered
timing.avgOpenSecondsnumber | nullAvg seconds from accepted → first open
domains[]arrayTop recipient domains: domain, sent, delivered, deliveryRate
bounceReasons[]arrayBounce breakdown: label, value
timeseries[]arrayPer-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

ErrorStatusCause
AuthenticationError401Invalid or missing API key
NotFoundError404No campaign with that ID (no sends)
BadRequestError400Invalid date range (reversed, or > 366 days)

Practical Examples

Rank your campaigns by open rate

ts
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

ts
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

ts
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);

Released under the MIT License.