Mailmus
B2B

Verified domains (B2B)

Prove a company owns an email domain, then let the people who have an address there join on their own.

A company can prove it owns an email domain, such as acme.com, by publishing a record in its DNS zone. Once the record is visible, that domain can route enterprise sign-in, and it can let the people who carry an address there into the organization without an invitation.

The proof matters. Without it, anyone could claim a domain they do not own and catch the sign-ins of its employees, or collect them into an organization of their own.

Claim a domain

Add the domain to the organization, from Project → Organizations → the company → Domains, or from your server:

await fetch(
  `https://api.mailmus.app/projects/${projectId}/b2b/organizations/${organizationId}/domains`,
  {
    method: "POST",
    headers: { Authorization: `Bearer ${secretKey}`, "Content-Type": "application/json" },
    body: JSON.stringify({ domain: "acme.com" }),
  },
);
// → { id, domain, status: "PENDING", record: "mailmus-verification=0f8c…", … }

The answer carries record: the exact TXT record to publish at the root of the domain. Give it to whoever looks after the company's DNS zone.

The same domain can be claimed in two different projects, each with its own record. Two suppliers can legitimately serve the same company, and neither inherits a proof the other made.

Check the record

Mailmus looks the record up when you ask, and once a day after that:

// POST /projects/{projectId}/b2b/organizations/{organizationId}/domains/{domainId}/verify
// → { status: "VERIFIED", verifiedAt: "…", … }  or  { status: "FAILED", … }

DNS changes take minutes to spread, so a first check often comes back FAILED. Ask again once the zone has settled.

verifiedAt keeps the first success and is never cleared, even if the record later disappears. It tells a domain that was never honoured apart from one that was really used and whose zone broke since.

The proof can be lost

A zone that breaks, a record removed during a migration, a company that lets a domain go: the daily check notices, the domain goes back to FAILED, and the fact reaches your server as organization.domain_verification_lost. A domain that cannot be resolved at all is not treated as a lost proof, since an unanswered lookup is not an answer.

What a verified domain gives you

Enterprise sign-in. A connection to a company's identity provider can only be turned on for a domain that has been proved. This is checked every time the connection is created, changed, or activated.

Automatic joining, described below.

Automatic joining

Once a domain is proved, you can decide that an address at that domain is enough to belong to the organization. Someone signing in with sophie@acme.com becomes a member of Acme, with no invitation.

This is off by default, and set per domain. Proving that a company owns a domain and wanting every holder of an address there to walk in are two different decisions. A company may have proved acme.com and acme-contractors.com, and want the door open only on the first.

// PATCH /projects/{projectId}/b2b/organizations/{organizationId}/domains/{domainId}
// { "autoJoinEnabled": true }

The domain must be verified before you can turn this on. Turning it off never requires that: you can always close the door, including on a domain whose zone has just broken. People already in stay where they are; only new arrivals stop.

Letting someone in

Nothing happens on its own: you ask, right after you sign someone in. The call is safe to repeat, so you can put it on every sign-in, and yesterday's call that failed catches up by itself.

From your server, with your secret key. This works whether or not you use Mailmus Auth:

// POST /projects/{projectId}/b2b/domain-join
// { "customerId": "cus_01HZY3M4K5" }
// → { joined: true, organizationId: "org_…", domain: "acme.com", reason: "JOINED" }

From your application, with the person's own access token, when you sign people in with Mailmus Auth:

// POST /projects/{projectId}/auth/organizations/domain-join
// → the same answer, for the person the token belongs to

reason says what happened, so a call that does nothing is never mistaken for a call that failed:

reasonWhat it means
JOINEDAdded just now.
ALREADY_MEMBERNothing to do, they were already in.
NO_EMAIL_DOMAINThe address carries no usable domain.
NO_MATCHING_DOMAINNo organization in this project has proved that domain with automatic joining on.

The membership records how the person arrived, in source: DOMAIN. That is deliberately not SSO, where an identity provider signs an assertion about them. Here nobody proved anything about the person; the domain of their address decided.

Exact matching

The comparison is exact. acme.com covers sophie@acme.com and nothing else: not sophie@mail.acme.com, and certainly not sophie@notacme.com. A subdomain that should open the door is claimed and proved separately.

Removing a domain

// DELETE /projects/{projectId}/b2b/organizations/{organizationId}/domains/{domainId}

The company loses its proof, and nobody is let in by that domain any more. A domain still used by an enterprise sign-in connection is refused rather than quietly breaking it: delete the connection first.

What lands on the history

Every one of these moments is kept on the organization's history and reaches your server as a webhook: organization.domain_added, organization.domain_verified, organization.domain_verification_lost, organization.domain_removed, and organization.domain_auto_join_updated. Each line says who acted, including when it was the daily check rather than a person.

Next

  • Organizations — members, roles, invitations, and the rest of the company account.
  • API Reference — every field of the endpoints above.

On this page