Track how recipients engage with your emails using Ark’s built-in open and click tracking. This guide covers setup, configuration, and best practices.
How Tracking Works
Open Tracking
Ark inserts a tiny transparent image (tracking pixel) into your HTML emails. When the image loads, we record an open event.
Click Tracking
Links in your emails are rewritten to pass through Ark’s tracking server. When a recipient clicks, we record the event and redirect them to the original URL.
Enabling Tracking
Tracking is enabled by default. You can control it per-email:
from ark import Ark
client = Ark()
email = client.emails.send(
from_ = "hello@yourdomain.com" ,
to = [ "user@example.com" ],
subject = "Check out our new features" ,
html = '<p>Click <a href="https://yourdomain.com/features">here</a> to learn more!</p>' ,
track_opens = True , # Default: True
track_clicks = True # Default: True
)
import Ark from 'ark' ;
const client = new Ark ();
const email = await client . emails . send ({
from: 'hello@yourdomain.com' ,
to: [ 'user@example.com' ],
subject: 'Check out our new features' ,
html: '<p>Click <a href="https://yourdomain.com/features">here</a> to learn more!</p>' ,
trackOpens: true , // Default: true
trackClicks: true , // Default: true
});
require "ark_email"
client = ArkEmail :: Client . new
email = client. emails . send_ (
from: "hello@yourdomain.com" ,
to: [ "user@example.com" ],
subject: "Check out our new features" ,
html: '<p>Click <a href="https://yourdomain.com/features">here</a> to learn more!</p>' ,
track_opens: true , # Default: true
track_clicks: true # Default: true
)
client := ark . NewClient ()
email , _ := client . Emails . Send ( ctx , ark . EmailSendParams {
From : "hello@yourdomain.com" ,
To : [] string { "user@example.com" },
Subject : "Check out our new features" ,
HTML : ark . String ( `<p>Click <a href="https://yourdomain.com/features">here</a> to learn more!</p>` ),
TrackOpens : ark . Bool ( true ), // Default: true
TrackClicks : ark . Bool ( true ), // Default: true
})
Disable tracking for sensitive emails like password resets or security notifications.
Custom Tracking Domain
By default, tracking links use Ark’s domain. For better branding and deliverability, set up a custom tracking domain.
Step 1: Add the Domain
Python
Node.js
Ruby
Go
cURL
tracking_domain = client.tracking.domains.create(
domain = "track.yourdomain.com" ,
default = True
)
print ( f "Domain ID: { tracking_domain.data.id } " )
print ( "Add the following DNS record:" )
print ( f " CNAME track.yourdomain.com -> track.arkhq.io" )
const trackingDomain = await client . tracking . domains . create ({
domain: 'track.yourdomain.com' ,
default: true ,
});
console . log ( `Domain ID: ${ trackingDomain . data . id } ` );
console . log ( 'Add the following DNS record:' );
console . log ( ' CNAME track.yourdomain.com -> track.arkhq.io' );
tracking_domain = client. tracking . domains . create (
domain: "track.yourdomain.com" ,
default: true
)
puts "Domain ID: #{ tracking_domain. data . id } "
puts "Add the following DNS record:"
puts " CNAME track.yourdomain.com -> track.arkhq.io"
trackingDomain , _ := client . Tracking . Domains . Create ( ctx , ark . TrackingDomainCreateParams {
Domain : ark . String ( "track.yourdomain.com" ),
Default : ark . Bool ( true ),
})
fmt . Printf ( "Domain ID: %s \n " , trackingDomain . Data . ID )
fmt . Println ( "Add the following DNS record:" )
fmt . Println ( " CNAME track.yourdomain.com -> track.arkhq.io" )
curl -X POST https://api.arkhq.io/v1/tracking/domains \
-H "Authorization: Bearer $ARK_API_KEY " \
-H "Content-Type: application/json" \
-d '{
"domain": "track.yourdomain.com",
"default": true
}'
Add the CNAME record provided in the response:
Type Name Value CNAME track.yourdomain.comtrack.arkhq.io
Step 3: Verify
Python
Node.js
Ruby
Go
cURL
result = client.tracking.domains.verify( "trk_abc123" )
if result.data.verified:
print ( "Tracking domain verified!" )
else :
print ( "DNS records not found yet" )
const result = await client . tracking . domains . verify ( 'trk_abc123' );
if ( result . data . verified ) {
console . log ( 'Tracking domain verified!' );
} else {
console . log ( 'DNS records not found yet' );
}
result = client. tracking . domains . verify ( "trk_abc123" )
if result. data . verified
puts "Tracking domain verified!"
else
puts "DNS records not found yet"
end
result , _ := client . Tracking . Domains . Verify ( ctx , "trk_abc123" )
if result . Data . Verified {
fmt . Println ( "Tracking domain verified!" )
} else {
fmt . Println ( "DNS records not found yet" )
}
curl -X POST https://api.arkhq.io/v1/tracking/domains/trk_abc123/verify \
-H "Authorization: Bearer $ARK_API_KEY "
Now your tracking links will use track.yourdomain.com instead of Ark’s domain.
Receiving Tracking Events
Via Webhooks
Set up webhooks to receive real-time tracking events:
Python
Node.js
Ruby
Go
cURL
webhook = client.webhooks.create(
url = "https://yourapp.com/webhooks/ark" ,
events = [ "MessageLoaded" , "MessageLinkClicked" ]
)
print ( f "Webhook created: { webhook.data.id } " )
const webhook = await client . webhooks . create ({
url: 'https://yourapp.com/webhooks/ark' ,
events: [ 'MessageLoaded' , 'MessageLinkClicked' ],
});
console . log ( `Webhook created: ${ webhook . data . id } ` );
webhook = client. webhooks . create (
url: "https://yourapp.com/webhooks/ark" ,
events: [ "MessageLoaded" , "MessageLinkClicked" ]
)
puts "Webhook created: #{ webhook. data . id } "
webhook , _ := client . Webhooks . Create ( ctx , ark . WebhookCreateParams {
URL : ark . String ( "https://yourapp.com/webhooks/ark" ),
Events : [] string { "MessageLoaded" , "MessageLinkClicked" },
})
fmt . Printf ( "Webhook created: %s \n " , webhook . Data . ID )
curl -X POST https://api.arkhq.io/v1/webhooks \
-H "Authorization: Bearer $ARK_API_KEY " \
-H "Content-Type: application/json" \
-d '{
"url": "https://yourapp.com/webhooks/ark",
"events": ["MessageLoaded", "MessageLinkClicked"]
}'
Open Event Payload
{
"type" : "email.opened" ,
"data" : {
"emailId" : "msg_abc123" ,
"to" : "user@example.com" ,
"userAgent" : "Mozilla/5.0..." ,
"ipAddress" : "192.168.1.1" ,
"location" : {
"city" : "San Francisco" ,
"region" : "CA" ,
"country" : "US"
},
"timestamp" : "2024-01-15T10:30:00Z"
}
}
Click Event Payload
{
"type" : "email.clicked" ,
"data" : {
"emailId" : "msg_abc123" ,
"to" : "user@example.com" ,
"url" : "https://yourdomain.com/features" ,
"userAgent" : "Mozilla/5.0..." ,
"ipAddress" : "192.168.1.1" ,
"timestamp" : "2024-01-15T10:31:00Z"
}
}
Via API
Query tracking data directly:
Python
Node.js
Ruby
Go
cURL
stats = client.tracking.stats.retrieve(
start_date = "2024-01-01" ,
end_date = "2024-01-31"
)
print ( f "Open rate: { stats.data.summary.open_rate } %" )
print ( f "Click rate: { stats.data.summary.click_rate } %" )
const stats = await client . tracking . stats . retrieve ({
startDate: '2024-01-01' ,
endDate: '2024-01-31' ,
});
console . log ( `Open rate: ${ stats . data . summary . openRate } %` );
console . log ( `Click rate: ${ stats . data . summary . clickRate } %` );
stats = client. tracking . stats . retrieve (
start_date: "2024-01-01" ,
end_date: "2024-01-31"
)
puts "Open rate: #{ stats. data . summary . open_rate } %"
puts "Click rate: #{ stats. data . summary . click_rate } %"
stats , _ := client . Tracking . Stats . Get ( ctx , ark . TrackingStatsParams {
StartDate : ark . String ( "2024-01-01" ),
EndDate : ark . String ( "2024-01-31" ),
})
fmt . Printf ( "Open rate: %.2f%% \n " , stats . Data . Summary . OpenRate )
fmt . Printf ( "Click rate: %.2f%% \n " , stats . Data . Summary . ClickRate )
curl "https://api.arkhq.io/v1/tracking/stats?startDate=2024-01-01&endDate=2024-01-31" \
-H "Authorization: Bearer $ARK_API_KEY "
Tracking Metrics
Metric Description Opens Number of unique opens Clicks Number of unique clicks Open Rate Opens / Delivered * 100 Click Rate Clicks / Delivered * 100 Click-to-Open Rate Clicks / Opens * 100
Metrics are deduplicated by recipient. Multiple opens/clicks from the same person count as one.
Tracking Limitations
Open Tracking
Image blocking : Many email clients block images by default
Privacy features : Apple Mail Privacy Protection, Gmail, and others may preload images, causing false positives
Plain text emails : Open tracking only works with HTML emails
Click Tracking
Link preview : Some clients preview links, causing false clicks
Security scanning : Corporate email systems may scan links
Plain text emails : Click tracking only works in HTML emails
Best Practices
Use for engagement trends, not absolutes
Due to image blocking and privacy features, open rates are underreported. Use them for trend analysis rather than exact measurements.
Set up a custom tracking domain
Using your own domain improves deliverability and looks more professional.
Disable tracking for sensitive communications and honor user preferences.
Focus on click-through rates to measure actual engagement rather than just opens.
Privacy Considerations
Be transparent about tracking:
Disclose in privacy policy : Mention that you track email engagement
Provide opt-out : Let users disable tracking if requested
Minimize data : Only collect what you need
API Reference View tracking API documentation