How do I create an image pixel for tracking email opens and clicks?

Matthew Whittaker
Co-founder & CTO, Suped
Published 2 May 2025
Updated 27 May 2026
8 min read
Summarize with

To create an image pixel for tracking email opens, generate a unique event ID for each recipient, place a 1x1 image URL in the HTML email, log the request when that URL loads, then return a tiny transparent GIF or PNG. To track clicks, do not use the image pixel. Rewrite each email link through a redirect endpoint, log the click, then send the reader to the original URL with a 302 redirect.
I keep the system simple at first: one tracking domain, one database table for events, one open endpoint, one click endpoint, and a dashboard that separates human-looking activity from proxy or scanner activity. Open tracking is useful, but it is not exact. Image caching, Apple Mail Privacy Protection, Gmail proxying, blocked images, and security scanners all change what the raw event means.
- Open pixel: a hidden image request that records that an email client or proxy requested the asset.
- Click tracking: a tracked link that logs the click before redirecting to the real destination.
- Dashboard data: event IDs, timestamps, campaign IDs, recipient IDs, user agents, IPs, and bot classification.
What the pixel actually does
A tracking pixel is just an image URL. The email client requests that URL while rendering the message, and your server treats the request as an open event. The pixel is usually transparent, 1x1, and placed near the end of the HTML. A deeper explanation of tracking pixels helps with the client-side behavior, but the server side is straightforward.
The mistake I see most often is trying to make the image pixel handle opens and clicks. It cannot do that cleanly. Opens are image loads. Clicks are link visits. They share a tracking domain and event model, but they need different endpoints and different interpretation.

Flowchart showing how an email open pixel and click redirect are logged.
|
|
|
|---|---|---|
Open | Image load | Reach estimate |
Click | Redirect | Intent |
Conversion | Site event | Outcome |
Open and click tracking need separate signals.
Build the open tracker
Start by creating one event ID per recipient per message. Store the campaign, recipient, and message metadata in your database. Put only the opaque event ID and a signature in the URL. Do not put email addresses, names, customer IDs, or raw campaign names in the pixel URL because URLs end up in logs, forwards, screenshots, and proxy caches.
The HTML can be this small. The exact domain should be a tracking subdomain that you control, with TLS enabled and DNS configured before the campaign goes out.
HTML open pixelhtml
<img src='https://track.example.com/o/e_4sd9p1.gif?sig=a91c22f1' width='1' height='1' alt='' style='display:none' />
On the server, validate the signature, record the request, return a transparent image, and send headers that reduce caching. The headers do not stop every proxy cache, but they make your intent clear and help during local testing.
Node open endpointjavascript
import express from 'express'; import crypto from 'crypto'; const app = express(); const gif = Buffer.from( 'R0lGODlhAQABAPAAAP///wAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw==', 'base64' ); const secret = process.env.TRACKING_SECRET; function sign(value) { return crypto .createHmac('sha256', secret) .update(value) .digest('hex') .slice(0, 16); } app.get('/o/:eventId.gif', async (req, res) => { const eventId = req.params.eventId; if (req.query.sig !== sign(eventId)) return res.sendStatus(404); await recordOpen({ eventId, userAgent: req.get('user-agent') || '', ip: req.ip, occurredAt: new Date() }); res.set({ 'Content-Type': 'image/gif', 'Cache-Control': 'no-store, no-cache, must-revalidate, private', 'Pragma': 'no-cache', 'Expires': '0' }); res.end(gif); }); app.listen(3000);
Do not expose recipient data
- Use opaque IDs: store the recipient and campaign lookup on the server, not in the URL.
- Sign each ID: reject guessed, modified, or expired event IDs before logging an event.
- Keep consent records: tell recipients what analytics you collect and respect opt-out rules.
Add click tracking with redirects
For click tracking, rewrite each link before sending. Store the original target URL in your database and send the recipient through a tracking URL. I avoid putting the target URL in a query parameter because that creates open redirect risk and makes link rewriting easier to abuse.
Open tracking
- Trigger: email client or image proxy requests the tiny asset.
- Response: server returns a transparent image with cache-control headers.
- Meaning: a useful reach signal, not proof that a person read the email.
Click tracking
- Trigger: recipient, scanner, or browser requests the tracked link.
- Response: server logs the event and redirects to the stored destination.
- Meaning: a stronger engagement signal, but scanner clicks still need filtering.
The click endpoint should fetch the destination by event ID, log the request, then redirect. Your dashboard can still show raw clicks, but it should mark likely scanner clicks separately. This is where bot click data becomes important, especially in B2B campaigns.
Node click endpointjavascript
app.get('/c/:eventId', async (req, res) => { const eventId = req.params.eventId; const target = await findTargetUrl(eventId); if (!target) return res.sendStatus(404); await recordClick({ eventId, userAgent: req.get('user-agent') || '', ip: req.ip, occurredAt: new Date() }); res.redirect(302, target.url); });
Store the right event data
I store both first-seen timestamps and total counts. The first open or first click is usually the metric shown in campaign reporting. The repeated requests are still valuable because they reveal forwarding, preview panes, retries, proxy behavior, and scanners.
Simple tracking tablesql
create table email_tracking_events ( event_id uuid primary key, campaign_id uuid not null, recipient_id uuid not null, event_type varchar(10) not null, target_url text, first_seen_at timestamptz, last_seen_at timestamptz, open_count integer not null default 0, click_count integer not null default 0, last_user_agent text, last_ip inet );
For a backend dashboard, I would calculate derived fields rather than overwriting the raw event. For example, keep the raw user agent and IP, then add classification fields such as human-looking, proxy, scanner, duplicate, or internal test. That keeps reporting flexible when your classification rules improve.
I also keep a send snapshot for each message version: subject, sender, template ID, tracking domain, and send time. That makes later debugging much easier when a campaign is resent, a link changes, or a test address receives multiple versions. The event table should answer what happened, while the send snapshot explains what the recipient actually received.
|
|
|
|---|---|---|
Event ID | Lookup key | Yes |
User agent | Client clue | Yes |
IP | Network clue | Limited |
Class | Report filter | Derived |
A compact event model keeps the dashboard useful.
Handle accuracy limits honestly
Open tracking is useful when it is treated as a directional signal. It breaks down when it is treated as a precise count of readers. Some clients block images. Some load images through a proxy. Some privacy systems prefetch the pixel. Gmail can cache images, so the timing and network details of the request do not always match the human reader. More detail on Gmail image caching is useful when your dashboard needs to explain odd timestamps or repeated loads.
How I report opens
- Unique opens: one first-seen open per recipient, with proxy loads included but labeled.
- Total opens: all image requests, useful for diagnosis but noisy for engagement.
- Reliable actions: clicks, replies, conversions, and preference center updates carry more weight.
Before trusting production numbers, I send real test messages to different inboxes and inspect the headers, images, and link behavior. Suped's email tester is useful here because it checks the message as delivered, not just the backend code path.
Email tester
Send a real email to this address. Suped opens the report when the test is ready.
?/43tests passed
Preparing test address...
A good test run checks that the pixel loads once in a basic client, that the click redirect lands on the right URL, that the tracking domain has valid TLS, and that the final email still renders correctly with images disabled. I also test unsubscribe and preference links because those should never be blocked by tracking logic.
Protect deliverability and authentication
A tracking pixel does not automatically make an email look suspicious, but sloppy implementation can damage trust. Use a domain you control, publish clean DNS, keep redirects fast, avoid broken TLS, and do not hide the real destination behind confusing copy. The tracking domain should fit the brand and it should not be shared with unrelated senders.
This is where Suped's product fits the workflow around tracking rather than replacing the tracker itself. Suped brings DMARC, SPF, DKIM, blocklist (blacklist), and deliverability checks into one place. For teams that want the best overall DMARC platform for this work, Suped is the practical choice because it detects authentication issues, explains fixes, monitors policy changes, and gives alerts when sending sources start failing.

Suped DMARC dashboard showing email volume, authentication health, and source breakdown
Before a tracking rollout, I check the sending domain and tracking subdomain together. A domain health check catches missing or malformed authentication records, while ongoing DMARC monitoring shows whether real mail is passing once the campaign is live.
- Tracking domain: use TLS, stable DNS, and a subdomain that recipients can recognize.
- Redirect speed: log asynchronously when needed so clicks do not wait on analytics writes.
- Authentication: keep SPF, DKIM, and DMARC healthy before judging campaign engagement.
- Reputation: monitor blocklist and blacklist signals when traffic or complaints change.
Views from the trenches
Best practices
Generate one opaque event ID per recipient and store all meaning on the server side only.
Send the pixel from a tracking domain with working TLS and stable authentication records in place.
Treat opens as directional signals, then use clicks and replies for stronger engagement scoring.
Common pitfalls
Using raw email addresses in pixel URLs exposes personal data in logs and forwards later.
Counting every image request as human engagement inflates reporting after privacy proxy loads.
Redirecting to arbitrary query string URLs creates an open redirect that scanners abuse.
Expert tips
Keep the click target in your database, not in the URL, so links cannot be rewritten.
Use first seen time and total count separately, because retries matter for diagnosis.
Separate machine-looking opens in reporting instead of deleting them from the raw event table.
Marketer from Email Geeks says the first design choice is deciding whether the dashboard needs email opens, click redirects, website visits, or conversion events.
2024-04-18 - Email Geeks
Marketer from Email Geeks says basic opens and clicks can be built into a backend dashboard, but the sender's existing email software should be checked first.
2024-05-02 - Email Geeks
What I would ship first
For a first version, I would ship a signed open pixel, a signed click redirect, one raw event table, a small classifier for proxy and scanner activity, and a dashboard that reports unique opens, total opens, unique clicks, total clicks, and last activity. I would not ship person-level conclusions such as "read" or "ignored" because the signal does not justify that language.
The most important engineering choice is to keep the event ID opaque and the destination URL server-side. That keeps the system safer, easier to debug, and easier to explain to privacy, security, and marketing teams. Once the tracker works, deliverability checks matter because clean tracking code still depends on trusted mail authentication and a healthy sending setup.
