DEV Community

Qasim
Qasim

Posted on

Track email opens, clicks, and replies with Nylas

You send an email from your app and then you're blind. Did it land? Did anyone read it? Did they click the link you actually cared about? Building that visibility yourself means a redirect service for links, a tracking pixel host, and a pipeline to collect the events. Nylas message tracking gives you all three by setting a few flags on the send request, and the open, click, and reply events arrive as webhooks you already know how to handle.

This post is a working tour of message tracking from two angles: the HTTP API for your backend, and the Nylas CLI for the terminal. I work on the CLI, so the terminal commands below are the ones I reach for when I'm testing that tracking fires.

How message tracking works

Message tracking is opt-in per message. When you set a tracking flag on a send, Nylas modifies the outgoing message so the action can be detected: an open drops in a tracking pixel, link tracking rewrites the links, and reply tracking watches the thread. When the recipient acts, a POST arrives at your webhook endpoint describing what happened. You enable any combination of three options, plus a label to identify the message later.

One requirement up front: message tracking is not available on Sandbox applications. If you send with tracking options on a trial account, the API rejects it with the error "Tracking options are not allowed for trial accounts", so you need a production application to use any of this.

Enable tracking on a send

Tracking lives in a tracking_options object on the POST /messages/send body. Each flag is a boolean, and the label is a free-text string echoed back in every notification so you can tie an event to a campaign, a user, or a workflow step. The request below enables all three and labels the message:

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 '{
    "subject": "Following up on your demo",
    "body": "Thanks for your time. <a href=\"https://example.com/pricing\">See pricing here.</a>",
    "to": [{ "email": "prospect@example.com" }],
    "tracking_options": {
      "opens": true,
      "links": true,
      "thread_replies": true,
      "label": "demo-followup"
    }
  }'
Enter fullscreen mode Exit fullscreen mode

The CLI exposes the same options as flags on nylas email send. --track-opens and --track-links toggle open and click tracking, and --track-label sets the label string. It's the fast way to fire a tracked test send and watch the webhook arrive:

nylas email send \
  --to prospect@example.com \
  --subject "Following up on your demo" \
  --body 'Thanks for your time. <a href="https://example.com/pricing">See pricing here.</a>' \
  --track-opens \
  --track-links \
  --track-label "demo-followup"
Enter fullscreen mode Exit fullscreen mode

Note one gap between the surfaces: the CLI covers opens, links, and the label, but reply tracking is set through the API's thread_replies flag rather than a dedicated CLI flag. If your agent or app needs reply tracking, enable it on the API send.

How open tracking works

Open tracking embeds a small tracking pixel in the message body, and when the recipient's mail client loads that image, Nylas records the open and fires message.opened. This is the same mechanism every email tool uses, which means the same caveat applies: a recipient whose client blocks remote images won't register an open even if they read the message. Treat opens as a positive signal, not a guarantee of attention.

Because of image blocking and proxy prefetching, open counts run noisier than click counts. A privacy-focused mail client may preload images and register an open the recipient never made, while another blocks the pixel and undercounts. For anything you need to be precise about, like measuring engagement on a specific call to action, link tracking is the firmer signal because it requires a deliberate action.

How link click tracking works

Link tracking rewrites the links in your message with tracking URLs. When the recipient clicks one, Nylas logs the click, forwards them to the original address, and fires message.link_clicked. It applies to every valid link in the message, not a subset you choose, so enabling it tracks the whole message at once.

Two rules govern which links get rewritten. First, links must be valid HTML anchors with a proper URI scheme, written as <a href="https://www.example.com">link</a>, because a bare URL in plain text isn't something Nylas can rewrite. Second, links that embed login credentials are ignored on purpose: rewriting them would break authentication against the destination server, so credentialed URLs like private Google Form links are left untouched. Those links still work when clicked; they just aren't tracked.

The message.link_clicked payload carries detail beyond a simple count. A recents array holds entries for the last 50 click events on a given link in a given message, each with a link_index identifying which link and a click_id identifying the event, plus the clicker's IP address and user agent. A link_data dictionary lists the message's links with a count of clicks each has received. One indexing quirk to remember: the event count starts at 1, but the click_id and opened_id indices start at 0.

How thread reply tracking works

Reply tracking watches the conversation rather than the message body. With thread_replies enabled, Nylas fires a thread.replied webhook when anyone replies to the thread the message belongs to, which is how you detect a response without polling the mailbox for new messages. For an outreach agent, this is the trigger that says "the prospect wrote back" the moment it happens.

Reply tracking pairs naturally with the other two. A typical outreach send enables all three: opens tells you the message was seen, links tells you the call to action was clicked, and thread_replies tells you the conversation moved forward. Each event carries the label you set on the send, so a single webhook handler can route every signal for a campaign by reading one field.

Measure reply rates for an outreach agent

Reply tracking and the label together give you a clean engagement metric without a separate analytics pipeline. Send every message in a campaign with the same label and thread_replies enabled, then count thread.replied webhooks against that label to get a reply rate for the campaign. Because the label rides on every notification, you never have to map message IDs back to campaigns yourself.

For an AI agent running outreach, this closes the loop. The agent sends a tracked message, and when thread.replied fires it knows to fetch the thread and decide how to respond, all keyed off the label it set at send time. Pair that with opens and links and you can tell the difference between a prospect who never saw the message, one who read it but didn't act, and one who clicked through, which is exactly the signal an agent needs to pick its next move.

Current and legacy tracking triggers

Each tracking trigger comes in two forms: the current trigger and a .legacy variant. Alongside message.opened, message.link_clicked, and thread.replied, Nylas also emits message.opened.legacy, message.link_clicked.legacy, and thread.replied.legacy. The legacy variants exist for applications still on the older notification format, so those integrations keep receiving events in the shape they were built for.

For a new integration, subscribe to the current message.opened, message.link_clicked, and thread.replied triggers, not the legacy ones. You only need a legacy variant if you're maintaining an older application written against the previous payload format. Picking the current triggers keeps your handler aligned with the webhook schemas linked above.

Read a message's tracking settings back

Sometimes you need to know what tracking was enabled on a message after the fact, not just react to its events. The Messages API exposes this through the include_tracking_options value on the fields query parameter. Requesting it on a GET /messages call returns each message along with the tracking settings it was sent with, so you can confirm a message went out tracked.

curl --request GET \
  --url "https://api.us.nylas.com/v3/grants/<GRANT_ID>/messages?fields=include_tracking_options" \
  --header "Authorization: Bearer <NYLAS_API_KEY>"
Enter fullscreen mode Exit fullscreen mode

This matters when you're debugging a campaign that isn't reporting events. If message.opened never fires, the first thing to check is whether the message was actually sent with opens enabled, and reading the tracking options back tells you that directly instead of guessing. It's also useful for auditing which sends in a batch carried tracking and which slipped through without it.

Caveats that bite in production

A few behaviors around grants and accounts will cost you tracking data if you don't plan for them, and none of them are obvious until events go missing.

The biggest one is grant deletion. If you delete a grant, the tracking links in messages it already sent stop working, because Nylas can no longer match incoming events to the deleted grant. You lose open, click, and reply notifications for everything that grant sent. When a grant expires, re-authenticate it rather than deleting and recreating it, so the tracking history stays intact.

There's also a backfill limit tied to grant health. If tracking events occur while a grant is out of service for more than 72 hours, you can't backfill those notifications once it recovers. The notifications for message.opened, message.link_clicked, and thread.replied events that happened during the outage won't be backfilled, so keeping grants healthy is part of keeping tracking data complete.

Things to keep in mind

A short list of practices keeps message tracking accurate and your handler simple.

  • Use a production application. Tracking is rejected on Sandbox with "Tracking options are not allowed for trial accounts", so confirm you're upgraded before relying on it.
  • Trust clicks over opens. Image blocking and proxy prefetching make opens noisy; a click is a deliberate action and a firmer signal.
  • Write links as HTML anchors. Only valid <a href> links with a proper URI scheme get rewritten, and credentialed URLs are skipped by design.
  • Read label in your webhook handler. It's the cleanest way to route an event back to the campaign or user that triggered it, since it rides on every notification.
  • Re-authenticate expired grants, never delete them. Deleting a grant kills its tracking links and notifications for already-sent mail.

Wrapping up

Message tracking turns "I sent it and hoped" into "it was opened at 9:14, the pricing link was clicked, and they replied twenty minutes later." Set tracking_options on the send, or the --track-opens and --track-links flags on the CLI, subscribe to the three webhooks, and read the label to tie each event back to its source. The mechanics, pixels for opens and rewritten links for clicks, are handled for you, so what's left is deciding what your app does when the signal arrives.

Where to go next:

Top comments (0)