> ## Documentation Index
> Fetch the complete documentation index at: https://docs.polymorfa.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Track campaign links

> Give each campaign recipient a short link, count link opens per link, and receive the first open of each link as a webhook event.

Link tracking replaces a link in a campaign message with a short link that is
different for every recipient. When a recipient opens it, Polymorfa counts the
open and redirects them to your destination.

<Note>
  Link tracking is a beta. In the campaign builder's message step, a team owner
  or admin selects **Join** on the Link tracking beta row, reviews the terms and
  selects **Accept and join**. Until your team has joined and the beta is
  available to it, requests that set tracked links,
  launch a campaign with tracked links, or read link opens answer
  `403 feature_unavailable`.
</Note>

## What you can track

* Campaigns sent from linked-device numbers or test numbers. A campaign that
  would send from Official API numbers is refused at launch, because an
  approved template keeps its approved links.
* Up to five links per campaign. Each link has a `key` and a destination `url`.
* `key` is 1 to 32 lowercase letters, digits, or underscores, unique within the
  campaign.
* `url` is an absolute `https` URL with a public domain name, at most 2,048
  characters. User names, passwords, IP addresses, and local network names
  such as `localhost` or names ending in `.local` or `.internal` are refused.

## Add tracked links to a campaign

### Use the console

In the campaign builder's message step, open **Tracked links**, choose
**Add link**, and enter a key and a destination URL. The panel shows the
placeholder to paste into your message, such as `{{link:offer}}`.

### Use the API

Send `trackedLinks` when you create or update a draft, and write
`{{link:<key>}}` where the link belongs in the message.

```bash theme={null}
curl -X PATCH "https://api.polymorfa.com/messaging/projects/$PROJECT_SLUG/campaigns/$CAMPAIGN_ID" \
  -H "Authorization: Bearer $POLYMORFA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "trackedLinks": [
      { "key": "offer", "url": "https://example.com/spring-sale" }
    ]
  }'
```

On the Platform API, send the same field to `POST /platform/campaigns` or
`PATCH /platform/campaigns/{campaignId}`.

A message such as `Hi {{first_name}}, see the sale: {{link:offer}}` reaches
each recipient with their own short link in place of `{{link:offer}}`. The
placeholder works anywhere the campaign's message text does, including a
template's body and URL buttons.

Campaign reads return `trackedLinks`, an empty array when the campaign tracks
none. Send `null` or `[]` to remove the links; removing them does not need the
beta.

Tracked links change only while the campaign is a draft, including while it
waits for a scheduled start. After sending starts, a change answers `409`. A
launched campaign waiting for its start must still define every
`{{link:<key>}}` its messages use.

## Launch checks

Launch answers `400 invalid_parameter` with `param` set to `trackedLinks` when:

* a message uses a `{{link:<key>}}` the campaign does not define, so a
  placeholder is never sent as plain text; or
* the campaign has tracked links and would send from Official API numbers.

## What recipients receive

Each recipient gets a short link on the API host for every tracked link their
message uses:

```text theme={null}
https://api.polymorfa.com/l/<token>
```

* Opening it answers a temporary redirect to the destination the link had
  when the message was sent. A later change to the campaign never moves a sent
  link.
* The redirect sets no cookies and sends no referrer.
* A link stops working 90 days after its message was sent. An expired or
  unknown link answers `404`.
* When a send is retried, the retry carries a new short link. The earlier link
  keeps working until it expires.

## Read link opens

While your team has the beta, every `GET` request for a short link counts as an
open; `HEAD` requests never do. An open is not proof that a person clicked:
apps and security scanners that preview a link also open it.

```bash theme={null}
curl "https://api.polymorfa.com/messaging/projects/$PROJECT_SLUG/campaigns/$CAMPAIGN_ID/links" \
  -H "Authorization: Bearer $POLYMORFA_KEY"
```

```json theme={null}
{
  "success": true,
  "data": [
    { "key": "offer", "url": "https://example.com/spring-sale", "uniqueOpens": 412, "totalOpens": 655 }
  ]
}
```

| Field | Meaning |
| - | - |
| `uniqueOpens` | Recipients who opened the link at least once. |
| `totalOpens` | Every recorded open, including repeat opens. |

The Platform API returns the same list from
`GET /platform/campaigns/{campaignId}/links`. Reading needs `campaigns:read`.
In the console, the campaign's analytics page shows **Link opens**.

## Receive the first open as a webhook

Subscribe a project webhook to `campaign.recipient_link_opened`. Polymorfa
sends it once per recipient and tracked link, at the first open.

```json theme={null}
{
  "id": "019b2345-5555-7555-8555-555555555555",
  "session": "sales-1",
  "timestamp": "2026-10-05T14:20:00.000Z",
  "event": "campaign.recipient_link_opened",
  "payload": {
    "campaignId": "019b2345-3333-7333-8333-333333333333",
    "recipientId": "019b2345-4444-7444-8444-444444444444",
    "linkKey": "offer",
    "openedAt": 1791190800
  }
}
```

`session` names the number that sent the recipient's message. `openedAt` uses
Unix seconds. The event carries no phone number, link,
destination, IP address, or user agent. Use `recipientId` with the campaign's
recipient list to join it to your own records. See
[Campaign events](/api/webhooks#campaign-events) for delivery rules.

## Privacy and retention

* Polymorfa does not store a recipient's IP address or user agent with an
  open.
* Open counts are kept for 400 days after a recipient's first open of a link.
* Deleting a campaign or a recipient deletes its short links and opens.

## Leaving the beta

When your team withdraws from the beta or the beta becomes unavailable to it:

* Messages sent afterwards carry each link's original destination, without
  tracking.
* Short links already sent keep redirecting until they expire, without
  recording opens or sending events.
* Setting tracked links, launching a campaign with tracked links, and reading
  link opens answer `403 feature_unavailable`. Removing tracked links still
  works.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.