Guides
Click tracking
Count link clicks in Ray emails with trackClicks, group them across sends with campaignId, and read click stats with GET /clicks.
Set trackClicks: true on an email POST /send and Ray rewrites the links in the HTML body so each click is counted before the recipient is redirected to the original URL. Read the counts with GET /clicks. Requests go to https://ray-api.gege.mn with Authorization: Bearer $RAY_API_KEY.
Click tracking is available on the Starter plan and above (see https://ray.gege.mn/pricing). On the Free plan trackClicks is accepted but ignored: links are sent unchanged.
Enable it on a send
curl -X POST https://ray-api.gege.mn/send \
-H "Authorization: Bearer $RAY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"channelConfigId": "9f1c2a7e-3b4d-4e5f-8a6b-1c2d3e4f5a6b",
"templateId": "1d2e3f4a-5b6c-4d7e-8f9a-0b1c2d3e4f5a",
"params": { "name": "Ada" },
"recipient": { "email": "[email protected]" },
"trackClicks": true,
"campaignId": "spring-sale-2026"
}'
await fetch("https://ray-api.gege.mn/send", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.RAY_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
channelConfigId: "9f1c2a7e-3b4d-4e5f-8a6b-1c2d3e4f5a6b",
templateId: "1d2e3f4a-5b6c-4d7e-8f9a-0b1c2d3e4f5a",
params: { name: "Ada" },
recipient: { email: "[email protected]" },
trackClicks: true,
campaignId: "spring-sale-2026",
}),
});
What gets rewritten
- Only email channels (
ses_email,smtp_email). On other channelstrackClickshas no effect; in a multi-channeldeliveries[]send only the email deliveries are rewritten. - Only the HTML body (
bodyHtml). The plain-text part is left alone. - Only anchors with an absolute
http://orhttps://href.mailto:,tel:,#anchorsand relative URLs are not touched. - An anchor with a
data-ray-no-trackattribute is skipped. Use it for unsubscribe and legal links:
{
"bodyHtml": "<p><a href=\"https://shop.example.com/sale\">Shop the sale</a></p><p><a data-ray-no-track href=\"https://shop.example.com/unsubscribe?u={{userId}}\">Unsubscribe</a></p>"
}
Each tracked link points to Ray's redirect endpoint, GET /c/{token}. The token is HMAC-signed and carries the destination URL, your workspace, the sendId and the campaignId. The endpoint records the click and answers 302 to the destination. Because the destination comes only from the signed token, the redirect cannot be abused as an open redirect; a tampered or invalid token returns 404. The redirect endpoint needs no API key, since recipients are not authenticated.
With fan-out (targets[]), all recipients share one rendered body, so clicks are attributed to the send, not to an individual recipient. For per-recipient attribution, send one request per recipient and group them with campaignId.
Read click stats
GET /clicks requires at least one of sendId or campaignId. With both, both filters apply.
curl "https://ray-api.gege.mn/clicks?campaignId=spring-sale-2026" \
-H "Authorization: Bearer $RAY_API_KEY"
{
"totalClicks": 42,
"byUrl": [
{ "url": "https://shop.example.com/sale", "clicks": 37 },
{ "url": "https://shop.example.com/faq", "clicks": 5 }
]
}
byUrl is sorted by clicks, highest first.
async function clickStats(query: { sendId?: string; campaignId?: string }) {
const url = new URL("https://ray-api.gege.mn/clicks");
if (query.sendId) url.searchParams.set("sendId", query.sendId);
if (query.campaignId) url.searchParams.set("campaignId", query.campaignId);
const res = await fetch(url, { headers: { Authorization: `Bearer ${process.env.RAY_API_KEY}` } });
if (!res.ok) throw new Error(`GET /clicks failed: ${res.status} ${await res.text()}`);
return (await res.json()) as { totalClicks: number; byUrl: Array<{ url: string; clicks: number }> };
}
| Status | error |
message |
|---|---|---|
400 |
validation_error |
one of `sendId` or `campaignId` is required |
400 |
validation_error |
`sendId` must be a UUID |
Caveats
- Counts are total clicks, not unique clicks.
- Counts include automated clicks: corporate mail security scanners often fetch every link in a message. Treat the numbers as a trend signal.
- There is no open tracking (no tracking pixel).