Skip to main content
Resend Docs

Search documentation

Type to search this documentation.

On this pageOverview

Open and Click Tracking

Open and click tracking is disabled by default for all domains. We suggest only tracking open rates for Broadcasts to ensure that inbox providers do not mistakenly identify your transactional emails as marketing emails.

You can enable tracking in the Resend Dashboard under the Configuration tab or programmatically when you create a new domain or update an existing domain.

Once verified, all tracked links in your emails will use your tracking subdomain (e.g., links.emails.example.com).

Go to the Domains page and click on the domain you want to configure.

In the Configuration tab, click Configure under Enable tracking metrics.

Open and Click Tracking

Provide a name for your tracking subdomain, enable open and/or click tracking, and click +Add domain.

Verify Custom Tracking Subdomain

Add the CNAME record to your DNS settings (e.g. Cloudflare, GoDaddy, etc.) to verify your tracking subdomain.

Add Custom Tracking Subdomain

Click I've added the records to verify the domain.

Once verified, all tracked links in your emails will use your custom tracking subdomain.

You can add a tracking subdomain when creating a new domain by passing the tracking_subdomain field along with click_tracking and/or open_tracking.

Node.js
import { Resend } from 'resend';

const resend = new Resend('re_xxxxxxxxx');

const { data, error } = await resend.domains.create({
  name: 'example.com',
  openTracking: true,
  clickTracking: true,
  trackingSubdomain: 'links',
});
PHP
$resend = Resend::client('re_xxxxxxxxx');

$resend->domains->create([
  'name' => 'example.com',
  'open_tracking' => true,
  'click_tracking' => true,
  'tracking_subdomain' => 'links',
]);
Python
import resend

resend.api_key = "re_xxxxxxxxx"

params: resend.Domains.CreateParams = {
  "name": "example.com",
  "open_tracking": True,
  "click_tracking": True,
  "tracking_subdomain": "links",
}

resend.Domains.create(params)
Ruby
require "resend"

Resend.api_key = "re_xxxxxxxxx"

params = {
  name: "example.com",
  open_tracking: true,
  click_tracking: true,
  tracking_subdomain: "links",
}
Resend::Domains.create(params)
Go
package main

import "github.com/resend/resend-go/v4"

func main() {
	client := resend.NewClient("re_xxxxxxxxx")

	params := &resend.CreateDomainRequest{
		Name:              "example.com",
		OpenTracking:      resend.Bool(true),
		ClickTracking:     resend.Bool(true),
		TrackingSubdomain: "links",
	}

	client.Domains.Create(params)
}
Rust
use resend_rs::{types::CreateDomainOptions, Resend, Result};

#[tokio::main]
async fn main() -> Result<()> {
  let resend = Resend::new("re_xxxxxxxxx");

  let _domain = resend
    .domains
    .create(
      CreateDomainOptions::new("example.com")
        .with_open_tracking(true)
        .with_click_tracking(true)
        .with_tracking_subdomain("links"),
    )
    .await?;

  Ok(())
}
Java
import com.resend.*;
import com.resend.services.domains.model.CreateDomainOptions;

Resend resend = new Resend("re_xxxxxxxxx");

CreateDomainOptions params = CreateDomainOptions.builder()
        .name("example.com")
        .openTracking(true)
        .clickTracking(true)
        .trackingSubdomain("links")
        .build();

resend.domains().create(params);
.NET
using Resend;

IResend resend = ResendClient.Create( "re_xxxxxxxxx" );

var resp = await resend.DomainAddAsync( new DomainAddData()
{
    DomainName = "example.com",
    OpenTracking = true,
    ClickTracking = true,
    TrackingSubdomain = "links",
} );
cURL
curl -X POST 'https://api.resend.com/domains' \
     -H 'Authorization: Bearer re_xxxxxxxxx' \
     -H 'Content-Type: application/json' \
     -d $'{
  "name": "example.com",
  "open_tracking": true,
  "click_tracking": true,
  "tracking_subdomain": "links"
}'

If you already have a domain, you can call the update endpoint to add a tracking subdomain and enable tracking.

Node.js
import { Resend } from 'resend';

const resend = new Resend('re_xxxxxxxxx');

const { data, error } = await resend.domains.update({
  id: 'd91cd9bd-1176-453e-8fc1-35364d380206',
  openTracking: true,
  clickTracking: true,
  trackingSubdomain: 'links',
});
PHP
$resend = Resend::client('re_xxxxxxxxx');

$resend->domains->update(
  'd91cd9bd-1176-453e-8fc1-35364d380206',
  [
    'open_tracking' => true,
    'click_tracking' => true,
    'tracking_subdomain' => 'links',
  ]
);
Python
import resend

resend.api_key = "re_xxxxxxxxx"

params: resend.Domains.UpdateParams = {
  "id": "d91cd9bd-1176-453e-8fc1-35364d380206",
  "open_tracking": True,
  "click_tracking": True,
  "tracking_subdomain": "links",
}

resend.Domains.update(params)
Ruby
require "resend"

Resend.api_key = "re_xxxxxxxxx"

Resend::Domains.update({
  id: "d91cd9bd-1176-453e-8fc1-35364d380206",
  open_tracking: true,
  click_tracking: true,
  tracking_subdomain: "links",
})
Go
package main

import "github.com/resend/resend-go/v4"

func main() {
	client := resend.NewClient("re_xxxxxxxxx")

	params := &resend.UpdateDomainRequest{
		OpenTracking:      true,
		ClickTracking:     true,
		TrackingSubdomain: "links",
	}

	client.Domains.Update("d91cd9bd-1176-453e-8fc1-35364d380206", params)
}
Rust
use resend_rs::{types::DomainChanges, Resend, Result};

#[tokio::main]
async fn main() -> Result<()> {
  let resend = Resend::new("re_xxxxxxxxx");

  let changes = DomainChanges::new()
    .with_open_tracking(true)
    .with_click_tracking(true)
    .with_tracking_subdomain("links");

  let _domain = resend
    .domains
    .update("d91cd9bd-1176-453e-8fc1-35364d380206", changes)
    .await?;

  Ok(())
}
Java
import com.resend.*;
import com.resend.services.domains.model.UpdateDomainOptions;

Resend resend = new Resend("re_xxxxxxxxx");

UpdateDomainOptions params = UpdateDomainOptions.builder()
        .id("d91cd9bd-1176-453e-8fc1-35364d380206")
        .openTracking(true)
        .clickTracking(true)
        .trackingSubdomain("links")
        .build();

resend.domains().update(params);
.NET
using Resend;

IResend resend = ResendClient.Create( "re_xxxxxxxxx" );

await resend.DomainUpdateAsync(
    new Guid( "d91cd9bd-1176-453e-8fc1-35364d380206" ),
    new DomainUpdateData()
    {
        TrackOpen = true,
        TrackClicks = true,
        TrackingSubdomain = "links",
    }
);
cURL
curl -X PATCH 'https://api.resend.com/domains/d91cd9bd-1176-453e-8fc1-35364d380206' \
     -H 'Authorization: Bearer re_xxxxxxxxx' \
     -H 'Content-Type: application/json' \
     -d $'{
  "open_tracking": true,
  "click_tracking": true,
  "tracking_subdomain": "links"
}'

Whether creating or updating a domain, the API will return the required DNS records that need to be added to your domain provider's DNS settings. Note the Tracking record.

API
{  "id": "4dd369bc-aa82-4ff3-97de-514ae3000ee0",  "name": "example.com",  "created_at": "2026-03-28 17:12:02.059593+00",  "status": "not_started",  "open_tracking": true,  "click_tracking": true,  "tracking_subdomain": "links",  "capabilities": {    "sending": "enabled",    "receiving": "disabled"  },  "records": [    // ... other records ...    {      "record": "Tracking",      "name": "links.example.com",      "type": "CNAME",      "value": "links1.resend-dns.com",      "ttl": "Auto",      "status": "not_started"    }  ],  "region": "us-east-1"}

Add the required CNAME record to your domain provider's DNS settings (e.g. Cloudflare, GoDaddy, etc.).

Once you've added the required DNS record(s), call the verify endpoint to verify the domain.

Node.js
import { Resend } from 'resend';

const resend = new Resend('re_xxxxxxxxx');

const { data, error } = await resend.domains.verify(
  'd91cd9bd-1176-453e-8fc1-35364d380206',
);
PHP
$resend = Resend::client('re_xxxxxxxxx');

$resend->domains->verify('d91cd9bd-1176-453e-8fc1-35364d380206');
Python
import resend

resend.api_key = "re_xxxxxxxxx"

resend.Domains.verify(domain_id="d91cd9bd-1176-453e-8fc1-35364d380206")
Ruby
require "resend"

Resend.api_key = "re_xxxxxxxxx"

Resend::Domains.verify("d91cd9bd-1176-453e-8fc1-35364d380206")
Go
package main

import "github.com/resend/resend-go/v4"

func main() {
	client := resend.NewClient("re_xxxxxxxxx")

	client.Domains.Verify("d91cd9bd-1176-453e-8fc1-35364d380206")
}
Rust
use resend_rs::{Resend, Result};

#[tokio::main]
async fn main() -> Result<()> {
  let resend = Resend::new("re_xxxxxxxxx");

  resend
    .domains
    .verify("d91cd9bd-1176-453e-8fc1-35364d380206")
    .await?;

  Ok(())
}
Java
import com.resend.*;
import com.resend.services.domains.model.VerifyDomainResponse;

Resend resend = new Resend("re_xxxxxxxxx");

VerifyDomainResponse verified = resend.domains().verify("d91cd9bd-1176-453e-8fc1-35364d380206");
.NET
using Resend;

IResend resend = ResendClient.Create( "re_xxxxxxxxx" );

await resend.DomainVerifyAsync( new Guid( "d91cd9bd-1176-453e-8fc1-35364d380206" ) );
cURL
curl -X POST 'https://api.resend.com/domains/d91cd9bd-1176-453e-8fc1-35364d380206/verify' \
     -H 'Authorization: Bearer re_xxxxxxxxx' \
     -H 'Content-Type: application/json'

All tracked links in your emails will use your tracking subdomain.

View the API reference for more details.

A 1x1 pixel transparent GIF image is inserted in each email and includes a unique reference to this image file. When the image is downloaded, Resend can tell exactly which message was opened and by whom.

To track clicks, Resend modifies each link in the body of the HTML email to point to your tracking subdomain.

When recipients clicks a link, the request is redirected to your tracking subdomain, their click event is recorded, and they are redirected to the original URL.

When you change your tracking subdomain, you may see two CNAME records in the DNS Records table: one active and one inactive (shown dimmed). The inactive record is kept intentionally to avoid breaking links in emails that were already sent using the previous subdomain. You do not need to remove it.

Because tracking subdomains are used in email links, they're handled differently than other records.

  • Not removable: After your tracking subdomain has been created, it can only be changed, never removed. This behavior preserves any email links that may already be sent with the current tracking subdomain. For this reason, do not remove old tracking DNS records. (All previously used records remain active and are included in the response.)
  • Requires verification: After changing the tracking subdomain, a new DNS record must be verified. Until then, the previous value is used.

If you remove a domain with a configured tracking subdomain, the Resend provisioned proxy will also be removed, breaking any existing email links that use that subdomain.

To keep links working, set up your own proxy pointing to Resend’s tracking DNS records before removing the domain.

Custom tracking subdomains require us to issue a TLS certificate. Some domains under regional restrictions are not supported by our certificate provider and cannot use custom tracking subdomains. Learn more in the certificate provider docs.

Suggest an edit

Propose a replacement for this page. The site team reviews it before applying any changes.

Export
Documentation menu