domains
Verified email domains let an organization prove it owns a domain such as acme.com, so your sign-in page can find the right tenant from a work email address.
Verified email domains let an organization prove it owns a domain such as acme.com, so your sign-in page can find
the right tenant from a work email address. People do not know tenant IDs, and every sign-in names a tenant, so
"use your work email" needs a trustworthy mapping from domain to organization. This group creates that mapping and
answers the public lookup. It is step one of enterprise onboarding.
How domain verification works
- An administrator claims a domain with
add. The claim starts aspendingand comes with a DNS TXT record to publish: the name_better-iam-challenge.{domain}with the valuebetter-iam-verification={token}. - After publishing the record, the administrator calls
verify. The server looks the record up and, when the value matches, marks the domainverified. - From then on, the public
discovercall maps email addresses at that domain to the organization.
A verified domain belongs to exactly one tenant. Several tenants may hold pending claims to the same domain, but
only the first to verify owns it; the others then fail with DOMAIN_TAKEN. Consumer mailbox providers (Gmail,
Outlook, iCloud, and similar) can never be claimed, because their addresses belong to individuals, not one
organization. Deployment options under domains change the TXT label (recordName), replace the blocked list
(blockedDomains), or inject the DNS resolver (resolveTxt), for example to use DNS over HTTPS.
Domains are normalized before use: lowercase, no trailing dot, and at least two labels with an alphabetic top-level domain. Claims are removed when the tenant is purged.
| Method | What it does | Access |
|---|---|---|
add | Claims a domain for the tenant and returns the TXT record the organization must publish to prove control. | Credential |
delete | Releases a domain claim; a verified domain stops resolving to the tenant immediately. | Credential |
discover | Finds the organization that verified an email address's domain, with the sign-in rules it enforces. | Public |
list | Lists the tenant's claimed domains, newest first, with their status and the TXT record each one needs. | Credential |
verify | Looks up the domain's TXT record and marks the claim verified when the record matches. | Credential |
add
Claims a domain for the tenant and returns the TXT record the organization must publish to prove control.
Used inEnterprise onboarding
- Permission:
iam:domains:createoniam/domains/{domain}. - Audited as:
iam:domains:create. - Errors:
INVALID_INPUTwhen the value is not a domain such asexample.com;DOMAIN_NOT_ALLOWEDfor a shared mailbox provider on the blocked list;CONFLICTwhen this tenant already claimed the domain;DOMAIN_TAKENwhen another tenant has verified it.
The result carries dnsRecord (type, name, value); show it to the administrator exactly as returned. The
claim grants nothing until it is verified.
const claimed = await iam.api.domains.add(credential, { tenantId, domain: 'acme.com' });
// Publish claimed.dnsRecord at the DNS provider, then call domains.verify.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/domains/add" \
-H "Authorization: Bearer $BETTER_IAM_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Better-IAM: 1" \
-d '{
"tenantId": "<tenantId>",
"domain": "<domain>"
}'iam.api.domains.add(
credential: CredentialInput,
input: { tenantId: string; domain: string },
): Promise<{
id: string;
tenantId: string;
domain: string;
status: 'pending' | 'verified';
createdAt: number;
createdBy: string;
verifiedAt: number | undefined;
lastCheckedAt: number | undefined;
dnsRecord: { type: 'TXT'; name: string; value: string };
}>delete
Releases a domain claim; a verified domain stops resolving to the tenant immediately.
- Permission:
iam:domains:deleteoniam/domains/{domain}. - Audited as:
iam:domains:delete. - Errors:
NOT_FOUNDwhen the claim is not in this tenant.
Releasing a verified domain frees it, so another organization can then claim and verify it.
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/domains/delete" \
-H "Authorization: Bearer $BETTER_IAM_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Better-IAM: 1" \
-d '{
"tenantId": "<tenantId>",
"domainId": "<domainId>"
}'iam.api.domains.delete(
credential: CredentialInput,
input: { tenantId: string; domainId: string },
): Promise<{ deleted: boolean }>discover
Finds the organization that verified an email address's domain, with the sign-in rules it enforces.
Used inEnterprise onboarding,OAuth and OIDC sign-in
- Permission: None: public. No credential is needed.
- Errors:
INVALID_INPUTwhenemailhas no@or the domain is malformed;NOT_FOUNDwhen no active organization verified the domain;WRONG_REGION(421) when another region serves the organization.
This is home-realm discovery for a login screen: the person types their email, you call discover, and you send
them to the right tenant with the right method. The answer carries the tenant's tenantId, name, type, alias
(slug, when set), allowedMethods (null means every method the deployment enables), and requireMfa, plus its
home region and signInUrl when organization addresses or regions are configured. Pass either email or
domain. In a multi-region deployment, an organization homed elsewhere answers WRONG_REGION with its sign-in URL
there, so a global sign-in page can send the person on.
Unknown, pending, released, and inactive domains, and tenants under an inactive ancestor, all answer the same
NOT_FOUND, so the call does not reveal which of these applies. The result is public discovery data by design;
apply ingress rate limits as you would for tenants.lookup.
const org = await client.domains.discover({ email: 'alice@acme.com' });
await client.auth.signIn({ tenantId: org.tenantId, email: 'alice@acme.com', password });Prop
Type
A DomainDiscovery 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/domains/discover" \
-H "Content-Type: application/json" \
-H "X-Better-IAM: 1" \
-d '{}'iam.api.domains.discover(
input: { email?: string; domain?: string },
): Promise<DomainDiscovery>list
Lists the tenant's claimed domains, newest first, with their status and the TXT record each one needs.
- Permission:
iam:domains:readoniam/domains/*. - Audited as:
iam:domains:read.
Each entry shows status (pending or verified), when it was verified, and when the DNS record was last
checked (lastCheckedAt), which helps an administrator see whether a failed verification was retried.
Prop
Type
An array of 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/domains/list" \
-H "Authorization: Bearer $BETTER_IAM_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Better-IAM: 1" \
-d '{
"tenantId": "<tenantId>"
}'iam.api.domains.list(
credential: CredentialInput,
input: { tenantId: string },
): Promise<{
id: string;
tenantId: string;
domain: string;
status: 'pending' | 'verified';
createdAt: number;
createdBy: string;
verifiedAt: number | undefined;
lastCheckedAt: number | undefined;
dnsRecord: { type: 'TXT'; name: string; value: string };
}[]>verify
Looks up the domain's TXT record and marks the claim verified when the record matches.
Used inEnterprise onboarding
- Permission:
iam:domains:updateoniam/domains/{domain}. - Audited as:
iam:domains:update. - Errors:
NOT_FOUNDwhen the claim is not in this tenant;DOMAIN_TAKENwhen another tenant verified the domain first.
A missing or not-yet-visible record is not an error: the call returns verified: false, records lastCheckedAt,
and leaves the claim pending, so you can retry after DNS propagates. The DNS lookup runs before the database
transaction opens, and a failed lookup counts as "not found". Verifying a domain that is already verified returns
verified: true without another lookup.
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/domains/verify" \
-H "Authorization: Bearer $BETTER_IAM_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Better-IAM: 1" \
-d '{
"tenantId": "<tenantId>",
"domainId": "<domainId>"
}'iam.api.domains.verify(
credential: CredentialInput,
input: { tenantId: string; domainId: string },
): Promise<{
verified: boolean;
domain: {
id: string;
tenantId: string;
domain: string;
status: 'pending' | 'verified';
createdAt: number;
createdBy: string;
verifiedAt: number | undefined;
lastCheckedAt: number | undefined;
dnsRecord: { type: 'TXT'; name: string; value: string };
};
}>Better IAM is created by Sean Filimon
Last updated