# Domains

## Domain Model

### Fields

- **`object`** `"domain"`

- **`id`** `string`

   Unique identifier, prefixed with `dom_`.

- **`team_id`** `string`

- **`domain`** `string`

   The hostname the customer added, stored lowercase.

- **`ownership_type`** [`DnsRecordType`](/api/domains#dns-record-type)

   The record proving the customer controls this hostname. Always a TXT at
   `_tailglow-verify.{domain}`, required even for a domain with no purpose, which needs nothing
   else. Verification is per-hostname: it proves nothing about the parent domain or a sibling.

- **`ownership_name`** `string`

- **`ownership_value`** `string`

- **`ownership_status`** [`DnsRecordStatus`](/api/domains#dns-record-status)

   What the last lookup found. `unchecked` means no lookup has run yet, which is different from
   `missing` (looked up, definitively not there).

- **`ownership_actual`** `string | null`

   What DNS actually returned. Null when the record is absent or has not been looked up.

- **`ownership_checked_at`** [`ISODateString | null`](/api/domains#iso-date-string)

- **`ownership_verified`** `boolean`

   Whether Tailglow currently treats the ownership record as good. True while it resolves, and it
   stays true for 24 hours after it stops so a brief DNS problem does not unverify a working
   domain.

- **`ownership_failed_at`** [`ISODateString | null`](/api/domains#iso-date-string)

   When the ownership record had been missing long enough that Tailglow stops serving the domain.
   Null while ownership holds, including during the grace period that follows a failed check.

- **`routing_type`** [`DnsRecordType | null`](/api/domains#dns-record-type)

   The record routing traffic to Tailglow, or null while the domain has no purpose and so asks for
   no record. Always a CNAME at the hostname itself.

- **`routing_name`** `string | null`

- **`routing_value`** `string | null`

- **`routing_status`** [`DnsRecordStatus`](/api/domains#dns-record-status)

   What the last lookup found. A routing record whose target has since moved reads as `unchecked`:
   the stored answer described the old target, so it says nothing about the current one.

- **`routing_actual`** `string | null`

- **`routing_checked_at`** [`ISODateString | null`](/api/domains#iso-date-string)

- **`routing_verified`** `boolean`

   Whether Tailglow currently treats the routing record as good. True while it resolves, and it
   stays true for seven days after it stops so a DNS change in progress does not take a live
   domain out of service.

- **`certificate_status`** [`CertificateStatus`](/api/domains#certificate-status)

   Progress of the TLS certificate Tailglow provisions once routing is verified.

- **`certificate_error`** `string | null`

   Why the last certificate attempt failed; null unless `certificate_status` is `failed`.

- **`certificate_provisioned_at`** [`ISODateString | null`](/api/domains#iso-date-string)

   When the current certificate was issued.

- **`purpose`** [`DomainTrafficPurpose | null`](/api/domains#domain-traffic-purpose)

   What this domain serves, or null when it serves nothing yet. A domain serves ingest traffic or
   status pages, never both.

- **`is_sso_enabled`** `boolean`

   Whether members with an email at this domain sign in through SSO. Adds no DNS record.

- **`lifecycle_action`** [`DomainLifecycleAction | null`](/api/domains#domain-lifecycle-action)

   Infrastructure work Tailglow is currently doing for this domain; null when idle.

- **`lifecycle_status`** [`DomainLifecycleStatus | null`](/api/domains#domain-lifecycle-status)

   How that work is progressing.

- **`lifecycle_error`** `string | null`

   Why the last attempt failed. Tailglow retries automatically.

- **`last_checked_at`** [`ISODateString | null`](/api/domains#iso-date-string)

   When this domain was last checked. A domain can be checked once every 30 seconds; per-record
   timing lives on `ownership_checked_at` and `routing_checked_at`.

- **`created_at`** [`ISODateString`](/api/domains#iso-date-string)

- **`updated_at`** [`ISODateString`](/api/domains#iso-date-string)

- **`updated_by`** `string | null`

   The user who last changed this domain.

### Referenced Types

#### ISODateString

`ISODateString`

An ISO 8601 date-time string returned at the JSON API boundary.

#### DnsRecordType

`"txt" | "cname"`

#### DnsRecordStatus

`"unchecked" | "valid" | "invalid" | "missing" | "unavailable"`

#### CertificateStatus

`"none" | "provisioning" | "active" | "failed"`

#### DomainTrafficPurpose

`"ingest" | "pages"`

What a domain serves. Stored on the domain.

#### DomainLifecycleAction

`"attach" | "reconcile" | "disable_ingest" | "disable_pages" | "delete_domain" | "delete_team"`

#### DomainLifecycleStatus

`"pending" | "processing" | "failed"`

## List Domains

### Endpoint

Retrieve a list of custom domains for the current team.

```http
GET /v1/domains
```

**Scope:** `domains:read`

### Query Parameters

- **`order_by`** `string`
  Field used to order the domains. Optional. Defaults to `"created_at"`. Allowed values: `"created_at"`, `"domain"`, `"certificate_status"`.

- **`ownership_verified`** `string`
  Filter domains by ownership verification state. Optional. Allowed values: `"true"`, `"false"`.

- **`certificate_status`** [`CertificateStatus`](/api/domains#certificate-status)
  Filter domains by the status of their TLS certificate. Optional.

- **`limit`** `number`
  Maximum number of items to return. Optional. Defaults to `25`. Minimum: `1`. Maximum: `200`.

- **`after`** `string`
  Cursor from `pagination.next_cursor` of a previous response. Returns the resources after that page. Optional.

- **`before`** `string`
  Cursor from `pagination.prev_cursor` of a previous response. Returns the resources before that page. Optional.

- **`sort`** `string`
  Sort direction for the result set. Optional. Defaults to `"desc"`. Allowed values: `"asc"`, `"desc"`.

### Response

```ts
{
  message: string;
  data: Domain[];
  status: 200;
  error: null;
  pagination: Pagination;
  endpoint: string;
}
```

### Comments

- `ownership_verified` accepts the strings `true` and `false`. Any other value is rejected.
- `after` and `before` are mutually exclusive.

## Retrieve Domain

### Endpoint

Retrieve a single custom domain, including the DNS records needed to verify and route it.

```http
GET /v1/domains/:domain_id
```

**Scope:** `domains:read`

### Path Parameters

- **`domain_id`** `string` -- **Required**
  Unique identifier of the domain.

### Response

Domain retrieved

```ts
{
  message: string;
  data: Domain;
  status: 200;
  error: null;
  pagination: null;
  endpoint: string;
}
```

## Create Domain

### Endpoint

Add a custom domain to the current team.

```http
POST /v1/domains
```

**Scope:** `domains:write`

### Request Body

- **`domain`** `string` -- **Required**
  The custom domain to add, for example `analytics.example.com`. Stored in lowercase. Minimum length: `1`. Maximum length: `255`.

- **`purpose`** [`DomainTrafficPurpose`](/api/domains#domain-traffic-purpose)
  Traffic purpose to configure for this domain. Use `ingest` to receive analytics data, or `pages` to serve status pages. Leave it out to add the domain without a purpose and choose one later. Optional.

### Response

```ts
{
  message: string;
  data: Domain;
  status: 201;
  error: null;
  pagination: null;
  endpoint: string;
}
```

### Comments

- The number of custom domains a team can add is limited by its billing plan.
- A hostname can only be registered once. Adding one that already exists returns a conflict.
- Some hostnames are reserved and cannot be added.
- `domain` cannot contain a wildcard (`*`). Add each hostname you want to serve as its own domain.
- `domain` must be a valid hostname such as `analytics.example.com`. Labels may contain letters, digits and hyphens, cannot start or end with a hyphen, and the top level domain must be at least two letters.

## Check Domain DNS

### Endpoint

Look up every DNS record this domain needs and report what is currently there.

```http
POST /v1/domains/:domain_id/check
```

**Scope:** `domains:write`

### Path Parameters

- **`domain_id`** `string` -- **Required**
  Unique identifier of the domain.

### Response

```ts
{
  message: string;
  data: Domain;
  status: 200;
  error: null;
  pagination: null;
  endpoint: string;
}
```

### Comments

- A check that completes always returns 200. A record that does not resolve is a result, not an error, so read the outcome from `ownership_status` and `routing_status` rather than the status code.
- Ownership is checked first. Routing is only checked once ownership has been verified and the domain has a traffic purpose enabled.
- A record that has never been looked up reports `unchecked`, which is not the same as `missing`.
- A domain can be checked at most once every 30 seconds.

## Update Domain

### Endpoint

Update a domain's traffic purpose or SSO assignment.

```http
POST /v1/domains/:domain_id
```

**Scope:** `domains:write`

### Path Parameters

- **`domain_id`** `string` -- **Required**
  Unique identifier of the domain.

### Request Body

- **`purpose`** [`DomainTrafficPurpose`](/api/domains#domain-traffic-purpose)
  What this domain serves. Ownership must be verified before a purpose can be set, and `null` takes the domain out of service. Omit to leave it unchanged. Optional.

- **`is_sso_enabled`** `boolean`
  Whether SSO is assigned to this domain. Optional.

### Response

```ts
{
  message: string;
  data: Domain;
  status: 200;
  error: null;
  pagination: null;
  endpoint: string;
}
```

### Comments

- Domain ownership must be verified before a purpose can be set.
- A domain serves either ingest traffic or pages. Send `purpose: null` to take it out of service before setting the other, rather than switching between them directly.
- Setting `purpose` to null queues infrastructure teardown. The domain keeps reporting its old purpose until that work completes.
- Only one lifecycle operation runs at a time. If another is in progress, retry shortly.

## Delete Domain

### Endpoint

Delete a custom domain from the current team.

```http
DELETE /v1/domains/:domain_id
```

**Scope:** `domains:delete`

### Path Parameters

- **`domain_id`** `string` -- **Required**
  Unique identifier of the domain.

### Response

```ts
{
  message: string;
  data: null;
  status: 202;
  error: null;
  pagination: null;
  endpoint: string;
}
```

### Comments

- Deletion runs in the background. The domain remains listed with a pending lifecycle state until its edge configuration has been removed.

