trackOpen
The trackOpen webhook event reports that the tracking pixel of a sent message was loaded. EmailEngine adds that pixel to outgoing HTML messages when open tracking is on.
When This Event is Triggered
The trackOpen event fires when a request reaches EmailEngine's /open.gif endpoint with a valid signature, which happens when the recipient's mail client loads the tracking pixel embedded in the message.
Open tracking works only when all of these hold:
- The message had an HTML part. A plain text message gets no pixel
- Open tracking was on for that message, either per submission or through the
trackOpenssetting (see Enabling Open Tracking) - A
serviceUrlwas configured when the message was sent, since the pixel needs an absolute URL - The recipient's mail client loads remote images
Limitations
Open tracking is an approximation, not a measurement:
- Image blocking - Many clients block remote images until the reader asks for them
- Privacy proxies - Apple Mail Privacy Protection and similar features fetch the pixel from a relay, so the open is recorded but the IP address and time belong to the proxy rather than the reader
- Text-only viewing - A reader who views the plain text alternative never loads the pixel
- Caching - A client that caches the image reports only the first open
- Prefetching - Security scanners and link-protection services fetch the pixel without a human involved
- Repeat opens - Nothing is deduplicated on EmailEngine's side: a client that does not cache the image loads it on every view, so one message can produce several events
EmailEngine drops the requests it can recognize as automated, see Automated request filtering. It cannot recognize all of them.
Payload Schema
Top-Level Fields
| Field | Type | Required | Description |
|---|---|---|---|
serviceUrl | string or null | Yes | The configured EmailEngine service URL, null if not set |
account | string | Yes | Account ID that sent the tracked message |
date | string | Yes | ISO 8601 timestamp when the open was recorded |
event | string | Yes | Always trackOpen |
data | object | Yes | Open tracking data object (see below) |
The event carries no path or specialUse. The unique event identifier is sent as the HTTP header X-EE-Wh-Event-Id, not in the JSON payload.
Data Object Fields
| Field | Type | Required | Description |
|---|---|---|---|
messageId | string | Yes | Message-ID header of the tracked email, as EmailEngine wrote it when the message was queued. It is the same value the Submit API returned and the messageSent event reports as originalMessageId |
remoteAddress | string | Yes | IP address that requested the tracking pixel, see Client address |
userAgent | string | No | User-Agent header of that request. Absent when the client sent none |
The event carries no folder, message or recipient reference beyond messageId. Record the Message-ID when you submit the message if you need to correlate an open with a recipient.
Client address
remoteAddress is the address of the socket the request came in on, unless the enableApiProxy setting is on, which it is by default. With it on, EmailEngine reads the address from the X-Forwarded-For header the reverse proxy adds:
- When
EENGINE_API_PROXY_ADDRESSESlists your proxies, the header is honored only on a request that arrived from one of them, and the address reported is the first hop in the chain that is not one of your proxies - When it is not set, the first address in the header is trusted from any peer. A client that can reach EmailEngine directly can then name its own address, so set the variable whenever the port is reachable without going through the proxy
Example Payload
{
"serviceUrl": "https://emailengine.example.com",
"account": "user123",
"date": "2025-10-17T06:56:01.430Z",
"event": "trackOpen",
"data": {
"messageId": "<0ee381d9-581a-2a57-6038-15e64c76f108@example.com>",
"remoteAddress": "203.0.113.42",
"userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.0.0 Safari/537.36"
}
}
Without a User-Agent
Some clients fetch the image without a User-Agent header, and then the field is omitted:
{
"serviceUrl": "https://emailengine.example.com",
"account": "marketing",
"date": "2025-10-17T14:30:00.000Z",
"event": "trackOpen",
"data": {
"messageId": "<campaign-2025-q4-001@marketing.example.com>",
"remoteAddress": "198.51.100.7"
}
}
Enabling Open Tracking
Per-Message Tracking
Set trackOpens on the submission:
curl -X POST "https://emailengine.example.com/v1/account/user123/submit" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"from": {
"name": "Sales Team",
"address": "sales@example.com"
},
"to": [
{
"address": "prospect@company.com"
}
],
"subject": "Your requested information",
"html": "<p>Hello,</p><p>Here is the information you requested.</p>",
"trackOpens": true
}'
Messages submitted over the SMTP interface use the X-EE-Tracking-Enabled header instead, which switches both open and click tracking on or off for that message.
Instance-Wide Setting
trackOpens is also a setting, applied to every submission that does not set the field itself:
curl -X POST "https://emailengine.example.com/v1/settings" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"trackOpens": true,
"trackClicks": false
}'
In the admin interface the same switch is Track Email Opens under Configuration > Email Processing.
Which Value Wins
EmailEngine resolves open tracking for each message in this order, taking the first value that is a boolean:
trackOpenson the submissiontrackingEnabledon the submission, or theX-EE-Tracking-Enabledheader for an SMTP submission. This covers both opens and clicks- The
trackOpenssetting - The
trackSentMessagessetting, which covers both opens and clicks and is kept for compatibility - Off
Handling the Event
The same message produces an event every time the pixel is loaded: a second read, another device, or a client that does not cache images. Count opens per Message-ID rather than treating each event as a new reader:
async function handleTrackOpen(event) {
const { date, data } = event;
const sentMessage = await db.sentMessages.findOne({ messageId: data.messageId });
if (!sentMessage) {
return;
}
await db.sentMessages.updateOne(
{ messageId: data.messageId },
{
$set: { opened: true, lastOpenedAt: new Date(date) },
$inc: { openCount: 1 }
}
);
}
Technical Details
How the Tracking Pixel Works
When open tracking applies to a message, EmailEngine:
- Builds a payload naming the account and the Message-ID, base64url encodes it as the
dataparameter, and signs it with the instance'sserviceSecretas thesigparameter - Inserts a 1x1 pixel image tag into the first HTML part of the outgoing message, just before its closing
</body>tag, or appends it when the part has no</body>. A message with several HTML parts gets one pixel
The pixel it inserts:
<img src="https://emailengine.example.com/open.gif?data=eyJhY3QiOiJvcGVuIn0&sig=Zm9vYmFy" style="border: 0px; width:1px; height: 1px;" tabindex="-1" width="1" height="1" alt="">
/open.gif verifies the signature before doing anything else and answers 403 when it does not match, so a tracking URL cannot be forged or edited to name another account. EmailEngine mints a serviceSecret on its own the first time it needs one, so signing is always on; set your own value through the settings API if you want to control it.
A request that passes verification is answered with a 1x1 transparent GIF and Cache-Control: no-store, whether or not a webhook was sent.
Automated Request Filtering
Before sending the webhook, EmailEngine checks the requesting address against:
- The published address ranges of Google's crawlers, which is what fetches the pixel when a Gmail user has not opened the message
- A reverse DNS lookup, treating a hostname under
barracuda.comorspfbl.netas a scanner
A request that matches is written to the log at debug level and no webhook is sent. Everything else is reported, including privacy relays and link-protection scanners that do not identify themselves.
Related Events
- trackClick - Triggered when a tracked link is clicked
- listUnsubscribe - Triggered when a recipient uses the one-click unsubscribe link
- messageSent - Carries the
messageIdthis event refers back to
See Also
- Webhooks Overview - Delivery, retries, headers and signing
- trackClick - The click half of the same tracking configuration
- Sending Emails - Submitting a message with
trackOpensset - SMTP interface - Switching tracking on with
X-EE-Tracking-Enabled - Settings API - Setting
trackOpensandserviceSecret