DEV Community

Qasim
Qasim

Posted on

Track email opens and clicks with the Nylas API

You sent an important email and now you're staring at the thread wondering: did they even open it? Did they click the link you sent? For a sales follow-up, a newsletter, or a transactional message you need to land, that silence is a problem. Email tracking answers the question by telling you when a recipient opens a message, clicks a link inside it, or replies to the thread. This post wires that up with the Email API and shows the CLI flags that turn it on from the terminal.

It's a worked use case rather than an endpoint tour, covering the tracking_options you set on a send and the tracking webhooks you receive, from two angles: the HTTP API your backend calls and the nylas CLI for sending a tracked message fast. I work on the CLI, so the commands below are the ones I reach for when I want to see tracking fire end to end.

Three things you can track

Tracking comes in three flavors, and you choose which ones you want per message. Open tracking tells you when a recipient first opens the message. Link tracking tells you when they click a link inside it, and which link. Reply tracking tells you when someone replies to the thread. Each is a separate boolean you flip on at send time, so a message can carry any combination of the three.

The reason they're separate is that they answer different questions and have different reliability. An open is the lightest signal and the easiest to miss, since it depends on the recipient's email client loading an image. A click is a stronger signal of real intent, because someone deliberately acted. A reply is the strongest of all, an explicit response. Most teams turn on opens and links together, because opens alone undercount and the two together give a fuller picture of engagement than either by itself.

Enable tracking when you send

You turn tracking on by adding a tracking_options object to a POST /v3/grants/{grant_id}/messages/send request. It has three boolean fields, opens, links, and thread_replies, each defaulting to false, plus an optional label string. Set the ones you want to true, and the message goes out instrumented for exactly those events. One prerequisite: tracking needs a production application, since a sandbox (trial) account rejects the request with "Tracking options are not allowed for trial accounts."

curl --request POST \
  --url "https://api.us.nylas.com/v3/grants/<GRANT_ID>/messages/send" \
  --header "Authorization: Bearer <NYLAS_API_KEY>" \
  --header "Content-Type: application/json" \
  --data '{
    "to": [{ "email": "prospect@example.com" }],
    "subject": "Following up",
    "body": "<p>Hi, just checking in. <a href=\"https://example.com/demo\">Book a demo</a>.</p>",
    "tracking_options": {
      "opens": true,
      "links": true,
      "thread_replies": true,
      "label": "demo-follow-up"
    }
  }'
Enter fullscreen mode Exit fullscreen mode

Notice the body is HTML with a real anchor tag, which matters for the next sections. The label is a free-text description, up to 2,048 characters, that rides along with the tracking data so you can tell later why a message was tracked or which campaign it belonged to. The same tracking_options object works when you create a draft, so a message reviewed before sending can carry tracking too.

Send a tracked message from the CLI

The CLI exposes the same three toggles as flags on nylas email send. Pass --track-opens to instrument opens, --track-links for clicks, and --track-label to attach a grouping label. There's no separate reply flag, so for thread reply tracking you reach for the API's thread_replies field, but opens and links, the two most teams use, are one flag each.

nylas email send \
  --to prospect@example.com \
  --subject "Following up" \
  --body "<p>Hi, just checking in. <a href='https://example.com/demo'>Book a demo</a>.</p>" \
  --track-opens --track-links --track-label "demo-follow-up"
Enter fullscreen mode Exit fullscreen mode

This is the fastest way to confirm tracking works before you build it into an app: send yourself a tracked message, open it, click the link, and watch the events arrive. The nylas email tracking-info command prints a reference of the tracking features and the webhook payloads they produce, which is handy when you're setting up the listener and want the shapes in front of you.

Receive the events through webhooks

Tracking is delivered asynchronously, so the events come to you as webhooks rather than in the send response. You subscribe to three triggers that map one-to-one onto the options: message.opened fires on an open, message.link_clicked on a click, and thread.replied on a reply. Each POST to your endpoint carries the message_id and the details of what happened.

app.post("/webhooks/nylas", async (req, res) => {
  res.sendStatus(200); // acknowledge fast
  const { type, data } = req.body;
  const obj = data.object; // tracking payload nests under data.object
  if (type === "message.opened") {
    await recordOpen(obj.message_id, obj.recents);
  } else if (type === "message.link_clicked") {
    // recents = each individual click; link_data = aggregate [{ url, count }]
    await recordClicks(obj.message_id, obj.recents, obj.link_data);
  } else if (type === "thread.replied") {
    await recordReply(obj);
  }
});
Enter fullscreen mode Exit fullscreen mode

The tracking fields sit under data.object, not directly on data, so reach for data.object.message_id and friends. An open event's recents array carries each open with a timestamp, the recipient's ip, and user_agent. A click event carries both a recents array of individual clicks and a link_data array listing each tracked url with a running count, so you see not just that a link was clicked but which one and how often. One indexing quirk to know: the event count starts at 1, while the opened_id and click_id indices start at 0, so don't conflate the two when you store them.

How open tracking actually works

Open tracking is a transparent one-pixel image. When you enable it, Nylas inserts that pixel into the message's HTML, and when the recipient's email client renders the message, it requests the image from the tracking server, which records the open. That mechanism has two consequences worth designing around, because they decide how much you can trust an open.

First, it needs HTML and a client that loads remote images. Ad blockers, corporate proxies, and content delivery networks can all intercept the pixel request, so an open that never registers doesn't mean the message wasn't read. Open rates run lower than reality for this reason, especially since some clients pre-fetch or block images by default. Second, repeated loads are deduplicated: if the pixel is fetched several times within one minute, only the first counts as an open, so a client that reloads doesn't inflate your numbers. The practical takeaway is to treat opens as a floor, not a precise count, and to pair them with link tracking for a signal that doesn't depend on image loading.

How link tracking actually works

Link tracking works by rewriting links, and the detail that trips people up is that it's all-or-nothing per message. When you enable it, the service rewrites every valid HTML link in the body to route through a tracking redirect, then forwards the click to the real destination. You can't track only some links and leave others alone; enabling link tracking instruments all of them.

For a link to be rewritten, it has to be a properly formed HTML anchor with a valid URI, like <a href="https://example.com">demo</a>. A bare www.example.com with no anchor tag won't be tracked, because there's nothing to rewrite. There's also a security carve-out: links carrying embedded login credentials are deliberately left alone, since rewriting them would break the destination's authentication. A private Google Form URL, for instance, contains credentials, so it's skipped, the link still works when clicked but produces no tracking event. Knowing these rules up front saves you from debugging a "missing" click that was never trackable to begin with.

Track replies, and label what you track

Reply tracking is the third option and the most reliable signal, because a reply is an explicit human action with nothing to block or rewrite. Set thread_replies to true and a thread.replied webhook fires when someone responds on the thread, which is how you detect engagement on a message that asked for one without polling the mailbox for new messages yourself.

The label field is the small piece that makes tracking data usable at scale. It's an optional description, up to 2,048 characters, attached at send time and carried through to the events, so when a message.opened arrives you can tell which campaign or message type it belongs to without a separate lookup. Use it to group a batch, a "march-newsletter" or "trial-day-3" label, and your analytics can bucket events by intent rather than by an opaque message ID. It costs nothing to set and saves a join later.

Don't delete a grant you're still tracking

One lifecycle detail bites people who don't know it. Tracking links and pixels are tied to the grant that sent the message, and if you delete that grant, the tracking stops working: the service can no longer match an incoming open or click to the deleted grant, so you lose tracking events for every message that grant sent, including ones already out in the wild. An expired grant is gentler, you still receive message.opened events on it, but a deleted one is a hard cutoff.

The rule that follows is the same one that applies across the API: re-authenticate an expired grant in place rather than deleting and recreating it. A delete-and-recreate cycle silently breaks tracking on every message the old grant ever sent, which surfaces as analytics that mysteriously go quiet for older sends. If you're running campaigns whose tracking matters for weeks after sending, treat grant deletion as the destructive operation it is for your tracking data.

Where tracking pays off

The same three options sit under a range of features, and which you enable follows the signal you need. A few that map straight on:

  • Sales follow-up timing. An open or click tells a rep the moment a prospect engaged, so the next touch lands when interest is fresh instead of on a fixed schedule.
  • Newsletter analytics. Open and click rates across a batch, grouped by label, show which subject lines and links actually performed.
  • Engagement-driven sequences. A thread.replied event advances or stops an automated sequence, so a reply pauses the follow-ups instead of a customer getting nagged after they already answered.
  • Delivery confidence. For a transactional message that must land, a registered open is a lightweight signal that it reached a real inbox, though a missed pixel never proves the opposite, so pair it with a click for certainty.

Each is the same tracking_options on a send plus a webhook listener, with the difference being which events you act on and how.

Things to keep in mind

A short list of details keeps tracking honest.

  • Opens undercount. The pixel depends on image loading, so ad blockers and CDNs suppress some opens; treat the number as a floor and pair it with links.
  • Link tracking is all-or-nothing. Enabling it rewrites every valid HTML link in the message; you can't instrument only some.
  • Use real anchor tags. Only properly formed <a href> links with valid URIs get rewritten; bare text URLs and credential-bearing links are skipped.
  • Events arrive by webhook. Subscribe to message.opened, message.link_clicked, and thread.replied; nothing comes back in the send response.
  • Label your sends. The optional label, up to 2,048 characters, groups events by campaign so your analytics don't hinge on raw message IDs.
  • Don't delete grants mid-campaign. Deleting a grant kills tracking for every message it sent; re-authenticate an expired grant instead.

Wrapping up

Email tracking turns a silent send into a measurable one. Add a tracking_options object with opens, links, or thread_replies to a send, or pass --track-opens and --track-links from the CLI, and the events arrive as message.opened, message.link_clicked, and thread.replied webhooks carrying timestamps, IPs, and the clicked URLs. Remember that opens undercount because they rely on a loaded pixel, that link tracking rewrites every valid link or none, and that deleting a grant breaks tracking for everything it sent. Label your sends and lean on clicks and replies for the signals that matter.

Where to go next:

AI-answer pages for agents

When this post is published, link AI agents and crawlers to the retrieval-ready version on cli.nylas.com:

Top comments (0)