Skip to main content

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:

  1. The message had an HTML part. A plain text message gets no pixel
  2. Open tracking was on for that message, either per submission or through the trackOpens setting (see Enabling Open Tracking)
  3. A serviceUrl was configured when the message was sent, since the pixel needs an absolute URL
  4. 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​

FieldTypeRequiredDescription
serviceUrlstring or nullYesThe configured EmailEngine service URL, null if not set
accountstringYesAccount ID that sent the tracked message
datestringYesISO 8601 timestamp when the open was recorded
eventstringYesAlways trackOpen
dataobjectYesOpen 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​

FieldTypeRequiredDescription
messageIdstringYesMessage-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
remoteAddressstringYesIP address that requested the tracking pixel, see Client address
userAgentstringNoUser-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_ADDRESSES lists 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:

  1. trackOpens on the submission
  2. trackingEnabled on the submission, or the X-EE-Tracking-Enabled header for an SMTP submission. This covers both opens and clicks
  3. The trackOpens setting
  4. The trackSentMessages setting, which covers both opens and clicks and is kept for compatibility
  5. 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:

  1. Builds a payload naming the account and the Message-ID, base64url encodes it as the data parameter, and signs it with the instance's serviceSecret as the sig parameter
  2. 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.com or spfbl.net as 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.

  • trackClick - Triggered when a tracked link is clicked
  • listUnsubscribe - Triggered when a recipient uses the one-click unsubscribe link
  • messageSent - Carries the messageId this event refers back to

See Also​