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

# Links and domains

> How creator links resolve on every device, and how to connect your own attribution domain

A creator link is a vanity URL that EVO owns the redirect for. Every hit is recorded before the visitor is forwarded, which is what mints the click token the rest of the system depends on.

## Link anatomy

```
https://tryapp.link/jane
        └─ domain ──┘└slug┘
```

* **Domain** — a host registered as an `AttributionDomain`. A link can only use a registered domain.
* **Slug** — lowercase `a-z0-9_-`, up to 64 characters. Required on shared domains, optional on a custom domain.

A custom domain can use the root, so `getdupe.app` with an empty slug is a valid link — that is what a creator says out loud on camera. Non-root slugs are for bio links and per-creator variants.

An unknown slug on a known domain falls back to that domain's root link, so a mistyped or stale path still lands on the product page and still counts a click.

### Link types

| Type  | Destination                                          | Notes                                                                       |
| ----- | ---------------------------------------------------- | --------------------------------------------------------------------------- |
| `web` | `destination_url`, decorated with `evo_cid` and UTMs | Must have a destination                                                     |
| `app` | The matching store for the visitor's platform        | Needs `ios_url` or `android_url`; `destination_url` is the desktop fallback |

App links always use the EVO engine. Web links may instead use the `airbridge` engine, which forwards through an Airbridge tracking link and lets Airbridge own the touchpoint.

## What happens on each device

<Steps>
  <Step title="The hit reaches the redirect app">
    Any non-EVO host pointed at the Dialed frontend is treated as an attribution host and rewritten to `/l/<host>/<path>`. There is no per-domain allowlist to update.
  </Step>

  <Step title="The click is recorded">
    The route posts the domain, slug, query string, referrer, user agent, country, and visitor IP to `POST /api/public/attribution/clicks` with a 4-second timeout. The backend returns the redirect URL, click token, detected platform, link type, and brand name.
  </Step>

  <Step title="The visitor is forwarded">
    Desktop and unknown platforms get a `302`. Phones on an app link get the smart install page instead.
  </Step>
</Steps>

### Desktop or unknown platform

A `302` to the destination URL with the click token and UTMs appended. For an app link with no configured store URL for that platform, the same web fallback is used.

Store pages are the exception: an App Store or Google Play destination is forwarded untouched, because Apple and Google ignore the query string and the attribution happens in the app via the SDK instead.

### Phone on an app link

EVO serves a small "Get *Brand*" page with a single **Get the app** button. There is no auto-redirect, on purpose: the clipboard handoff only works inside a real tap, and it is what turns a probabilistic IP match into a deterministic one.

Tapping the button copies the `evc_…` click token to the clipboard, then opens the store:

* **iOS** — the App Store URL, with `?mt=8` appended when it has no query string.
* **Android** — an `intent://details?id=<package>` URL with `S.browser_fallback_url` set, when the package can be parsed from the Play URL's `id` parameter. Otherwise the plain Play URL.

After 2.5 seconds the page shows "Not opening? Tap the button again."

### TikTok, Instagram, and other in-app browsers

When the user agent looks like an in-app webview (Instagram, Facebook, Messenger, TikTok, Snapchat, Twitter, LinkedIn, Pinterest, Line), the page also renders a hint card telling the visitor how to escape it:

| Browser       | Instruction                                                                     |
| ------------- | ------------------------------------------------------------------------------- |
| TikTok        | Tap the ··· menu in the top right, then **Open in browser**                     |
| Instagram     | Tap the ··· menu in the top right, then **Open in external browser**            |
| Facebook      | Tap the ··· menu, then **Open in Safari** (iOS) or **Open in Chrome** (Android) |
| Anything else | Open this link in your phone's browser                                          |

On iOS the card also offers a direct **Open in Safari** link built from the current page URL.

<Warning>
  Clipboard access inside an in-app webview is unreliable. When the token does not survive, the install still resolves through the creator code or the 24-hour hashed-IP fallback — see [Creator codes](/attribution/creator-codes).
</Warning>

### When the backend is slow or the link is unknown

If the click cannot be recorded, the visitor still lands somewhere real. The route falls back to the per-domain URL in `ATTRIBUTION_FALLBACK_URLS`, and if that is not configured, renders a link-not-found page with a `404`. Attribution is lost; the visitor is not.

## UTM passthrough

Incoming UTMs on the vanity URL are stored on the click and forwarded to web destinations. EVO fills in defaults only for keys the incoming URL did not carry:

| Parameter      | EVO default                                              |
| -------------- | -------------------------------------------------------- |
| `utm_source`   | `evo_creators`                                           |
| `utm_medium`   | The creator's user id, or `brand` for a brand-level link |
| `utm_campaign` | The link slug, or the domain when the slug is empty      |

`utm_source`, `utm_medium`, `utm_campaign`, `utm_content`, and `utm_term` are all recorded on the click row. The rest of the incoming query string is forwarded too — except a replayed `evo_cid`, which is always stripped and replaced with the freshly minted token.

## Shared vs custom domains

|            | Shared        | Custom                     |
| ---------- | ------------- | -------------------------- |
| Owner      | EVO           | One brand                  |
| Usable by  | Every brand   | Only its brand             |
| Slug       | Required      | Optional (root allowed)    |
| Managed by | EVO team only | The brand, from its portal |

Shared domains require a slug precisely so several brands can safely share one host. `tryapp.link/overcomer` and `tryapp.link/dupe` can coexist; a root link on a shared host cannot.

## Connecting a custom domain

From the client portal, **Results → Attribution → Domains**, enter a domain you own. EVO registers it, attaches it to the Vercel project, and returns the exact DNS records to create.

### DNS records

| Domain shape                            | Record                                |
| --------------------------------------- | ------------------------------------- |
| Apex, for example `getdupe.app`         | `A` `@` → `76.76.21.21`               |
| Subdomain, for example `go.getdupe.app` | `CNAME` `go` → `cname.vercel-dns.com` |

Vercel's rank-1 recommendation replaces those defaults when it returns one. An unverified domain also gets a `TXT` verification challenge in its `dns_records` list — create that too.

### Statuses

| Status          | Meaning                                                                                 |
| --------------- | --------------------------------------------------------------------------------------- |
| `pending`       | Registered, but DNS has not been checked yet                                            |
| `misconfigured` | Attached to Vercel, but unverified or not pointed well enough to issue a certificate    |
| `active`        | Verified and configured — links on it are live                                          |
| `unmanaged`     | No Vercel token configured, so only the database record is managed                      |
| `error`         | The last Vercel call failed; `last_error` has the reason, and a re-check can be retried |

Press **Check** to re-run verification after your DNS changes propagate. Removing a domain that still has links returns `domain_has_links` (`409`) with the offending links — delete or move them first.

## For the EVO team

### Registering a shared domain

Shared domains are registered from **Sales Attribution → Domains** in the admin console (`POST /api/internal/attribution-domains` with `kind: "shared"` and no brand). A shared domain must not have a brand; a custom domain must.

The app's own hosts are rejected: anything on `evomarketing.co` or `vercel.app` cannot be registered as an attribution domain.

### Cloudflare automation

For EVO-owned shared domains, the required A, CNAME, and TXT records can be created in Cloudflare automatically once Vercel reports them. After the records are upserted, EVO re-checks Vercel in the same request so a newly verified domain can go `active` immediately.

Custom client domains are **never** changed in Cloudflare. A missing zone or an unset token simply leaves the Vercel status and the manual `dns_records` instructions intact.

### Environment variables

| Variable                    | Purpose                                                                                                                                                       |
| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `VERCEL_TOKEN`              | Bearer token with access to the Vercel team. Without it, domains are stored as `unmanaged` and no Vercel calls are made                                       |
| `VERCEL_PROJECT_ID`         | Frontend project id; defaults to Dialed's production frontend project                                                                                         |
| `VERCEL_TEAM_ID`            | Vercel team id; defaults to EVO's production team                                                                                                             |
| `CLOUDFLARE_API_TOKEN`      | Enables shared-domain DNS automation. Needs `Zone.DNS:Edit` on every EVO-owned zone it may manage, including the `tryapp.link` zone in the Ignited IO account |
| `ATTRIBUTION_FALLBACK_URLS` | JSON map of vanity host → fallback URL, used when a click cannot be recorded                                                                                  |
| `ATTRIBUTION_APP_HOSTS`     | Comma-separated hosts that serve the app itself and must never be treated as attribution hosts                                                                |
