---
title: Custom tracking domains
description: Serve short-link redirects and ingestion from your own subdomain instead of api.retidal.com, via Cloudflare custom hostnames or NS delegation.
---

A custom tracking domain lets [Short links](/docs/attribution/short-links) redirects and
event ingestion resolve under your own subdomain (e.g. `track.yourbrand.com`) instead of
`api.retidal.com` — useful for first-party cookie durability and brand consistency in ad
destination URLs.

<Warning title="This is the tracking domain, not the email sending domain">
  This page covers `GET /api/projects/{projectId}/domains` and `POST
  /api/projects/{projectId}/domains`, the tracking-traffic custom domain. Retidal also
  has a separate email **sending** domain feature (`GET
  /api/projects/{projectId}/email/domains`) for deliverability (SPF/DKIM) — that is a
  different feature with its own onboarding flow, not covered here. Don't point one at
  the other's DNS setup.
</Warning>

## Prerequisites

- Ownership of the subdomain you want to onboard, and (for `ns_delegation` mode) the
  ability to update your registrar's nameserver records for that subdomain.

## Authentication

Every `/api/projects/{projectId}/domains...` endpoint on this page is a **Management API**
call — host `https://retidal.com`, authenticated with the `trackly_session` cookie from a
logged-in console user, not a project API key. The redirect endpoint itself
(`GET https://api.retidal.com/s/{code}`, used to verify a domain in the last step below) is
the separate **Ingestion API** — see [Authentication](/docs/authentication) for the full
two-API split.

## Listing and creating domains

```
GET https://retidal.com/api/projects/{projectId}/domains
POST https://retidal.com/api/projects/{projectId}/domains
```

<ParamField name="hostname" in="body" type="string" required>
  The subdomain to onboard, 3–255 characters (e.g. `track.yourbrand.com`).
</ParamField>
<ParamField name="displayName" in="body" type="string" required>
  A human-readable label shown in the console.
</ParamField>
<ParamField name="onboardingMode" in="body" type="string">
  `cloudflare_custom_hostname` (Cloudflare manages the hostname directly) or
  `ns_delegation` (you delegate the subdomain's nameservers to Retidal's Route53 zone).
</ParamField>

`POST /api/projects/{projectId}/domains` returns `409` if the hostname is already
occupied by another domain record.

Console → Projects → Settings → Domains is where a domain is created and its
provisioning status tracked end to end.

## Provisioning

Which provisioning call you make depends on `onboardingMode`:

```
POST https://retidal.com/api/projects/{projectId}/domains/{id}/cloudflare/provision
```

For `cloudflare_custom_hostname` domains — provisions the Cloudflare-managed custom
hostname. `POST /api/projects/{projectId}/domains/{id}/cloudflare/provision` returns
`400` if called on an `ns_delegation` domain, or if the domain's current state doesn't
allow provisioning yet.

```
POST https://retidal.com/api/projects/{projectId}/domains/{id}/route53/provision
```

For `ns_delegation` domains — provisions the Route53 hosted zone and returns the
nameservers you must point your registrar at. `POST
/api/projects/{projectId}/domains/{id}/route53/provision` returns `400` if called on a
`cloudflare_custom_hostname` domain.

## Refreshing and verifying (optional)

```
POST https://retidal.com/api/projects/{projectId}/domains/{id}/cloudflare/refresh
POST https://retidal.com/api/projects/{projectId}/domains/{id}/route53/verify
```

`cloudflare/refresh` re-checks a Cloudflare-mode domain's hostname status. `route53/verify`
checks whether your registrar's NS delegation has propagated and, once it has, triggers
the one-shot Cloudflare hostname supply that finishes an `ns_delegation` domain's setup.

## Domain detail and deletion (optional)

```
GET https://retidal.com/api/projects/{projectId}/domains/{id}
DELETE https://retidal.com/api/projects/{projectId}/domains/{id}
```

`DELETE /api/projects/{projectId}/domains/{id}` retires the Cloudflare hostname before
soft-deleting the record — it returns `502` if the Cloudflare-side retire call fails, so
a failed delete does not silently leave an orphaned hostname pointed at Retidal.

## What this unlocks

A verified custom domain lets [short links](/docs/attribution/short-links) redirect from
your own host, so ad destination URLs carry your brand instead of `api.retidal.com`. The
redirect sets a host-only cookie on that subdomain, which improves attribution continuity
for repeat visits **on the same device**.

It does not link devices. A custom domain's cookie is scoped to that host and is not
shared with your site; connecting a phone to a laptop still depends entirely on both
calling [identify](/docs/sending-data/identify) with the same `userId` — see
[cross-device attribution](/docs/attribution/cross-device).

## Verify it worked

<Steps>
  <Step title="Confirm domain status">
    Console → Projects → Settings → Domains shows the domain's status directly. Equivalently,
    from a browser session already logged into the console (so the `trackly_session` cookie
    is sent automatically):
    ```bash
    curl -s https://retidal.com/api/projects/{projectId}/domains/{id} \
      -H "Cookie: trackly_session=$SESSION_COOKIE"
    # expect: 200 with the domain's status showing fully provisioned — Cloudflare
    # hostname active, or Route53 delegation verified
    ```
  </Step>
  <Step title="Confirm redirects resolve on the custom hostname">
    ```bash
    curl -s -o /dev/null -D - https://api.retidal.com/s/{code} | grep -i location
    # expect: HTTP/2 302 — repeat the same request against your own provisioned
    # subdomain instead of api.retidal.com and confirm it redirects identically
    # (see Short links)
    ```
  </Step>
</Steps>

## If it doesn't work

- If DNS/TLS provisioning never completes and nothing sent through the domain arrives at
  all, work through [Events not arriving](/docs/troubleshooting/events-not-arriving).
- If you're stuck mid-setup with no single clear symptom yet (nameservers not showing as
  delegated, hostname status stuck pending), work through the
  [health checklist](/docs/troubleshooting/health-checklist).

## Worked example

Onboarding `track.example-brand.com` via NS delegation:

```json title="POST /api/projects/{projectId}/domains"
{
  "hostname": "track.example-brand.com",
  "displayName": "Example Brand — Tracking",
  "onboardingMode": "ns_delegation"
}
```

```json title="POST .../domains/{id}/route53/provision response"
{
  "ok": true,
  "nameServers": [
    "ns-1234.awsdns-12.org",
    "ns-567.awsdns-34.com",
    "ns-890.awsdns-56.net",
    "ns-345.awsdns-78.co.uk"
  ]
}
```

Point `track.example-brand.com`'s nameservers at the four values returned above at your
registrar, then call `route53/verify` to confirm delegation and trigger the final
Cloudflare hostname supply.

## Guardrails

DNS propagation is not instant — calling `route53/verify` before your registrar has
actually applied the nameserver change reads as **stuck provisioning**, not a Retidal
failure; re-check after normal DNS TTL windows (often up to 24–48 hours) before treating
it as broken. Separately, deleting a domain (`DELETE
/api/projects/{projectId}/domains/{id}`) retires the Cloudflare hostname first — if that
retire call fails, the whole delete fails with `502` rather than leaving an
**orphaned hostname** silently pointed at Retidal after the record is gone from your
console.

## Next steps

<CardGroup cols={2}>
  <Card title="Short links" href="/docs/attribution/short-links">
    Redirect through your custom domain once it's provisioned.
  </Card>
  <Card title="Ad platforms" href="/docs/attribution/ad-platforms">
    Keep destination URLs on-brand end to end.
  </Card>
</CardGroup>
