← Back to blog

Email Analytics: Measuring What Actually Matters

Track deliverability, opens, clicks, and bounce rates in one dashboard. See which metrics actually predict inbox placement, then act on the data.

Most email analytics dashboards show vanity metrics. Pretty charts that tell you nothing.

"You got 2.5K opens" → great, but what does that mean? Are your emails getting better or worse? Should you change the subject line or the CTA?

Here's how to measure email performance that actually matters, and how to pull the numbers straight from Tratto instead of building your own tracking pipeline.

The Core Metrics

Every number below comes from one call:

curl "https://api.tratto.email/v1/analytics/summary?period=30d" \
  -H "Authorization: Bearer tratto_live_..."
{
  "data": {
    "period": "30d",
    "totalSent": 50000,
    "delivered": 49500,
    "opened": 24750,
    "clicked": 7425,
    "bounced": 400,
    "complained": 100,
    "deliveryRate": 99,
    "openRate": 50,
    "clickRate": 30,
    "bounceRate": 0.8,
    "avgDeliveryLatencySeconds": 4.2
  }
}

Rates are already computed as percentages: 99 means 99%, not 0.99. No client-side math required.

Delivery Rate

delivered / totalSent = deliveryRate

Why it matters: Foundation metric. If delivery is <95%, something's wrong.

What's good: 95-99% is healthy. <90% means DNS issues or reputation problems.

If it drops: check deliveryRate and bounceRate from the same summary call before anything else:

const summary = await tratto.analytics.getSummary('7d')
 
if (summary.deliveryRate < 95) {
  await alertOncall(`Delivery rate at ${summary.deliveryRate}% over the last 7 days`)
}

Bounce Rate

bounced / totalSent = bounceRate

Hard vs soft:

  • Hard (mailbox doesn't exist): remove from list, never retry
  • Soft (server busy, mailbox full): retry later, no suppression yet

What's good: <2% hard bounce rate. Soft bounces are normal.

How the suppression actually works: you don't write this logic yourself, Tratto already suppresses hard-bounced contacts automatically after the first hard bounce, and after three consecutive soft bounces. This runs in livemode only: bounces produced with a test key leave the contact untouched. If you want to react to it (say, page whoever is on call), listen for the webhook event instead of polling:

if (event.type === 'bounced') {
  // For a bounce, `data` is { type, subType }; `type` is 'Permanent' or 'Transient'
  if (event.data.type === 'Permanent') {
    await notifyOnCall(`Hard bounce: ${event.recipient}, contact suppressed automatically`)
  }
}

Open Rate

opened / delivered = openRate

Why it matters: Indicates relevance. If open rate drops, your subject lines are losing appeal.

What's good: 15-25% for newsletters, 20-40% for transactional (password resets have high open rates).

Important caveat: Open rates are measured via pixel tracking, and the pixel counts more than people. Apple Mail Privacy Protection pre-fetches remote images for the accounts that have it on, which registers as an open nobody performed; Gmail routes images through its own image proxy rather than switching tracking off. Both effects push the reported open rate above the number of humans who actually opened the message, so read it as a trend line over time, not as a headcount.

One more thing to know about the denominator: /v1/analytics/summary computes openRate as opened / delivered flat, with no exclusions. Contacts who opted out of tracking (trackingOptOut) still sit in that denominator and can only ever count as non-openers. The exclusion exists only in campaign statistics, where those recipients are reported separately as untracked, so per-campaign numbers and account-wide numbers won't line up exactly.

Click-Through Rate (CTR)

clicked / opened = clickRate

Why it matters: Shows if recipients care about your message. Opened but didn't click = wrong CTA or content.

What's good: 1-3% for newsletters, 5-15% for marketing campaigns.

Which links get clicked: pull the per-link breakdown for a specific campaign instead of guessing:

curl "https://api.tratto.email/v1/campaigns/camp_abc123/links?limit=10" \
  -H "Authorization: Bearer tratto_live_..."
{
  "data": [
    { "linkUrl": "https://example.com/pricing", "clicks": 142, "uniqueClicks": 98 },
    { "linkUrl": "https://example.com/docs", "clicks": 37, "uniqueClicks": 30 }
  ]
}

Complaint Rate

complained / totalSent = complaint rate

Why it matters: Rising complaint rate damages your sender reputation. ISPs watch this closely.

What's good: <0.1% complaint rate. Anything above 0.3% is a problem.

What happens automatically: a complaint suppresses the contact immediately, same as a hard bounce, no code required on your end. React to it if you want visibility:

if (event.type === 'complained') {
  const rate = complaintsThisWeek / sentThisWeek
  if (rate > 0.003) {
    await alertSecurityTeam('High complaint rate detected')
  }
}

Trend Over Time

Don't just look at yesterday's data. Pull the daily breakdown:

curl "https://api.tratto.email/v1/analytics/timeseries?period=30d" \
  -H "Authorization: Bearer tratto_live_..."
{
  "data": [
    { "date": "2026-07-30", "sent": 5000, "delivered": 4950, "opened": 2475, "bounced": 40 },
    { "date": "2026-07-29", "sent": 4800, "delivered": 4752, "opened": 2376, "bounced": 38 }
  ]
}

Should show:

  • Stable or rising delivery rate
  • Stable or rising open rate
  • Stable or falling complaint rate

Red flags:

  • Delivery rate dropping = reputation issue
  • Open rate dropping = content problem
  • Complaint rate rising = list quality problem

Need history past 90 days? period=180d and period=1y are available too, served from a nightly aggregate rather than in real time, so same-day numbers won't show up until the next day's run. One catch if you move these calls from curl into the SDK: AnalyticsPeriod in @tratto/email stops at 90d, so the two long periods are REST-only for now and passing them to getSummary() is a TypeScript error.

Actions Based on Metrics

If delivery rate drops below 95%:

  • Check SPF/DKIM/DMARC alignment
  • Verify DNS records
  • Contact Tratto support (might be ISP block)

If open rate drops:

  • A/B test subject lines
  • Check send time (morning vs evening)
  • Remove inactive subscribers

If CTR is low:

  • Improve CTA placement
  • Rewrite the CTA copy — Tratto's campaign A/B test covers the subject line (subjectA / subjectB), not the body, so body changes are tested by iterating between sends
  • Shorten email (mobile recipients scrolling less)

If complaint rate rises:

  • Review email content (too salesy?)
  • Check list hygiene (too many purchased addresses?)
  • Reduce sending frequency

Email analytics should answer: "Are my emails working?" If you can't answer that from your dashboard, you're measuring wrong things.

Join Tratto for real email analytics with real-time webhooks.