# Track email opens and replies

Source: https://developer.nylas.com/docs/cookbook/use-cases/build/track-email-opens/

A sales rep sends a follow-up to a prospect and wants to know the moment it lands and gets read, so they can call while the offer is top of mind. That signal, "your email was just opened," is what powers outreach sequences, deal-stage automation, and reply-based routing. You get it by turning on tracking when you send a message and listening for the webhook that fires when the recipient acts.

## Enable tracking when you send

Opens, clicks, and replies are only reported for messages you send with tracking turned on, so the tracking starts at send time, not after. Pass a `tracking_options` object on the [Send Message](/docs/reference/api/messages/send-message/) request with the booleans `opens`, `links`, and `thread_replies`, plus an optional `label` that's echoed back in every notification.

The request below sends an HTML message with all three signals enabled. The `label` is a free-text tag you read later to match a notification back to a campaign or contact. Message tracking needs a production application; trial accounts return "Tracking options are not allowed for trial accounts".

```bash
curl --request POST \
  --url 'https://api.us.nylas.com/v3/grants/<NYLAS_GRANT_ID>/messages/send' \
  --header 'Content-Type: application/json' \
  --header 'Authorization: Bearer <NYLAS_API_KEY>' \
  --data-raw '{
    "subject": "Quick follow-up on your trial",
    "body": "Thanks for trying us out. Reply or <a href=\"https://example.com/demo\">book a demo</a> when ready.",
    "to": [{ "name": "Kim Townsend", "email": "kim@example.com" }],
    "tracking_options": {
      "opens": true,
      "links": true,
      "thread_replies": true,
      "label": "trial-followup-q2"
    }
  }'
```

```js [sendTracking-Node.js SDK]
const Nylas = require("nylas").default;

const nylas = new Nylas({ apiKey: "<NYLAS_API_KEY>" });

const message = await nylas.messages.send({
  identifier: "<NYLAS_GRANT_ID>",
  requestBody: {
    subject: "Quick follow-up on your trial",
    body: 'Thanks for trying us out. Reply or <a href="https://example.com/demo">book a demo</a> when ready.',
    to: [{ name: "Kim Townsend", email: "kim@example.com" }],
    trackingOptions: {
      opens: true,
      links: true,
      threadReplies: true,
      label: "trial-followup-q2",
    },
  },
});

console.log(`Sent message ${message.data.id}`);
```

```python [sendTracking-Python SDK]
from nylas import Client

nylas = Client(api_key="<NYLAS_API_KEY>")

message = nylas.messages.send(
    identifier="<NYLAS_GRANT_ID>",
    request_body={
        "subject": "Quick follow-up on your trial",
        "body": 'Thanks for trying us out. Reply or <a href="https://example.com/demo">book a demo</a> when ready.',
        "to": [{"name": "Kim Townsend", "email": "kim@example.com"}],
        "tracking_options": {
            "opens": True,
            "links": True,
            "thread_replies": True,
            "label": "trial-followup-q2",
        },
    },
)

print(f"Sent message {message.data.id}")
```

These three flags work the same way whether the grant is Google, Microsoft, or any other provider you send through, so one send path covers your whole sender base.

## Subscribe to open and reply events

The send request enables tracking, but the events arrive over webhooks, so you need a destination subscribed to the right triggers before they can reach you. Three triggers report engagement: `message.opened` for opens, `message.link_clicked` for clicked links, and `thread.replied` for replies. You can subscribe to all three on a single webhook URL, so one endpoint receives every tracking event.

Create the destination with a `POST /v3/webhooks/`, passing your HTTPS endpoint as `webhook_url` and the triggers in `trigger_types`. The request below subscribes one endpoint to all three tracking events. For the full create flow with Node.js, Python, Ruby, Java, and Kotlin, see [Using webhooks with Nylas](/docs/v3/notifications/#set-up-a-webhook).

```bash
curl --request POST \
  --url 'https://api.us.nylas.com/v3/webhooks/' \
  --header 'Content-Type: application/json' \
  --header 'Authorization: Bearer <NYLAS_API_KEY>' \
  --data-raw '{
    "trigger_types": ["message.opened", "message.link_clicked", "thread.replied"],
    "webhook_url": "https://yourapp.com/webhooks/nylas",
    "description": "Email engagement tracking"
  }'
```

When you create the webhook, Nylas sends a `GET` with a `challenge` parameter that your endpoint must echo back to finish activation. For real-time setup across all the message and event triggers, see [Get real-time updates with webhooks](/docs/cookbook/use-cases/build/realtime-webhooks/).

## Handle a tracking notification

Each notification names the message it concerns, so your handler can match the event back to the contact you tracked without storing anything extra. The payload includes the tracked message's ID, the `label` you set at send time, and a `recents` array holding the last 50 events for that message, each stamped with a timestamp, IP, and user agent. Your endpoint should return `200 OK` within 10 seconds so the API doesn't count the delivery as failed and retry it.

The handler below reads the trigger type and the message ID, then routes on the event. Open events depend on a tracking pixel that the recipient's client loads, so a client that blocks remote images never fires `message.opened`, even when the message was read. Treat opens as a soft signal and lean on clicks and replies for harder proof.

```python
from flask import Flask, request

app = Flask(__name__)

@app.route("/webhooks/nylas", methods=["GET", "POST"])
def nylas_webhook():
    if request.method == "GET":
        return request.args.get("challenge", ""), 200

    event = request.get_json()
    trigger = event["type"]
    message_id = event["data"]["object"]["message_id"]

    if trigger == "message.opened":
        print(f"Opened: {message_id}")
    elif trigger == "message.link_clicked":
        print(f"Link clicked: {message_id}")
    elif trigger == "thread.replied":
        print(f"Thread replied: {message_id}")

    return "", 200
```

## Things to know about email tracking

Open tracking is the least reliable of the three signals, so design your logic around its blind spots rather than trusting raw open counts. Nylas records an open when the recipient's client loads a transparent one-pixel image embedded in the message. Corporate gateways and privacy-focused clients block remote images by default, which silently drops the event, and prefetching proxies do the opposite by loading the pixel before a human reads anything.

[Apple Mail Privacy Protection](https://support.apple.com/guide/iphone/protect-your-email-privacy-iph0c7457a45/ios), on by default since iOS 15, routes images through Apple's proxy and pre-loads them, registering an open whether or not the recipient saw the message. Apple's share of the email client market sits near 50%, so a large slice of your open data is inflated. The practical takeaway: report opens as "opened at least once," never as a unique read count, and never gate revenue logic on an open alone.

Click tracking is sturdier because it records a real human action. When you set `links: true`, Nylas rewrites every valid HTML anchor in the body to a tracking URL, logs the click, then forwards the recipient to the original destination. It rewrites links across the whole message rather than per-link, caps at 100 tracked links, and skips URLs that carry login credentials so it doesn't break authenticated destinations. Reply tracking via `thread.replied` is the strongest signal of the three, since a reply is unambiguous intent.

Each trigger also ships a `.legacy` variant (`message.opened.legacy`, `message.link_clicked.legacy`, `thread.replied.legacy`) for applications still on the older notification format. New integrations should subscribe to the non-legacy triggers shown above. Because tracking rewrites message content and pixels recipients, be transparent: honor consent, disclose tracking where your jurisdiction requires it, and don't track links that carry sensitive data. For the full field reference and per-trigger payloads, see [tracking messages](/docs/v3/email/message-tracking/).

## What's next

- [Tracking messages](/docs/v3/email/message-tracking/) for the full tracking reference, scopes, and per-provider behavior
- [Get real-time updates with webhooks](/docs/cookbook/use-cases/build/realtime-webhooks/) to subscribe one endpoint to messages, opens, replies, and calendar events
- [Using webhooks with Nylas](/docs/v3/notifications/) for webhook setup, signature verification, and failure handling
- [Webhook notification schemas](/docs/reference/notifications/) for every trigger's payload shape