trackClick
The trackClick webhook event reports that a rewritten link in a sent message was followed. EmailEngine rewrites the links of outgoing HTML messages through its own /redirect endpoint when click tracking is on.
When This Event is Triggered
The trackClick event fires when a request reaches EmailEngine's /redirect endpoint with a valid signature, which happens when the recipient follows a rewritten link. EmailEngine records the click and then redirects the browser to the original destination.
Click tracking works only when all of these hold:
- The message had an HTML part with
http:orhttps:links. Links in the plain text alternative are never rewritten - Click tracking was on for that message, either per submission or through the
trackClickssetting (see Enabling Click Tracking) - A
serviceUrlwas configured when the message was sent, since the rewritten link needs an absolute URL
Limitations
- Plain text parts - Only HTML parts are rewritten, so a reader in plain text mode follows the original link and nothing is recorded
- Non-HTTP schemes -
mailto:,tel:and other schemes are left alone - Link prefetching - Security scanners and link-protection services follow links without a human involved
- Visible rewriting - The recipient sees an EmailEngine URL in the status bar rather than the destination, and some corporate mail filters rewrite it again on top
EmailEngine drops the requests it can recognize as automated, see Automated request filtering. It cannot recognize all of them.
Common Use Cases
- Email engagement analytics - Track click-through rates for marketing campaigns
- Link performance analysis - Identify which links in your emails get the most engagement
- Sales follow-up - Know when a prospect clicks on a proposal link
- Content optimization - Determine which call-to-action buttons perform best
- A/B testing - Compare click rates across different email variations
- User journey tracking - Understand recipient behavior by tracking link interactions
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 click was recorded |
event | string | Yes | Always trackClick |
data | object | Yes | Click 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 |
url | string | Yes | The destination URL, exactly as it appeared in the message before rewriting |
remoteAddress | string | Yes | IP address that followed the link. Behind a reverse proxy this is the client address taken from X-Forwarded-For, and only when the request came from an address listed in EENGINE_API_PROXY_ADDRESSES |
userAgent | string | No | User-Agent header of that request. Absent when the client sent none |
The event carries no recipient reference. Record the Message-ID when you submit the message if you need to correlate a click with a recipient.
Example Payload
{
"serviceUrl": "https://emailengine.example.com",
"account": "user123",
"date": "2025-10-17T06:56:27.882Z",
"event": "trackClick",
"data": {
"messageId": "<0ee381d9-581a-2a57-6038-15e64c76f108@example.com>",
"url": "https://example.com/product-page",
"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 follow the link without a User-Agent header, and then the field is omitted:
{
"serviceUrl": "https://emailengine.example.com",
"account": "marketing",
"date": "2025-10-17T14:32:15.000Z",
"event": "trackClick",
"data": {
"messageId": "<campaign-2025-q4-001@marketing.example.com>",
"url": "https://shop.example.com/promo?code=SAVE20",
"remoteAddress": "198.51.100.7"
}
}
Enabling Click Tracking
Per-Message Tracking
Set trackClicks 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": "Marketing Team",
"address": "marketing@example.com"
},
"to": [
{
"address": "customer@company.com"
}
],
"subject": "Check out our latest products",
"html": "<p>Hello!</p><p>Visit our <a href=\"https://shop.example.com\">online store</a>.</p>",
"trackClicks": 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
trackClicks 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 '{
"trackClicks": true,
"trackOpens": false
}'
In the admin interface the same switch is Track Link Clicks under Configuration > Email Processing.
Which Value Wins
EmailEngine resolves click tracking for each message in this order, taking the first value that is a boolean:
trackClickson the submissiontrackingEnabledon the submission, or theX-EE-Tracking-Enabledheader for an SMTP submission. This covers both opens and clicks- The
trackClickssetting - The
trackSentMessagessetting, which covers both opens and clicks and is kept for compatibility - Off
Handling the Event
Basic Handler
async function handleTrackClick(event) {
const { account, date, data } = event;
console.log(`Link clicked for account ${account}`);
console.log(` Message-ID: ${data.messageId}`);
console.log(` URL: ${data.url}`);
console.log(` Clicked at: ${date}`);
console.log(` From IP: ${data.remoteAddress}`);
// Update your database or analytics system
await recordLinkClick({
messageId: data.messageId,
clickedUrl: data.url,
clickedAt: new Date(date),
ipAddress: data.remoteAddress,
userAgent: data.userAgent
});
}
Tracking Click Patterns
Analyze which links perform best across your campaigns:
async function handleTrackClick(event) {
const { data, date } = event;
// Parse the URL to categorize the click
const clickedUrl = new URL(data.url);
// Track clicks by domain and path
await analytics.trackEvent('email_link_click', {
messageId: data.messageId,
url: data.url,
domain: clickedUrl.hostname,
path: clickedUrl.pathname,
timestamp: date
});
// Track specific call-to-action buttons
if (clickedUrl.pathname.includes('/buy') || clickedUrl.pathname.includes('/purchase')) {
await analytics.trackConversion('purchase_click', {
messageId: data.messageId,
timestamp: date
});
}
}
Correlating Clicks with Sent Messages
Use the messageId to link clicks back to your original sent messages:
async function handleTrackClick(event) {
const { data } = event;
// Find the original message in your database
const sentMessage = await db.sentMessages.findOne({
messageId: data.messageId
});
if (sentMessage) {
// Update click stats
await db.sentMessages.updateOne(
{ messageId: data.messageId },
{
$set: { hasClicks: true },
$inc: { clickCount: 1 },
$push: {
clicks: {
url: data.url,
clickedAt: new Date(event.date),
ipAddress: data.remoteAddress
}
}
}
);
// Notify sales team if this is a high-value link
if (data.url.includes('/pricing') || data.url.includes('/demo')) {
await notifySalesTeam(sentMessage, event);
}
}
}
Technical Details
How Link Rewriting Works
When click tracking applies to a message, EmailEngine, in each HTML part of the outgoing message:
- Finds every
<a href="http...">attribute - Builds a payload naming the account, the Message-ID and the original URL, base64url encodes it as the
dataparameter, and signs it with the instance'sserviceSecretas thesigparameter - Replaces the
hrefwith a/redirectURL carrying those two parameters
A rewritten link:
<a href="https://emailengine.example.com/redirect?data=eyJhY3QiOiJjbGljayJ9&sig=Zm9vYmFy">View Product</a>
/redirect verifies the signature before doing anything else and answers 403 when it does not match, so a tracking URL cannot be edited to redirect somewhere else. 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.
Links That Are Not Rewritten
Two kinds of link are left as they are, so that tracking cannot break them:
- A link to this instance's own
/unsubscribeendpoint, which carries its own signed payload - A link to this instance's own
/redirectendpoint, so a forwarded or re-sent message is not wrapped twice
Both are recognized by origin, so a /redirect URL on a different host is rewritten like any other link.
Redirect Flow
When a recipient follows a rewritten link:
- The browser requests
/redirectwith thedataandsigparameters - EmailEngine verifies the signature and rejects the request with
403if it does not match - EmailEngine checks whether the request looks automated, see below
- If it does not, the
trackClickwebhook is queued - The browser is answered with a
302redirect to the original URL, whether or not a webhook was queued
Automated Request Filtering
Before queuing the webhook, EmailEngine checks the requesting address against:
- The published address ranges of Google's crawlers
- 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, but the redirect still happens. Everything else is reported, including link-protection scanners that do not identify themselves.
Best Practices
- Use with open tracking - Opens and clicks answer different questions; enable both to tell a read from an act
- Handle multiple clicks - The same link may be clicked multiple times; decide how to count them
- Respect privacy - Be transparent with recipients about tracking and comply with privacy regulations (GDPR, CAN-SPAM)
- Set your own service secret - EmailEngine mints one if you do not, but a value you set is one you can rotate and keep across restores
- Monitor for anomalies - Watch for unusual click patterns that might indicate security scanner activity
- Test tracked links - Verify that tracked links redirect correctly to the intended destinations
Related Events
- trackOpen - Triggered when the tracking pixel of a message is loaded
- 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
- trackOpen - The open half of the same tracking configuration
- Sending Emails - Submitting a message with
trackClicksset - SMTP interface - Switching tracking on with
X-EE-Tracking-Enabled - Settings API - Setting
trackClicksandserviceSecret