Self-Hosted Email Open & Click Tracking
Set track_opens and track_clicks on the Temps send API to get a tracking pixel, rewritten links, and a per-event timeline — no SendGrid plan or third-party processor.
Temps Team
May 17, 2026 · 2mo ago
Self-hosted email tracking lets you record open and click events from your own server — no SendGrid plan, no third-party data processor, no monthly tracking fee. Temps ships email open and click tracking as a built-in feature of its transactional email service: 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 Rust binary that handles your git-push deployments.
TL;DR: Set
track_opens: trueandtrack_clicks: trueon the Temps send API. Temps injects a 1×1 pixel before</body>, rewrites allhttp(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 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 an Apache 2.0. The tracking pixel and link-rewriting machinery shipped in v0.0.7; the analytics UI — per-email timeline, aggregate dashboard, per-link click breakdown — shipped in v0.1.0.
Quotable claim 1: Temps ships self-hosted email open and click tracking as a built-in feature — no third-party service required. Tracking is opt-in per email (
track_opens: true/track_clicks: true), free to self-host (Apache 2.0), and part of the same single Rust binary 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:
-
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 302-redirects to the original destination. Links withmailto:,tel:,#anchor, andjavascript:schemes pass through unmodified. -
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 hitsGET /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.
Quotable claim 2: Temps stores the tracked HTML (pixel + rewritten links) separately from the original HTML in the
tracked_html_bodycolumn. Dashboard previews render the original body, so viewing sent mail in the console never triggers your own tracking pixel and inflates open counts.
How to enable email tracking in Temps
Using the REST API (curl)
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
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
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-agentclicked— each link click with the destination URL and link indexdelivered/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.
Quotable claim 3: Temps tracks email opens and clicks at the single-email level (per-event timeline + per-link breakdown) and at the account level (aggregate delivery rate, open rate, click rate, click-to-open rate). All 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 | Apache 2.0 | Proprietary | Proprietary | Proprietary |
| Cost at 100k emails/mo | ~$0 (your server) | ~$30/mo | ~$20/mo | ~$50/mo |
At 100,000 emails per month, self-hosted tracking with Temps costs approximately $0 beyond your server bill, compared to $20–$50/mo with commercial providers. Temps automatically suppresses recipients after permanent bounces and complaints. Commercial providers still include broader deliverability tooling such as IP warming and reputation guidance.
Self-hosted email tracking vs. Flyway / Liquibase-style migration tools
Email tracking is unrelated to schema migration tools, but developers often ask whether self-hosted observability platforms (like Temps) manage their own database schema automatically. Temps handles its own internal migrations via Sea-ORM migration crates — you never need to run Flyway or Liquibase to upgrade Temps itself. For your own application database, Temps provides a managed PostgreSQL service but does not run your application's migrations for you; that stays in your CI/CD pipeline.
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. 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 v0.1.0
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 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 without a $20–$50/mo tracking fee, 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:
- Configure an SMTP provider in Platform Settings (or use the built-in send if you have DKIM set up on your domain).
- Add
"track_opens": trueand/or"track_clicks": trueto each send API call where you want tracking. - Open the email detail page in the Temps console to see the per-event timeline.
- 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 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 full v0.1.0 release notes
Get weekly updates