# hostnames (/docs/reference/api/hostnames)

> Custom hostnames let an organization sign in at an address of its own, such as login.acme.com, instead of the subdomain the deployment gives it (acme.signin.ex…



Custom hostnames let an organization sign in at an address of its own, such as `login.acme.com`, instead of the
subdomain the deployment gives it (`acme.signin.example.com`). Enterprise customers ask for this so the sign-in page
sits on their own domain, which their people recognize and their security team can vouch for. This group claims a
hostname, hands back the DNS records that prove control and route traffic, verifies it, makes it the organization's
primary address, and releases it. It needs `hosts.customHostnames: true`; see
[sign-in addresses and regions](/docs/operations/deployment/hosts-and-regions).

## How a hostname goes live [#how-a-hostname-goes-live]

1. [`add`](#add) claims the hostname and returns two DNS records: a TXT record that proves the organization controls
   the name, and (when the deployment sets `hosts.cnameTarget`) a CNAME that sends its traffic to the deployment.
2. The organization publishes both records with its DNS provider.
3. [`verify`](#verify) looks the TXT record up. Once it matches, the hostname belongs to the organization and to no
   other, and requests on it are pinned to the organization, like its subdomain.
4. Optionally, [`setPrimary`](#setprimary) makes it the address sign-in URLs and email links use.

Your TLS termination must also serve a certificate for the hostname. With on-demand certificates (Caddy's `ask`,
Cloudflare for SaaS, and similar), let `iam.hosts.allowed(hostname)` decide: it is true only for verified hostnames
of active organizations. Passkeys are bound to one domain, so they are not offered on a custom hostname outside the
deployment's passkey domain; people sign in there with the other methods.

| Method                      | What it does                                                                                                                     | Access     |
| --------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | ---------- |
| [`add`](#add)               | Claims a hostname for the organization and returns the DNS records that verify and route it.                                     | Credential |
| [`delete`](#delete)         | Releases a hostname; a verified one stops resolving to the organization at once.                                                 | Credential |
| [`list`](#list)             | Lists the organization's claimed hostnames, newest first, with their status and DNS records.                                     | Credential |
| [`setPrimary`](#setprimary) | Makes a verified hostname the organization's canonical sign-in address, or goes back to its subdomain.                           | Credential |
| [`verify`](#verify)         | Looks up the hostname's TXT record and, when it matches, marks the hostname verified so it starts resolving to the organization. | Credential |

## add [#add]

Claims a hostname for the organization and returns the DNS records that verify and route it.

**HTTP:** `POST /api/iam/hostnames/add` (requires a credential) · **Browser client:** `client.hostnames.add()`

* **Permission:** `iam:hostnames:create` on the tenant.
* **Audited as:** `iam:hostnames:create`, resource `hostnames/{hostname}`.
* **Errors:** `FEATURE_DISABLED` unless `hosts.customHostnames` is on; `INVALID_INPUT` for a malformed name;
  `HOSTNAME_NOT_ALLOWED` for a name the deployment uses itself (its base URL, a trusted origin, or its organization
  subdomain space); `CONFLICT` (409) when the organization already claimed it; `HOSTNAME_TAKEN` (409) when another
  organization verified it; `LIMIT_EXCEEDED` past 20 hostnames per organization.

The name is lowercased and a trailing dot removed. The claim stays `pending`, and resolves to nothing, until
[`verify`](#verify) sees the TXT record: `_better-iam-challenge.{hostname}` (the label follows
`domains.recordName`) with the value `better-iam-hostname={token}`.

```ts
const claimed = await iam.api.hostnames.add(credential, { tenantId, hostname: 'login.acme.com' });
// claimed.dnsRecords.verification: { type: 'TXT', name: '_better-iam-challenge.login.acme.com', value: '…' }
// claimed.dnsRecords.routing: { type: 'CNAME', name: 'login.acme.com', value: 'custom.signin.example.com' }
```

```ts title="Signature"
iam.api.hostnames.add(
  credential: CredentialInput,
  input: { tenantId: string; hostname: string },
): Promise<PublicHostname>
```

## delete [#delete]

Releases a hostname; a verified one stops resolving to the organization at once.

**HTTP:** `POST /api/iam/hostnames/delete` (requires a credential) · **Browser client:** `client.hostnames.delete()`

* **Permission:** `iam:hostnames:delete` on the tenant.
* **Audited as:** `iam:hostnames:delete`.
* **Errors:** `NOT_FOUND` when the hostname is not the organization's.

If it was the primary address, sign-in URLs fall back to the organization's subdomain. Remove the DNS records
afterwards, and revoke its TLS certificate if your certificate automation does not.

```ts title="Signature"
iam.api.hostnames.delete(
  credential: CredentialInput,
  input: { tenantId: string; hostnameId: string },
): Promise<{ deleted: boolean }>
```

## list [#list]

Lists the organization's claimed hostnames, newest first, with their status and DNS records.

**HTTP:** `POST /api/iam/hostnames/list` (requires a credential) · **Browser client:** `client.hostnames.list()`

* **Permission:** `iam:hostnames:read` on the tenant.
* **Audited as:** `iam:hostnames:read`.

Each entry shows `status` (`pending` or `verified`), whether it is `primary`, its sign-in `url`, and the records to
publish, so a settings page can show setup instructions until verification succeeds.

```ts title="Signature"
iam.api.hostnames.list(
  credential: CredentialInput,
  input: { tenantId: string },
): Promise<PublicHostname[]>
```

## setPrimary [#setprimary]

Makes a verified hostname the organization's canonical sign-in address, or goes back to its subdomain.

**HTTP:** `POST /api/iam/hostnames/setPrimary` (requires a credential) · **Browser client:** `client.hostnames.setPrimary()`

* **Permission:** `iam:hostnames:update` on the tenant.
* **Audited as:** `iam:hostnames:update`.
* **Errors:** `INVALID_INPUT` when the hostname is not verified yet; `NOT_FOUND` when it is not the organization's.

Sign-in URLs from [`tenants.lookup`](/docs/reference/api/tenants#lookup), `iam.hosts.signInUrl`, and the `signInUrl`
of every email the organization's people receive use the primary hostname. `hostnameId: null` clears it. The result
has the new primary hostname (or `null`) and the organization's `signInUrl`.

```ts
await iam.api.hostnames.setPrimary(credential, { tenantId, hostnameId: claimed.id });
```

```ts title="Signature"
iam.api.hostnames.setPrimary(
  credential: CredentialInput,
  input: { tenantId: string; hostnameId: string | null },
): Promise<{ primary: PublicHostname | null; signInUrl: string | null }>
```

## verify [#verify]

Looks up the hostname's TXT record and, when it matches, marks the hostname verified so it starts resolving to the organization.

**HTTP:** `POST /api/iam/hostnames/verify` (requires a credential) · **Browser client:** `client.hostnames.verify()`

* **Permission:** `iam:hostnames:update` on the tenant.
* **Audited as:** `iam:hostnames:update`.
* **Errors:** `FEATURE_DISABLED` unless custom hostnames are on; `HOSTNAME_TAKEN` (409) when another organization
  verified it first; `HOSTNAME_NOT_ALLOWED` when the deployment's own addresses changed to include it; `NOT_FOUND`
  when it is not the organization's.

DNS is queried before the transaction opens, through `domains.resolveTxt` when you configure one (for example DNS
over HTTPS). While the record is not visible yet, the call succeeds with `verified: false` and records
`lastCheckedAt`, so a settings page can poll it. A verified hostname stays verified; to re-prove control, delete it
and claim it again.

```ts title="Signature"
iam.api.hostnames.verify(
  credential: CredentialInput,
  input: { tenantId: string; hostnameId: string },
): Promise<{ verified: boolean; hostname: PublicHostname }>
```
