# Tracking messages

Source: https://developer.nylas.com/docs/v3/email/message-tracking/

The Nylas Messages API offers several tracking options that allow you to monitor engagement with the messages you send. You can monitor the following interactions:

- A link in the message has been clicked.
- The message has been opened.
- Someone replied to the thread.

> **Warn:** 
> **Message tracking is not available for Sandbox applications**. To use message tracking features, you need to upgrade to a production application. For more information about upgrading, see [Using the Dashboard](/docs/dev-guide/dashboard/)
> 
> If you're using a Sandbox application (trial account), you'll receive an error message: "Tracking options are not allowed for trial accounts".

## How message tracking works

When you enable a tracking flag, Nylas modifies the content of the message so it can be tracked. You can subscribe to the notification triggers for each of the available tracking options to be notified when an event occurs.

When a user acts on a message that you enabled tracking for (for example, opening the message), Nylas sends a `POST` to your notification endpoint (webhook listener or Pub/Sub topic) with information about the action. This includes important information that you can use to track or respond to the event.

To learn more about the general structure of message tracking notifications, see the [list of notification schemas](/docs/reference/notifications/#message-tracking-notifications).

> **Warn:** 
> **If you delete a grant, tracking links associated with that grant stop working.** Nylas can no longer match incoming tracking events to the deleted grant, so you lose open, click, and reply notifications for messages sent before the deletion. If a grant expires, re-authenticate it instead of deleting it to preserve tracking data. See [Handling expired grants](/docs/dev-guide/best-practices/grant-lifecycle/) for details.

## Scopes for message tracking

Before you start using message tracking, you need to request the following [scopes](/docs/dev-guide/scopes/):

- **Google**: `gmail.send`
- **Microsoft**: `Mail.ReadWrite`, `Mail.Send`

> **Info:** 
> **IMAP connectors don't support scopes**. Don't worry — you'll still receive webhook and Pub/Sub notifications when their trigger conditions are met. For more information, see [Create grants with IMAP authentication](/docs/v3/auth/imap/).

## Enable message tracking

To enable tracking for a message, include one of the `tracking_options` JSON object in your [Send Message](/docs/reference/api/messages/send-message/) or [Create Draft](/docs/reference/api/drafts/post-draft/) request.

```json
...
  "tracking_options": {
    "opens": true,      // Enable message open tracking.
    "links": true,      // Enable link clicked tracking.
    "thread_replies": true,    // Enable thread replied tracking.
    "label": "Use this string to describe the message you're enabling tracking for. It's included in notifications about tracked events."
  }
...
```

```python [enableTracking-Python SDK]

from nylas import Client

nylas = Client(
    "<NYLAS_API_KEY>",
    "<NYLAS_API_URI>"
)

grant_id = "<NYLAS_GRANT_ID>"
email = "<EMAIL>"

message = nylas.messages.send(
  grant_id,
  request_body={
    "to": [{ "name": "Name", "email": email }],
    "reply_to": [{ "name": "Name", "email": email }],
    "reply_to_message_id": "<MESSAGE_ID>",
    "subject": "Your Subject Here",
    "body": "Your email body here.",
    "tracking_options": {
      "opens": True,
      "links": True,
      "thread_replies": True,
    }
  }
)

print(message)

```

```ruby [enableTracking-Ruby SDK]
require 'nylas'

nylas = Nylas::Client.new(api_key: '<NYLAS_API_KEY>')

request_body = {
  subject: "With Love, from Nylas",
  body: "This email was sent using the <b>Ruby SDK</b> for the Nylas Email API.
      Visit <a href='https://nylas.com'>Nylas.com</a> for details.",
  to: [{name: "Nylas", email: "ireadthedocs@nylas.com"}],
  tracking_options: {label: "Track this message",
      opens: true,
      links: true,
      thread_replies: true}
}

email, _ = nylas.messages.send(identifier: '<NYLAS_GRANT_ID>', request_body: request_body)

puts "Message \"#{email[:subject]}\" was sent with ID #{email[:id]}"
```

```java [enableTracking-Java SDK]


public class EmailTracking {
  public static void main(String[] args) throws NylasSdkTimeoutError, NylasApiError {
    NylasClient nylas = new NylasClient.Builder("<NYLAS_API_KEY>").build();
    List<EmailName> emailNames = new ArrayList<>();
    emailNames.add(new EmailName("swag@example.com", "Nylas"));
    TrackingOptions options = new TrackingOptions("Track this message",true, true, true);

    SendMessageRequest requestBody = new SendMessageRequest.Builder(emailNames).
        subject("With Love, from Nylas").
        body("This email was sent using the <b>Java SDK</b> for the Nylas Email API. " +
            "Visit <a href='https://nylas.com'>Nylas.com</a> for details.").
        trackingOptions(options).build();

    Response<Message> email = nylas.messages().send("<NYLAS_GRANT_ID>", requestBody);

    System.out.println("Message " + email.getData().getSubject() + " was sent with ID " + email.getData().getId());
  }
}
```

```kt [enableTracking-Kotlin SDK]


fun main(args: Array<String>) {
  val nylas: NylasClient = NylasClient(apiKey = "<NYLAS_API_KEY>")
  val emailNames : List<EmailName> = listOf(EmailName("swag@example.com", "Nylas"))
  val options : TrackingOptions = TrackingOptions("Track this message", true, true, true)

  val requestBody : SendMessageRequest = SendMessageRequest.
      Builder(emailNames).
      subject("With Love, from Nylas").
      body("This email was sent using the <b>Kotlin SDK</b> for the Nylas Email API. " +
          "See the <a href='https://developer.nylas.com/docs/v3/sdks/'>Nylas documentation</a> for details.").
      trackingOptions(options).
      build()

  val email = nylas.messages().send("<NYLAS_GRANT_ID>", requestBody)

  print("Message " + email.data.subject + " was sent with ID " + email.data.id)
}
```

## Link clicked tracking

When you enable link clicked tracking for a message, Nylas replaces the links in the message with tracking links. When a user clicks one of the links, Nylas logs the click, forwards the user to the original link address, and sends you a notification.

> **Info:** 
> **Link clicked tracking applies to all links in a message, with some exceptions for security purposes**. It cannot be enabled for only _some_ links.

Nylas ignores any links that contain credentials, so you don't have to worry about rewriting sensitive URLs. For more information, see the [Best practices for link clicked tracking section](#best-practices-for-link-clicked-tracking).

### Formatting links for tracking

For link clicked tracking to work correctly, you must use a valid URI format and enclose the link in HTML anchor tags (for example, `<a href="https://www.example.com">link</a>`). Nylas supports the following URI schemes:

- `https`
- `mailto`
- `tel`

Nylas can detect and track links like the following examples:

- `<a href="https://www.google.com">google.com</a>`
- `<a href="mailto:nyla@example.com">Mailto Nyla!</a>`
- `<a href="tel:+1-201-555-0123">Call now to make your reservation.</a>`

The links below are invalid and cannot be tracked:

- `<a>www.google.com</a>`: Improperly formatted HTML.
- `<a href="zoommtg://zoom.us/join?confno=12345">Join a zoom conference</a>`: Unsupported URI scheme (`zoommtg:`).

> **Warn:** 
> **Nylas does not replace invalid links with a tracking link**. They are left as-written and are not tracked. Be sure to double-check your links before sending a message!

### Link clicked tracking examples

The following example shows the type of JSON notification you can expect.

```json

{
  "specversion": "1.0",
  "type": "message.link_clicked",
  "source": "/com/nylas/tracking",
  "id": "<WEBHOOK_ID>",
  "time": 1695480423,
  "data": {
    "application_id": "NYLAS_APPLICATION_ID",
    "grant_id": "NYLAS_GRANT_ID",
    "object": {
      "link_data": [
        {
          "count": 1,
          "url": "https://www.example.com"
        }
      ],
      "message_id": "18ac281f237c934b",
      "label": "Hey, just testing",
      "recents": [
        {
          "click_id": "0",
          "ip": "<IP ADDR>",
          "link_index": "0",
          "timestamp": 1695480422,
          "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36"
        },
        {
          "click_id": "1",
          "ip": "<IP ADDR>",
          "link_index": "0",
          "timestamp": 1695480422,
          "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36"
        },
        {
          "click_id": "2",
          "ip": "<IP ADDR>",
          "link_index": "0",
          "timestamp": 1695480422,
          "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36"
        }
      ],
      "sender_app_id": "<app id>",
      "timestamp": 1695480422
    }
  }
}


```

For more information about metadata in link clicked tracking responses, see the [Link Clicked notification schema](/docs/reference/notifications/message-tracking/message-link_clicked/).

#### The Recents array in link clicked tracking

The `recents` array in a [`message.link_clicked` notification](/docs/reference/notifications/message-tracking/message-link_clicked/) contains entries for the last 50 link tracking events for a specific link, in a specific message. Each entry includes a `link_index`, which identifies the link that was clicked, and a `click_id`, which identifies the specific event. The user's IP address and [user agent](https://www.useragents.me/) are also logged.

Event IDs are unique only within a specific `recents` array, and each message/trigger pair has its own `recents` array.

The `message.link_clicked` notification payload also includes the `link_data` dictionary, which contains the links from the message and a `count` representing how many times each link has been clicked at the time that the notification was generated.

> **Info:** 
> **Note**: While the `count` of events starts at `1`, the `click_id` and `opened_id` indices start at `0`.

### Best practices for link clicked tracking

When you enable link clicked tracking, Nylas rewrites all valid HTML links with a new URL that allows tracking. This process omits and does not rewrite tracking links with embedded login credentials, because the destination servers don't recognize the rewritten credentials. For this reason, Nylas ignores links that contain credentials. The links still work when clicked, but Nylas does not track user interactions with them. For example, private Google Form URLs contain login credentials, so Nylas ignores those links and they cannot be tracked.

## Message open tracking

When you enable message open tracking for a message, Nylas inserts a transparent one-pixel image into the message's HTML. When a recipient opens the message, their email client makes a request to Nylas to download the file. Nylas records that request as a `message.opened` event and sends you a notification.

> **Info:** 
> **If your grant is expired, you'll still receive `message.opened` notifications**. If your grant has been deleted, however, Nylas stops sending `message.send` notifications.

Because this method relies on the email client requesting the file from Nylas' servers, ad blockers and content delivery networks (CDNs) can interfere with message open tracking. It's best to use message open tracking along with link clicked tracking and other methods. For more information, see [Troubleshooting immediate webhook notifications](/docs/support/troubleshooting/immediate-webhook-notification/).

### Message open tracking examples

The following code samples show how to enable message open tracking for a message, and the type of JSON notification you can expect.

```bash
curl --request POST \
  --url https://api.us.nylas.com/v3/grants/<NYLAS_GRANT_ID>/messages/send \
  --header 'Authorization: Bearer <NYLAS_API_KEY>' \
  --header 'Content-Type: application/json' \
  --data '{
	"subject": "Hey Reaching Out with Nylas",
	"body": "Hey I would like to track this link <a href='https://espn.com'>My Example Link</a>",
	"to": [
		{
			"name": "John Doe",
			"email": "john.doe@example.com"
		}
	],
	"tracking_options": {
		"opens": true
	}
}'


```

```json [messageOpen-Notification (JSON)]

{
  "specversion": "1.0",
  "type": "message.opened",
  "source": "/com/nylas/tracking",
  "id": "<WEBHOOK_ID>",
  "time": 1695480567,
  "data": {
    "application_id": "NYLAS_APPLICATION_ID",
    "grant_id": "NYLAS_GRANT_ID",
    "object": {
      "message_data": {
        "count": 1,
        "timestamp": 1695480410
      },
      "message_id": "18ac281f237c934b",
      "label": "Testing Nylas Messaged Opened Tracking",
      "recents": [
        {
          "ip": "<IP ADDR>",
          "opened_id": 0,
          "timestamp": 1695480567,
          "user_agent": "Mozilla/5.0"
        },
        {
          "ip": "<IP ADDR>",
          "opened_id": 1,
          "timestamp": 1695480567,
          "user_agent": "Mozilla/5.0"
        },
        {
          "ip": "<IP ADDR>",
          "opened_id": 2,
          "timestamp": 1695480567,
          "user_agent": "Mozilla/5.0"
        }
      ],
      "sender_app_id": "<app id>",
      "timestamp": 1695480410
    }
  }
}


```

```js [messageOpen-Node.js SDK]

import Nylas from "nylas";

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

async function sendEmail() {
  try {
    const sentMessage = await nylas.messages.send({
      identifier: "<NYLAS_GRANT_ID>",
      requestBody: {
        to: [{ name: "Name", email: "<EMAIL>" }],
        replyTo: [{ name: "Name", email: "<EMAIL>" }],
        replyToMessageId: "<MESSAGE_ID>",
        subject: "Your Subject Here",
        body: "Your email body here.",
        trackingOptions: {
          opens: true,
        },
      },
    });

    console.log("Email sent:", sentMessage);
  } catch (error) {
    console.error("Error sending email:", error);
  }
}

sendEmail();


```

```python [messageOpen-Python SDK]

from nylas import Client

nylas = Client(
    "<NYLAS_API_KEY>",
    "<NYLAS_API_URI>"
)

grant_id = "<NYLAS_GRANT_ID>"
email = "<EMAIL>"

message = nylas.messages.send(
  grant_id,
  request_body={
    "to": [{ "name": "Name", "email": email }],
    "reply_to": [{ "name": "Name", "email": email }],
    "reply_to_message_id": "<MESSAGE_ID>",
    "subject": "Your Subject Here",
    "body": "Your email body here.",
    "tracking_options": {
      "opens": True,
      "links": False,
      "thread_replies": False,
    }
  }
)

print(message)

```

```ruby [messageOpen-Ruby SDK]
require 'nylas'

nylas = Nylas::Client.new(api_key: '<NYLAS_API_KEY>')

request_body = {
  subject: "With Love, from Nylas",
  body: "This email was sent using the <b>Ruby SDK</b> for the Nylas Email API.
      Visit <a href='https://nylas.com'>Nylas.com</a> for details.",
  to: [{name: "Nylas", email: "swag@example.com"}],
  tracking_options: {label: "Track when the message gets opened",
      opens: true,
      links: false,
      thread_replies: false}
}

email, _ = nylas.messages.send(identifier: '<NYLAS_GRANT_ID>', request_body: request_body)

puts "Message \"#{email[:subject]}\" was sent with ID #{email[:id]}"
```

```java [messageOpen-Java SDK]


public class EmailTracking {
  public static void main(String[] args) throws NylasSdkTimeoutError, NylasApiError {
    NylasClient nylas = new NylasClient.Builder("<NYLAS_API_KEY>").build();

    List<EmailName> emailNames = new ArrayList<>();
    emailNames.add(new EmailName("swag@example.com", "Nylas"));

    TrackingOptions options = new TrackingOptions("Track when the message gets opened", true, false, false);

    SendMessageRequest requestBody = new SendMessageRequest.Builder(emailNames).
        subject("With Love, from Nylas").
        body("This email was sent using the <b>Java SDK</b> for the Nylas Email API. " +
            "Visit <a href='https://nylas.com'>Nylas.com</a> for details.").
        trackingOptions(options).build();

    Response<Message> email = nylas.messages().send("<NYLAS_GRANT_ID>", requestBody);

    System.out.println("Message " + email.getData().getSubject() + " was sent with ID " + email.getData().getId());
  }
}
```

```kt [messageOpen-Kotlin SDK]


fun main(args: Array<String>) {
  val nylas: NylasClient = NylasClient(apiKey = "<NYLAS_API_KEY>")
  val emailNames : List<EmailName> = listOf(EmailName("swag@example.com", "Nylas"))
  val options : TrackingOptions = TrackingOptions("Track when the message gets opened", true, false, false)

  val requestBody : SendMessageRequest = SendMessageRequest.
      Builder(emailNames).
      subject("With Love, from Nylas").
      body("This email was sent using the <b>Kotlin SDK</b> for the Nylas Email API. " +
          "Visit <a href='https://nylas.com'>Nylas.com</a> for details.").
      trackingOptions(options).
      build()

  val email = nylas.messages().send("<NYLAS_GRANT_ID>", requestBody)

  print("Message " + email.data.subject + " was sent with ID " + email.data.id)
}
```

#### The Recents array in message open tracking

Like [link clicked tracking](#the-recents-array-in-link-clicked-tracking), the notification for [`message.opened`](/docs/reference/notifications/message-tracking/message-opened/) events contains a `recents` array. This array contains entries for the last 50 events for the message that generated the notification. Each entry includes an `opened_id` which identifies the specific event, the timestamp for the `message.opened` event, and the user's IP address and [user agent](https://www.useragents.me/).

## Thread replied tracking

When you send a message with thread replied tracking enabled, Nylas notifies you when there are new responses to the thread.

### Thread replied tracking examples

The following code samples show how to enable thread replied tracking for a message, and the kind of JSON notification you can expect.

```bash
curl --request POST \
  --url https://api.us.nylas.com/v3/grants/<NYLAS_GRANT_ID>/messages/send \
  --header 'Authorization: Bearer <NYLAS_API_KEY>' \
  --header 'Content-Type: application/json' \
  --data '{
	"subject": "Hey Reaching Out with Nylas",
	"body": "Hey I would like to track this link <a href='https://espn.com'>My Example Link</a>",
	"to": [
		{
			"name": "John Doe",
			"email": "john.doe@example.com"
		}
	],
	"tracking_options": {
		"thread_replies": true
	}
}'


```

```json [threadReplied-Notification (JSON)]

{
  "specversion": "1.0",
  "type": "thread.replied",
  "source": "/com/nylas/tracking",
  "id": "<WEBHOOK_ID>",
  "time": 1696007157,
  "data": {
    "application_id": "NYLAS_APPLICATION_ID",
    "grant_id": "NYLAS_GRANT_ID",
    "object": {
      "message_id": "<message-id-of-reply>",
      "root_message_id": "<message-id-of-original-tracked-message>",
      "label": "some-client-label",
      "reply_data": {
        "count": 1
      },
      "sender_app_id": "<app-id>",
      "thread_id": "<thread-id-of-sent-message>",
      "timestamp": 1696007157
    }
  }
}


```

```js [threadReplied-Node.js SDK]

import Nylas from "nylas";

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

async function sendEmail() {
  try {
    const sentMessage = await nylas.messages.send({
      identifier: "<NYLAS_GRANT_ID>",
      requestBody: {
        to: [{ name: "Name", email: "<EMAIL>" }],
        replyTo: [{ name: "Name", email: "<EMAIL>" }],
        replyToMessageId: "<MESSAGE_ID>",
        subject: "Your Subject Here",
        body: "Your email body here.",
        trackingOptions: {
          threadReplies: true,
        },
      },
    });

    console.log("Email sent:", sentMessage);
  } catch (error) {
    console.error("Error sending email:", error);
  }
}

sendEmail();


```

```python [threadReplied-Python SDK]

from nylas import Client

nylas = Client(
    "<NYLAS_API_KEY>",
    "<NYLAS_API_URI>"
)

grant_id = "<NYLAS_GRANT_ID>"
email = "<EMAIL>"

message = nylas.messages.send(
  grant_id,
  request_body={
    "to": [{ "name": "Name", "email": email }],
    "reply_to": [{ "name": "Name", "email": email }],
    "reply_to_message_id": "<MESSAGE_ID>",
    "subject": "Your Subject Here",
    "body": "Your email body here.",
    "tracking_options": {
      "opens": False,
      "links": False,
      "thread_replies": True,
    }
  }
)

print(message)

```

```ruby [threadReplied-Ruby SDK]
require 'nylas'

nylas = Nylas::Client.new(api_key: '<NYLAS_API_KEY>')

request_body = {
  subject: "With Love, from Nylas",
  body: "This email was sent using the <b>Ruby SDK</b> for the Nylas Email API.
      Visit <a href='https://nylas.com'>Nylas.com</a> for details.",
  to: [{name: "Nylas", email: "swag@example.com"}],
  tracking_options: {label: "Track message replies",
      opens: false,
      links: false,
      thread_replies: true}
}

email, _ = nylas.messages.send(identifier: '<NYLAS_GRANT_ID>', request_body: request_body)

puts "Message \"#{email[:subject]}\" was sent with ID #{email[:id]}"
```

```java [threadReplied-Java SDK]


public class EmailTracking {
  public static void main(String[] args) throws NylasSdkTimeoutError, NylasApiError {
    NylasClient nylas = new NylasClient.Builder("<NYLAS_API_KEY>").build();

    List<EmailName> emailNames = new ArrayList<>();
    emailNames.add(new EmailName("swag@example.com", "Nylas"));

    TrackingOptions options = new TrackingOptions("Track message replies",false, false, true);

    SendMessageRequest requestBody = new SendMessageRequest.Builder(emailNames).
        subject("With Love, from Nylas").
        body("This email was sent using the <b>Java SDK</b> for the Nylas Email API. " +
            "Visit <a href='https://nylas.com'>Nylas.com</a> for details.").
        trackingOptions(options).build();

    Response<Message> email = nylas.messages().send("<NYLAS_GRANT_ID>", requestBody);

    System.out.println("Message " + email.getData().getSubject() + " was sent with ID " + email.getData().getId());
  }
}
```

```kt [threadReplied-Kotlin SDK]


fun main(args: Array<String>) {
  val nylas: NylasClient = NylasClient(apiKey = "<NYLAS_API_KEY>")
  val emailNames : List<EmailName> = listOf(EmailName("swag@example.com", "Nylas"))
  val options : TrackingOptions = TrackingOptions("Track message replies", false, false, true)

  val requestBody : SendMessageRequest = SendMessageRequest.
      Builder(emailNames).
      subject("With Love, from Nylas").
      body("This email was sent using the <b>Kotlin SDK</b> for the Nylas Email API. " +
          "Visit <a href='https://nylas.com'>Nylas.com</a> for details.").
      trackingOptions(options).
      build()

  val email = nylas.messages().send("<NYLAS_GRANT_ID>", requestBody)

  print("Message " + email.data.subject + " was sent with ID " + email.data.id)
}
```

## Message tracking errors

The following sections describe errors that you may encounter when using message tracking.

### Link clicked tracking errors

Link clicked tracking is limited to 100 tracked links per message. A message that contains more than 100 links and has link clicked tracking enabled will fail to send with an error message like the following:

> Too many tracking links: 101 exceeds the maximum of 100. Please reduce the number of links in your email.

Disable link clicked tracking or reduce the number of links in the message to send it.

### Message open tracking errors

If a message with message open tracking enabled cannot handle the metadata object's size (for example, the `payload` is too large), you might receive the following error message:

> The message has not been sent. Please try resending with open tracking disabled.

Disable message open tracking to send the message.

### Thread replied tracking errors

If a message with thread replied tracking enabled cannot handle the metadata object's size (for example, the `payload` is too large), you might receive the following error message:

> The message has not been sent. Please try resending with reply tracking disabled.

Disable thread replied tracking to send the message.