BetterIAM
Server 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.

How a hostname goes live

  1. 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 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 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.

Methods5
Serveriam.api.hostnames
Clientclient.hostnames
HTTPPOST /api/iam/hostnames/*
MethodWhat it doesAccess
addClaims a hostname for the organization and returns the DNS records that verify and route it.Credential
deleteReleases a hostname; a verified one stops resolving to the organization at once.Credential
listLists the organization's claimed hostnames, newest first, with their status and DNS records.Credential
setPrimaryMakes a verified hostname the organization's canonical sign-in address, or goes back to its subdomain.Credential
verifyLooks 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.

POST/api/iam/hostnames/add
client.hostnames.add()Credential

Used inSign-in addresses and regions

  • 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 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' }
Input

Prop

Type

Returns

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>"
}'
Signature
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.

POST/api/iam/hostnames/delete
client.hostnames.delete()Credential
  • 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.

Input

Prop

Type

Returns

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>"
}'
Signature
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.

POST/api/iam/hostnames/list
client.hostnames.list()Credential
  • 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.

Input

Prop

Type

Returns

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>"
}'
Signature
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.

POST/api/iam/hostnames/setPrimary
client.hostnames.setPrimary()Credential

Used inSign-in addresses and regions

  • 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, 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 });
Input

Prop

Type

Returns

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>"
}'
Signature
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.

POST/api/iam/hostnames/verify
client.hostnames.verify()Credential

Used inSign-in addresses and regions

  • 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.

Input

Prop

Type

Returns

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>"
}'
Signature
iam.api.hostnames.verify(
  credential: CredentialInput,
  input: { tenantId: string; hostnameId: string },
): Promise<{ verified: boolean; hostname: PublicHostname }>

Was this page helpful?

Better IAM is created by Sean Filimon

Last updated

On this page