Skip to main content

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
}
}
FieldTypeDefaultDescription
opensbooleantrueInject a tracking pixel to record opens
clicksbooleantrueRewrite 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​

EventDescription
sentAccepted and handed to the provider
deliveredAccepted by the recipient's mail server
openedRecipient opened the email
clickedRecipient clicked a tracked link
bouncedDelivery failed (hard or soft)
complainedRecipient marked the email as spam
unsubscribedRecipient 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 200 quickly 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​

CodeHTTPMeaning
INVALID_DATE_RANGE400from/to malformed or from after to
WEBHOOK_NOT_CONFIGURED404No webhook URL set for this account

Last Updated: May 2026 · Need help? Contact Support →