Email Tracking
Track the full lifecycle of every email — sent, delivered, opened, clicked, bounced, complained, or unsubscribed — through webhooks for real-time events and the analytics endpoint for aggregate metrics.
Enabling Tracking
Open and click tracking are controlled per message via the tracking object on Send Email:
{
"from": "[email protected]",
"to": "[email protected]",
"subject": "Weekly Digest",
"html": "<p>Read more on <a href=\"https://yourapp.com\">our site</a>.</p>",
"tracking": {
"opens": true,
"clicks": true
}
}
| Field | Type | Default | Description |
|---|---|---|---|
opens | boolean | true | Inject a tracking pixel to record opens |
clicks | boolean | true | Rewrite links to record clicks |
Webhooks
Configure a webhook URL in your dashboard, then receive a POST for each event.
webhook-handler.js
app.post('/webhooks/email', (req, res) => {
const { event, email, message_id, timestamp } = req.body;
switch (event) {
case 'delivered':
console.log(`Delivered ${message_id} to ${email}`);
break;
case 'opened':
console.log(`Opened ${message_id} by ${email}`);
break;
case 'clicked':
console.log(`Link clicked in ${message_id} (${req.body.url})`);
break;
case 'bounced':
console.log(`Bounced ${message_id}: ${req.body.reason}`);
break;
case 'complained':
console.log(`Spam complaint from ${email}`);
break;
case 'unsubscribed':
await removeFromMailingList(email);
break;
}
res.sendStatus(200);
});
webhook_handler.py
from flask import Flask, request
app = Flask(__name__)
@app.post('/webhooks/email')
def email_webhook():
payload = request.get_json()
event = payload['event']
if event == 'bounced':
handle_bounce(payload['email'], payload['reason'])
elif event == 'unsubscribed':
remove_from_list(payload['email'])
return '', 200
Event Types
| Event | Description |
|---|---|
sent | Accepted and handed to the provider |
delivered | Accepted by the recipient's mail server |
opened | Recipient opened the email |
clicked | Recipient clicked a tracked link |
bounced | Delivery failed (hard or soft) |
complained | Recipient marked the email as spam |
unsubscribed | Recipient opted out |
Webhook Payload
{
"event": "clicked",
"message_id": "email_7K8L9M0N",
"email": "[email protected]",
"url": "https://yourapp.com/offer",
"timestamp": "2026-05-28T10:31:22Z",
"user_agent": "Mozilla/5.0",
"ip": "196.188.x.x"
}
Verifying Webhooks
Each request includes an X-AfriRoute-Signature header — an HMAC-SHA256 of the raw body keyed with your webhook secret. Always verify it before trusting the payload.
import crypto from 'crypto';
function verify(req, secret) {
const expected = crypto
.createHmac('sha256', secret)
.update(req.rawBody)
.digest('hex');
return expected === req.headers['x-afriroute-signature'];
}
Analytics Endpoint
Pull aggregate metrics for a date range or campaign.
GET /v1/email/analytics
analytics.js
const res = await fetch(
'https://api.afriroute.ai/api/v1/email/analytics?from=2026-05-01&to=2026-05-28',
{ headers: { 'Authorization': 'Bearer $AFRIROUTE_API_KEY' } }
);
console.log(await res.json());
Response
{
"sent": 10000,
"delivered": 9950,
"opened": 3500,
"clicked": 800,
"bounced": 50,
"complained": 5,
"deliverability_rate": 99.5,
"open_rate": 35.2,
"click_rate": 8.0
}
Best Practices
- Always verify the webhook signature before acting on an event.
- Respond with
200quickly and process asynchronously — slow handlers cause retries. - Handle hard bounces by suppressing the address; repeated sends hurt your reputation.
- Honor unsubscribes immediately (within 24 hours) to stay compliant.
- Expect duplicate events — webhooks are at-least-once; dedupe on
message_id+event.
Errors
| Code | HTTP | Meaning |
|---|---|---|
INVALID_DATE_RANGE | 400 | from/to malformed or from after to |
WEBHOOK_NOT_CONFIGURED | 404 | No webhook URL set for this account |
Related Resources
Last Updated: May 2026 · Need help? Contact Support →