# Self-Hosted Email Open & Click Tracking

> Set track_opens and track_clicks on the Temps send API for a tracking pixel, rewritten links, and a per-event timeline — no SendGrid needed.

David Viejo · Published 2026-05-17 · Updated 2026-10-20 · 11 min read

Canonical: https://temps.sh/blog/self-hosted-email-tracking-open-click-analytics

Self-hosted email tracking lets you record open and click events from your own server — no SendGrid plan and no third-party data processor. Temps ships email open and click tracking as a built-in feature of its [transactional email service](https://temps.sh/docs/transactional-email), which sends through a provider you connect (Amazon SES, Scaleway or any SMTP relay): set `track_opens: true` and `track_clicks: true` on the send API, and Temps injects a 1×1 tracking pixel, rewrites every `http(s)://` link, and stores each event with IP, user-agent, and timestamp — all inside the same platform that handles your git-push deployments.

> **TL;DR:** Set `track_opens: true` and `track_clicks: true` on the Temps send API. Temps injects a 1×1 pixel before `</body>`, rewrites all `http(s)://` links through `/api/emails/{id}/track/click/{idx}`, and records every event with IP, user-agent, and timestamp in its PostgreSQL database. The v0.1.0 beta UI adds a per-email event timeline, an aggregate dashboard, and a per-link click breakdown. Privacy trade-offs are real and documented below.

## Can you self-host email open and click tracking?

Yes. Temps ships email open tracking and click tracking as part of its built-in transactional email service, free to self-host under a dual MIT / Apache 2.0 licence. The tracking pixel and link-rewriting machinery shipped in [v0.0.7](https://temps.sh/blog/temps-0-0-7-release-docker-compose-edge-cdn-email-tracking); the analytics UI — per-email timeline, aggregate dashboard, per-link click breakdown — shipped in the v0.1.0 beta line.

Tracking is opt-in per email (`track_opens: true` / `track_clicks: true`), and no third-party tracking service is involved: events are recorded by the same Temps install that handles git-push deployments, web analytics, error tracking, and uptime monitoring.

## How does self-hosted email tracking work?

Temps uses a pure-Rust HTML transformer that processes the email body at send time — no external dependency. Two transforms happen in order:

1. **Link rewriting (click tracking):** Every `<a href="http(s)://...">` in the HTML is replaced with a tracking redirect URL: `/api/emails/{id}/track/click/{link_index}`. When a recipient clicks, Temps records the event and redirects (temporary redirect) to the original destination. Links with `mailto:`, `tel:`, `#anchor`, and `javascript:` schemes pass through unmodified.

2. **Pixel injection (open tracking):** A 1×1 transparent `<img>` tag is inserted immediately before `</body>` (or appended if `</body>` is absent). When an email client renders the image, it hits `GET /api/emails/{id}/track/open`, which returns a real GIF and records the event.

The transformed HTML is stored in a `tracked_html_body` column, separate from the original `html_body`. This separation matters: the Temps dashboard uses the original body for email previews, so browsing your own sent mail does not inflate open counts with phantom events.

Each event is stored in the `email_events` table with `event_type`, `link_url`, `link_index`, `ip_address`, `user_agent`, and a foreign-key cascade to the parent `emails` row. Aggregate counters (`open_count`, `click_count`, `first_opened_at`, `first_clicked_at`) live on the `emails` table itself for fast list rendering.

A separate `email_links` table maps each link index to the original URL and keeps a per-link click count — that is what powers the "which CTA got clicked" view in the dashboard.

## How to enable email tracking in Temps

### Using the REST API (curl)

```bash
curl -X POST https://your-temps.example.com/api/emails \
  -H "Authorization: Bearer $TEMPS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "hello@yourdomain.com",
    "to": ["user@example.com"],
    "subject": "Your receipt",
    "html": "<p>Thanks for your order. <a href=\"https://example.com/receipt/abc123\">View receipt</a>.</p>",
    "track_opens": true,
    "track_clicks": true
  }'
```

Both flags default to `false`, so tracking is opt-in per email. Password resets and magic links sent without these flags get no pixel and no link rewriting.

### Using the Node.js SDK

```typescript
import { sendEmail } from "@temps-sdk/node-sdk";

const { data } = await sendEmail({
  body: {
    from: "hello@yourdomain.com",
    to: ["user@example.com"],
    subject: "Your receipt",
    html: '<p>Thanks for your order. <a href="https://example.com/receipt/abc123">View receipt</a>.</p>',
    track_opens: true,
    track_clicks: true,
  },
});

console.log("Email sent, id:", data?.id);
```

Once sent, query tracking data with `getEmailTracking`, `getEmailEvents`, or `getEmailLinks` from the same SDK.

### Retrieving tracking events

```typescript
import { getEmailEvents, getEmailLinks } from "@temps-sdk/node-sdk";

// Per-email event timeline
const { data: events } = await getEmailEvents({
  path: { id: emailId },
});

// Per-link click breakdown
const { data: links } = await getEmailLinks({
  path: { id: emailId },
});
```

## What the analytics UI shows

Three surfaces, all under the Email section of the Temps console:

### Per-email event timeline

The Analytics tab on the email detail page shows a chronological list of every event:

- `opened` — timestamp of pixel load, with IP and user-agent
- `clicked` — each link click with the destination URL and link index
- `delivered` / `bounced` — provider webhook events from the SMTP/API layer

Useful for support tickets ("did this email arrive?") and for spotting whether an open came from a real user or from a proxy-prefetch.

### Per-link click breakdown

A separate card on the email detail page lists every tracked `<a>` link with its individual click count. The most-clicked CTA sorts to the top. Dead links and ignored CTAs are immediately visible.

### Global aggregate dashboard

The `/emails/events/stats` endpoint powers a dashboard that aggregates across every email sent: delivery rate, open rate, click rate, click-to-open rate, filterable by date range or by individual email.

All tracking data stays in your own PostgreSQL database — nothing is sent to a third-party analytics service.

## Self-hosted vs. commercial email tracking: comparison

| Capability                         | Temps (self-hosted)                                                           | SendGrid                                                                           | Resend                       | Postmark                                                               |
| ---------------------------------- | ----------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | ---------------------------- | ---------------------------------------------------------------------- |
| Open tracking                      | Yes                                                                           | Yes                                                                                | Yes                          | Yes                                                                    |
| Click tracking                     | Yes                                                                           | Yes                                                                                | Yes                          | Yes                                                                    |
| Per-link click breakdown           | Yes                                                                           | Yes                                                                                | Yes                          | Yes                                                                    |
| Aggregate open/click/bounce stats  | Yes                                                                           | Yes                                                                                | Yes                          | Yes                                                                    |
| Separate tracked vs. original HTML | Yes                                                                           | No                                                                                 | No                           | No                                                                     |
| Bounce categorization (hard/soft)  | Raw events only                                                               | Yes                                                                                | Yes                          | Yes                                                                    |
| Suppression list management        | Yes, automatic and domain-scoped                                              | Yes                                                                                | Yes                          | Yes                                                                    |
| A/B testing on email content       | No                                                                            | Yes (paid)                                                                         | Limited                      | No                                                                     |
| Webhook delivery for events        | No                                                                            | Yes                                                                                | Yes                          | Yes                                                                    |
| Self-hosted / full data ownership  | Yes                                                                           | No                                                                                 | No                           | No                                                                     |
| Open-source license                | dual MIT / Apache 2.0                                                         | Proprietary                                                                        | Proprietary                  | Proprietary                                                            |
| Price for 100k emails/mo           | Your provider's sending cost only (for example Amazon SES); tracking included | Essentials starts at $19.95/mo; the 100k price depends on the volume tier you pick | $35/mo (Pro, 100,000 emails) | About $126/mo on Platform ($18 for 10,000, then $1.20 per extra 1,000) |

on each provider's pricing page

At 100,000 emails per month, tracking with Temps adds nothing beyond your server bill and the sending provider you already pay (for example Amazon SES), whereas commercial providers bundle tracking into sending plans that start at $15–$20/mo and cost from $35/mo (Resend Pro) to about $126/mo (Postmark Platform) at this volume (prices from each provider's pricing page). For a full SendGrid comparison, see [Temps vs SendGrid](https://temps.sh/compare/vs-sendgrid). Temps automatically suppresses recipients after permanent bounces and complaints. Commercial providers still include broader deliverability tooling such as IP warming and reputation guidance.

## Honest privacy trade-offs of self-hosted email tracking

Running your own server does not change the privacy calculus. Three things to know:

### Open tracking is unreliable by design

Modern email clients proxy images to protect users:

- **Apple Mail Privacy Protection** loads tracking pixels on Apple servers immediately on arrival, before the user opens the email. Your event log will show an open from Apple's IP range regardless of whether the recipient actually read the message.
- **Gmail's image proxy** does similar prefetching for some accounts.
- **Outlook for Web** caches images server-side.

Treat open rate as a directional signal, not a precision metric. Click-through rate is more reliable because it requires user intent.

### IP and user-agent are personal data under GDPR

Storing IP addresses tied to email recipients is processing of personal data under [GDPR Article 4](https://gdpr-info.eu/art-4-gdpr/). If your recipients are in the EU, you need a legal basis (typically legitimate interest), a privacy notice that mentions tracking, and a path to opt out.

Self-hosting does not relieve you of compliance obligations. It does mean you are not transferring that data to a third-party data processor, which can simplify your records of processing activities.

### Tracking pixels are blockable

Recipients using hardened clients (Thunderbird with images disabled, Proton Mail, etc.) will not trigger your pixel. Don't build product logic that requires an open event to function. Use opens for analytics, not as a gate.

## What is not in the v0.1.0 betas

Honest list of gaps:

- **No A/B testing on email content.** Send two campaigns and compare aggregate stats manually.
- **No automatic bounce categorization.** Hard vs. soft bounce is not surfaced as a UI concept; the raw provider events are stored but not labeled.
- **No manual suppression-list dashboard.** Temps automatically adds permanent bounces and complaints to a domain-scoped suppression list, but it does not yet expose a dedicated management UI.
- **No webhook delivery for tracking events.** Events are queryable via the API but there is no real-time push to external systems.
- **No deliverability scoring.** No SPF / DKIM / DMARC validation in the dashboard. Use [mail-tester.com](https://mail-tester.com/) for that.

## When to use Temps vs. a commercial provider

Temps email tracking is the right choice if you are sending transactional email (password resets, receipts, alerts) and want basic delivery visibility while paying only your SES or SMTP sending cost, with the open and click events kept in your own database, or if your compliance posture requires keeping event data on your own infrastructure.

If you are sending marketing campaigns at volume and need manual suppression workflows, bounce categorization, IP warming advice, or real-time event webhooks to feed a CRM, use SendGrid, Resend, or Postmark for the email layer and connect them to Temps for deployment and observability.

## Quick-start checklist

If you are already on Temps:

1. Connect an email provider — Amazon SES, Scaleway or an SMTP relay — and verify your sending domain ([setup docs](https://temps.sh/docs/transactional-email)). Temps has no built-in SMTP server.
2. Add `"track_opens": true` and/or `"track_clicks": true` to each send API call where you want tracking.
3. Open the email detail page in the Temps console to see the per-event timeline.
4. Browse the aggregate dashboard under Emails → Analytics for account-wide open and click rates.

For new installs, see [how to send transactional email without SendGrid](https://temps.sh/blog/how-to-send-transactional-email-without-sendgrid) for the full provider setup walk-through.

## FAQ

### Does self-hosted email tracking work with plain-text emails?

No. The pixel is an HTML `<img>` tag and link rewriting targets `<a href>` tags. Plain-text emails receive neither. If you send multipart (HTML + plain text), only the HTML part is tracked.

### Can recipients see the tracking pixel?

The pixel is a 1×1 transparent GIF inserted before `</body>`. It is invisible but present in the source. Email clients that block remote images will display a prompt like "Show images?" — the pixel is what triggers that prompt. If the user clicks "show images," the pixel loads and the open is recorded.

### What about Apple Mail Privacy Protection?

Real problem. AMPP loads every image when the email arrives from Apple's own IP range, before the user sees the message. Every email sent to Apple Mail users will appear "opened" immediately. The event IP will be in Apple's ranges (17.x.x.x), which is one signal for discounting these events. Expect this; factor it into how you interpret open rates.

### How is the tracked HTML different from the original?

Two changes: (1) a 1×1 `<img>` is inserted just before `</body>`, and (2) every `<a href="http(s)://...">` is rewritten so its `href` points at `/api/emails/{id}/track/click/{link_index}`. The `email_links` table maps each index back to the original URL. Visually, the email looks identical to the recipient.

### Where do tracking events get stored?

In the control plane's PostgreSQL database in the `email_events` table. The data can be exported via the Temps API or directly via SQL. If you have enabled disk encryption on the host, all PostgreSQL data — including tracking events — is encrypted at rest.

### Can I disable tracking globally?

Yes — simply omit `track_opens` and `track_clicks` from the send API body (or pass `false`). Both default to `false`, so tracking is opt-in per email. There is no "always track everything" global toggle to accidentally leave on.

**Related:** [Read the v0.1.0 beta release notes](https://temps.sh/blog/temps-0-1-0-release-observe-postgres-ha-autofixer) · [Transactional email docs](https://temps.sh/docs/transactional-email)

---

Source: https://temps.sh/blog/self-hosted-email-tracking-open-click-analytics · Index of all pages: https://temps.sh/llms.txt
