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.
How a hostname goes live
addclaims the hostname and returns two DNS records: a TXT record that proves the organization controls the name, and (when the deployment setshosts.cnameTarget) a CNAME that sends its traffic to the deployment.- The organization publishes both records with its DNS provider.
verifylooks 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.- Optionally,
setPrimarymakes 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 | Claims a hostname for the organization and returns the DNS records that verify and route it. | Credential |
delete | Releases a hostname; a verified one stops resolving to the organization at once. | Credential |
list | Lists the organization's claimed hostnames, newest first, with their status and DNS records. | Credential |
setPrimary | Makes a verified hostname the organization's canonical sign-in address, or goes back to its subdomain. | Credential |
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
Claims a hostname for the organization and returns the DNS records that verify and route it.
Used inSign-in addresses and regions
- Permission:
iam:hostnames:createon the tenant. - Audited as:
iam:hostnames:create, resourcehostnames/{hostname}. - Errors:
FEATURE_DISABLEDunlesshosts.customHostnamesis on;INVALID_INPUTfor a malformed name;HOSTNAME_NOT_ALLOWEDfor 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_EXCEEDEDpast 20 hostnames per organization.
The name is lowercased and a trailing dot removed. The claim stays pending, and resolves to nothing, until
verify sees the TXT record: _better-iam-challenge.{hostname} (the label follows
domains.recordName) with the value better-iam-hostname={token}.
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' }Prop
Type
A PublicHostname object:
Prop
Type
Example HTTP request
Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.
curl -X POST "$IAM_URL/api/iam/hostnames/add" \
-H "Authorization: Bearer $BETTER_IAM_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Better-IAM: 1" \
-d '{
"tenantId": "<tenantId>",
"hostname": "<hostname>"
}'iam.api.hostnames.add(
credential: CredentialInput,
input: { tenantId: string; hostname: string },
): Promise<PublicHostname>delete
Releases a hostname; a verified one stops resolving to the organization at once.
- Permission:
iam:hostnames:deleteon the tenant. - Audited as:
iam:hostnames:delete. - Errors:
NOT_FOUNDwhen 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.
Prop
Type
Prop
Type
Example HTTP request
Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.
curl -X POST "$IAM_URL/api/iam/hostnames/delete" \
-H "Authorization: Bearer $BETTER_IAM_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Better-IAM: 1" \
-d '{
"tenantId": "<tenantId>",
"hostnameId": "<hostnameId>"
}'iam.api.hostnames.delete(
credential: CredentialInput,
input: { tenantId: string; hostnameId: string },
): Promise<{ deleted: boolean }>list
Lists the organization's claimed hostnames, newest first, with their status and DNS records.
- Permission:
iam:hostnames:readon 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.
Prop
Type
An array of PublicHostname.
Prop
Type
Example HTTP request
Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.
curl -X POST "$IAM_URL/api/iam/hostnames/list" \
-H "Authorization: Bearer $BETTER_IAM_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Better-IAM: 1" \
-d '{
"tenantId": "<tenantId>"
}'iam.api.hostnames.list(
credential: CredentialInput,
input: { tenantId: string },
): Promise<PublicHostname[]>setPrimary
Makes a verified hostname the organization's canonical sign-in address, or goes back to its subdomain.
Used inSign-in addresses and regions
- Permission:
iam:hostnames:updateon the tenant. - Audited as:
iam:hostnames:update. - Errors:
INVALID_INPUTwhen the hostname is not verified yet;NOT_FOUNDwhen it is not the organization's.
Sign-in URLs from 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.
await iam.api.hostnames.setPrimary(credential, { tenantId, hostnameId: claimed.id });Prop
Type
Prop
Type
Example HTTP request
Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.
curl -X POST "$IAM_URL/api/iam/hostnames/setPrimary" \
-H "Authorization: Bearer $BETTER_IAM_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Better-IAM: 1" \
-d '{
"tenantId": "<tenantId>",
"hostnameId": "<hostnameId>"
}'iam.api.hostnames.setPrimary(
credential: CredentialInput,
input: { tenantId: string; hostnameId: string | null },
): Promise<{ primary: PublicHostname | null; signInUrl: string | null }>verify
Looks up the hostname's TXT record and, when it matches, marks the hostname verified so it starts resolving to the organization.
Used inSign-in addresses and regions
- Permission:
iam:hostnames:updateon the tenant. - Audited as:
iam:hostnames:update. - Errors:
FEATURE_DISABLEDunless custom hostnames are on;HOSTNAME_TAKEN(409) when another organization verified it first;HOSTNAME_NOT_ALLOWEDwhen the deployment's own addresses changed to include it;NOT_FOUNDwhen 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.
Prop
Type
Prop
Type
Example HTTP request
Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.
curl -X POST "$IAM_URL/api/iam/hostnames/verify" \
-H "Authorization: Bearer $BETTER_IAM_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Better-IAM: 1" \
-d '{
"tenantId": "<tenantId>",
"hostnameId": "<hostnameId>"
}'iam.api.hostnames.verify(
credential: CredentialInput,
input: { tenantId: string; hostnameId: string },
): Promise<{ verified: boolean; hostname: PublicHostname }>Better IAM is created by Sean Filimon
Last updated