# domains (/docs/reference/api/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](/docs/federation/enterprise-onboarding).

## How domain verification works [#how-domain-verification-works]

1. An administrator claims a domain with [`add`](#add). The claim starts as `pending` and comes with a DNS TXT
   record to publish: the name `_better-iam-challenge.{domain}` with the value `better-iam-verification={token}`.
2. After publishing the record, the administrator calls [`verify`](#verify). The server looks the record up and,
   when the value matches, marks the domain `verified`.
3. From then on, the public [`discover`](#discover) call 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`](#add)           | Claims a domain for the tenant and returns the TXT record the organization must publish to prove control. | Credential |
| [`delete`](#delete)     | Releases a domain claim; a verified domain stops resolving to the tenant immediately.                     | Credential |
| [`discover`](#discover) | Finds the organization that verified an email address's domain, with the sign-in rules it enforces.       | Public     |
| [`list`](#list)         | Lists the tenant's claimed domains, newest first, with their status and the TXT record each one needs.    | Credential |
| [`verify`](#verify)     | Looks up the domain's TXT record and marks the claim verified when the record matches.                    | Credential |

## add [#add]

Claims a domain for the tenant and returns the TXT record the organization must publish to prove control.

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

* **Permission:** `iam:domains:create` on `iam/domains/{domain}`.
* **Audited as:** `iam:domains:create`.
* **Errors:** `INVALID_INPUT` when the value is not a domain such as `example.com`; `DOMAIN_NOT_ALLOWED` for a
  shared mailbox provider on the blocked list; `CONFLICT` when this tenant already claimed the domain;
  `DOMAIN_TAKEN` when 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.

```ts
const claimed = await iam.api.domains.add(credential, { tenantId, domain: 'acme.com' });
// Publish claimed.dnsRecord at the DNS provider, then call domains.verify.
```

```ts title="Signature"
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 [#delete]

Releases a domain claim; a verified domain stops resolving to the tenant immediately.

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

* **Permission:** `iam:domains:delete` on `iam/domains/{domain}`.
* **Audited as:** `iam:domains:delete`.
* **Errors:** `NOT_FOUND` when the claim is not in this tenant.

Releasing a verified domain frees it, so another organization can then claim and verify it.

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

## discover [#discover]

Finds the organization that verified an email address's domain, with the sign-in rules it enforces.

**HTTP:** `POST /api/iam/domains/discover` (no credential) · **Browser client:** `client.domains.discover()`

* **Permission:** None: public. No credential is needed.
* **Errors:** `INVALID_INPUT` when `email` has no `@` or the domain is malformed; `NOT_FOUND` when 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`](/docs/reference/api/tenants#lookup).

```ts
const org = await client.domains.discover({ email: 'alice@acme.com' });
await client.auth.signIn({ tenantId: org.tenantId, email: 'alice@acme.com', password });
```

```ts title="Signature"
iam.api.domains.discover(
  input: { email?: string; domain?: string },
): Promise<DomainDiscovery>
```

## list [#list]

Lists the tenant's claimed domains, newest first, with their status and the TXT record each one needs.

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

* **Permission:** `iam:domains:read` on `iam/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.

```ts title="Signature"
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 [#verify]

Looks up the domain's TXT record and marks the claim verified when the record matches.

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

* **Permission:** `iam:domains:update` on `iam/domains/{domain}`.
* **Audited as:** `iam:domains:update`.
* **Errors:** `NOT_FOUND` when the claim is not in this tenant; `DOMAIN_TAKEN` when 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.

```ts title="Signature"
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 };
  };
}>
```
