# How Listmonk Tracks Clicks and Views in Email Campaigns: A Deep Dive

> Discover how Listmonk tracks email opens and link clicks using tracking URLs and pixel images. Learn about its privacy settings and database recording for campaign analytics.

- Repository: [Kailash Nadh/listmonk](https://github.com/knadh/listmonk)
- Tags: deep-dive
- Published: 2026-05-19

---

**Listmonk tracks email opens and link clicks using template-generated tracking URLs and 1×1 pixel images, recording events in the `campaign_views` and `link_clicks` database tables while respecting configurable privacy settings.**

Listmonk is a high-performance, open-source newsletter and mailing list manager that provides granular campaign analytics through a sophisticated tracking mechanism. Understanding how listmonk tracks click and view links in campaigns reveals a multi-layered architecture spanning template preprocessing, HTTP handlers, and database persistence. The implementation relies on specific components within the `internal/manager`, `cmd/public`, and `internal/core` packages to capture engagement data accurately.

## Template Processing: Generating Tracking URLs

Tracking begins when a campaign is being **sent**, as the manager processes the email body to inject measurable elements. This occurs in [`internal/manager/manager.go`](https://github.com/knadh/listmonk/blob/main/internal/manager/manager.go), where URLs are rewritten and view pixels are generated before the message leaves the server.

### Link Rewriting with trackLink

The `trackLink` function intercepts every hyperlink in the email body and transforms it into a trackable endpoint. When building the message, the manager calls this function to register the raw URL in the database and return a tracking surrogate.

```go
// trackLink registers a URL and returns its UUID to be used in message templates
// for tracking links.
func (m *Manager) trackLink(url, campUUID, subUUID string) string {
    if m.cfg.DisableTracking { return url }
    url = strings.ReplaceAll(url, "&amp;", "&")
    // … cache lookup …
    uu, err := m.store.CreateLink(url)            // registers the raw URL
    // …
    return fmt.Sprintf(m.cfg.LinkTrackURL, uu, campUUID, subUUID)
}

```

The generated URL follows the pattern defined by `cfg.LinkTrackURL`, defaulting to `{{.BaseURL}}/t/{{.LinkUUID}}/{{.CampUUID}}/{{.SubUUID}}`. This ensures that when a recipient clicks a link, the request first hits listmonk's tracking endpoint before redirecting to the original destination.

### The View Tracking Pixel

For open tracking, listmonk injects a 1×1 transparent pixel via the `TrackView` template function defined in [`internal/manager/manager.go`](https://github.com/knadh/listmonk/blob/main/internal/manager/manager.go) within `makeGnericFuncMap`. This template tag expands to a URL such as `/t/v/{{.CampUUID}}/{{.SubUUID}}`, which renders as an invisible image in the email client.

## HTTP Handlers: Capturing Engagement Events

Once the email is delivered, recipient interactions trigger HTTP endpoints exposed by the public API in [`cmd/public.go`](https://github.com/knadh/listmonk/blob/main/cmd/public.go). These handlers receive the UUIDs embedded in the tracking URLs and delegate persistence to the core layer.

### Recording Campaign Views

The view pixel requests hit `GET /track/view/:campUUID/:subUUID/:dummy?`, handled by `RegisterCampaignView`. This function validates the request and records the open event while always returning a valid image to prevent broken image icons in email clients.

```go
func (a *App) RegisterCampaignView(c echo.Context) error {
    if a.cfg.Privacy.DisableTracking { … return pixel }
    subUUID := c.Param("subUUID")
    if !a.cfg.Privacy.IndividualTracking { subUUID = "" }
    campUUID := c.Param("campUUID")
    if campUUID != dummyUUID && subUUID != dummyUUID {
        a.core.RegisterCampaignView(campUUID, subUUID) // ← core call
    }
    // always return a 1×1 PNG
}

```

### Handling Link Clicks

Link clicks route to `GET /track/link/:linkUUID/:campUUID/:subUUID`, implemented by `RegisterCampaignLinkClick`. This handler looks up the original destination, records the click, and issues an HTTP 302 redirect.

```go
url, err := a.core.RegisterCampaignLinkClick(linkUUID, campUUID, subUUID)
// on success, HTTP 302 redirects to the original URL

```

## Core Registration and Database Persistence

The [`internal/core/campaigns.go`](https://github.com/knadh/listmonk/blob/main/internal/core/campaigns.go) file contains the business logic that actually commits tracking data to the database, isolating SQL execution from the HTTP transport layer.

### Registering Campaign Views

The `RegisterCampaignView` function executes a prepared statement to insert a row into the `campaign_views` table, capturing the campaign ID, subscriber ID, and timestamp.

```go
func (c *Core) RegisterCampaignView(campUUID, subUUID string) error {
    if _, err := c.q.RegisterCampaignView.Exec(campUUID, subUUID); err != nil { … }
    return nil
}

```

### Registering Link Clicks

Similarly, `RegisterCampaignLinkClick` records the interaction in `link_clicks` and retrieves the original URL for the redirect.

```go
func (c *Core) RegisterCampaignLinkClick(linkUUID, campUUID, subUUID string) (string, error) {
    var url string
    if err := c.q.RegisterLinkClick.Get(&url, linkUUID, campUUID, subUUID); err != nil { … }
    return url, nil
}

```

### Prepared Statements in models/queries.go

These core functions rely on prepared statements defined in [`models/queries.go`](https://github.com/knadh/listmonk/blob/main/models/queries.go):

```go
RegisterCampaignView      *sqlx.Stmt `query:"register-campaign-view"`
RegisterLinkClick        *sqlx.Stmt `query:"register-link-click"`

```

## Database Schema and Storage Architecture

The tracking data resides in two primary tables created via migration files such as [`internal/migrations/v2.0.0.go`](https://github.com/knadh/listmonk/blob/main/internal/migrations/v2.0.0.go) and [`v3.0.0.go`](https://github.com/knadh/listmonk/blob/main/v3.0.0.go):

- **`campaign_views`**: Stores `(campaign_id, subscriber_id, created_at)` tuples to count unique and total opens.
- **`link_clicks`**: Stores `(link_id, campaign_id, subscriber_id, created_at)` to attribute clicks to specific links and recipients.

These tables support materialized views for fast aggregation, exposed through the `Campaign` model in [`models/campaigns.go`](https://github.com/knadh/listmonk/blob/main/models/campaigns.go) via `Views` and `Clicks` fields.

## Privacy Controls and Configuration

Listmonk respects user privacy through settings defined in [`internal/core/settings.go`](https://github.com/knadh/listmonk/blob/main/internal/core/settings.go) and accessed via `cfg.Privacy`:

- **`DisableTracking`**: When enabled, all tracking is bypassed. The `trackLink` function returns raw URLs, and view handlers return the pixel without database writes.
- **`IndividualTracking`**: When set to `false`, the subscriber UUID is emptied before processing, recording aggregate counts without linking events to specific individuals.
- **`AllowExport`**: Governs whether subscribers can request a data export of their personal view and click history via `cmd/public.go → SelfExportSubscriberData`.

## Summary

- **Template Generation**: [`internal/manager/manager.go`](https://github.com/knadh/listmonk/blob/main/internal/manager/manager.go) rewrites links via `trackLink` and injects view pixels via `TrackView` during campaign sending.
- **HTTP Capture**: [`cmd/public.go`](https://github.com/knadh/listmonk/blob/main/cmd/public.go) handlers `RegisterCampaignView` and `RegisterCampaignLinkClick` receive tracking requests and interface with the core layer.
- **Data Persistence**: [`internal/core/campaigns.go`](https://github.com/knadh/listmonk/blob/main/internal/core/campaigns.go) commits events to `campaign_views` and `link_clicks` tables using prepared statements from [`models/queries.go`](https://github.com/knadh/listmonk/blob/main/models/queries.go).
- **Privacy Compliance**: Three configuration options allow administrators to disable tracking entirely, anonymize subscriber data, or permit self-service data exports.

## Frequently Asked Questions

### How does Listmonk track email opens without storing personally identifiable information?

Listmonk inserts a 1×1 transparent pixel URL generated by the `TrackView` template function. When the email client loads this image, the `RegisterCampaignView` handler in [`cmd/public.go`](https://github.com/knadh/listmonk/blob/main/cmd/public.go) receives the request. If the `IndividualTracking` privacy setting is disabled, the handler empties the subscriber UUID before recording the event, storing only that an open occurred without linking it to a specific individual.

### What happens when link tracking is disabled in listmonk?

When `DisableTracking` is set to `true` in the privacy configuration, the `trackLink` function in [`internal/manager/manager.go`](https://github.com/knadh/listmonk/blob/main/internal/manager/manager.go) returns the original URL unchanged, bypassing the UUID generation and database registration. Consequently, no entry is created in the `link_clicks` table, and recipients navigate directly to the destination without hitting listmonk's redirect endpoint.

### How does listmonk reconstruct the original URL when a tracked link is clicked?

When a tracked link is clicked, the `RegisterCampaignLinkClick` handler invokes `Core.RegisterCampaignLinkClick` in [`internal/core/campaigns.go`](https://github.com/knadh/listmonk/blob/main/internal/core/campaigns.go). This function executes the `RegisterLinkClick` prepared statement, which inserts a row into `link_clicks` and uses a `RETURNING` clause (or equivalent) to fetch the original URL stored during the initial `CreateLink` call. The handler then returns an HTTP 302 redirect to that original URL.

### Can subscribers export their own campaign view and click history?

Yes, if the `AllowExport` privacy setting is enabled, subscribers can request a data export through the `SelfExportSubscriberData` handler in [`cmd/public.go`](https://github.com/knadh/listmonk/blob/main/cmd/public.go). This retrieves the subscriber's aggregated data from the `campaign_views` and `link_clicks` tables, providing a complete record of their interactions with campaigns sent through the listmonk instance.