Organizations (B2B)
Self-service team accounts for your Customers — roles, invitations, active context.
Organizations lets your Customers belong to organizations (companies, teams), with their own members and roles. A Customer can belong to several organizations; one is active at a time, per session.
Off by default. Turn it on from Project → Organizations, or from your server with PATCH /projects/{projectId}/b2b/settings and your secret key. Organizations work without Mailmus Auth: members can be identified by your own user IDs.
Roles
Three built-in roles: org:owner, org:admin, org:member. Whoever creates an organization becomes its org:owner, and an organization always keeps at least one.
A member can have several roles, including roles you create for your own application, such as billing. See Roles and permissions to create them and to check what a member can do.
Your own identifiers
Give each organization the identifier your system already uses for that company, such as your customer ID, and find it again with it. Add members with your own user IDs: Mailmus never asks them for a password or a Mailmus account.
await fetch(`https://api.mailmus.app/projects/${projectId}/b2b/organizations`, {
method: "POST",
headers: { Authorization: `Bearer ${secretKey}`, "Content-Type": "application/json" },
body: JSON.stringify({ name: "Acme Inc.", externalId: "cus_8291" }),
});
// GET /projects/{projectId}/b2b/organizations?externalId=cus_8291
// POST /projects/{projectId}/b2b/organizations/{id}/members
// { "externalUserId": "user_8291", "email": "sophie@acme.com", "roles": ["org:admin"] }Each membership tells you how the person joined, in source: API when your server or the dashboard added them, INVITATION when they accepted an invitation, SSO when they signed in through the company's single sign-on, and DOMAIN when the company had proved the domain of their email address and left the door open. See Verified domains.
Suspending an organization
Set status to SUSPENDED to pause a company, for instance after a failed payment. Its members and history are kept, but it takes no new members, no invitation is sent or accepted, and nobody signs in through its single sign-on or switches to it in a session. Set it back to ACTIVE to resume. Both changes appear on the organization's history and reach your server as organization.suspended and organization.reactivated.
Invitations to your own application
By default, the link in an invitation email opens the Mailmus invitation page, where the person signs in with Mailmus Auth. If you sign people in yourself, send the link to your application instead: set invitationUrl in the organization settings (Project → Organizations, or PATCH /projects/{projectId}/b2b/settings), with {token} where the token goes.
https://app.example.com/invite?token={token}When the invited person arrives, sign them in your way, then accept the invitation from your server:
// POST /projects/{projectId}/b2b/invitations/accept
// { "token": "<token from the link>", "externalUserId": "user_9001" }
// → the membership, with the role the invitation carriedAn invitation only becomes the membership of the person it was sent to: if that person is already known under a different email address, the request is refused.
Moving to Mailmus Auth later
If you added members with your own user IDs and later let people sign in with Mailmus Auth, the same person can exist twice: once as your member, once as a Mailmus Auth customer. Link them from your server:
// POST /projects/{projectId}/customers/link
// { "customerId": "<Mailmus Auth customer>", "externalUserId": "user_8291" }
// → { "customer": { ... }, "mergedCustomerId": "<the member that was merged>" }The member's organizations, roles and history move to the Mailmus Auth customer, who keeps signing in as before. In an organization where both were members, they keep one membership with the roles of both. Mailmus never links two accounts on its own from an email address alone, and a member who already signs in with Mailmus cannot be merged this way.
Self-service (browser SDK)
const orgs = await auth.organizations.mine();
// OrganizationMembershipSummary[] — { id, name, roles, active }
await auth.organizations.setActive(organizationId);
// Forces a session refresh: session.getState().organizationId reflects the
// change as soon as the promise resolves (otherwise the token claims stay
// stale until the next natural refresh).
await auth.organizations.clearActive();
await auth.organizations.leave(organizationId);Invitations
// Preview an invitation without being signed in — useful to show
// "You have been invited to Acme" before the sign-up itself.
const preview = await auth.organizations.previewInvitation(token);
// Accepting — requires an active session.
const membership = await auth.organizations.acceptInvitation(token);The active context (the orgId and orgRoles claims) is included in the token — if your backend verifies JWTs locally, you get it with no extra network call.
Administration (server SDK)
Create organizations, and manage members and invitations from your backend with the mailmus package and your secret key:
import { SDK } from "mailmus";
const mailmus = new SDK({ bearer: process.env.MAILMUS_SECRET_KEY });
await mailmus.organizations.organizationsCreate({ projectId, name: "Acme Inc." });
await mailmus.organizations.organizationsUpdate({ projectId, organizationId, name: "Acme Corp" });
await mailmus.organizationMembers.organizationMembersRemove({ projectId, organizationId, customerId });
await mailmus.organizationInvitations.organizationInvitationsList({ projectId, organizationId });What happened to this organization
Every change to an organization is kept: members added or removed, roles changed, invitations sent and accepted, enterprise sign-ins, SSO connections configured. Each line says who acted, and who it was about.
Invitation emails are followed after they leave: the history shows whether
each one was delivered (organization.invitation_delivered), could not be
delivered (organization.invitation_bounced), or was marked as spam
(organization.invitation_complained). When a customer says they never
received the invitation, you can see why.
const { data, nextCursor } = await mailmus.b2b.events.list({
projectId: "proj_xxx",
id: organizationId,
});It is the answer to the question you get when a customer calls: why does Sophie no longer have access? The same facts reach your server as webhooks; this is what is still readable six months later, in your dashboard or through the API. Kept for 24 months.
SSO/SAML
For organizations that require signing in through their own identity provider (Okta, Azure AD, and others), see the API Reference — configured per organization through dedicated SSO connections, with Home Realm Discovery (auth.sso.start()) triggered automatically when the user's email matches a configured domain.
A connection can only be turned on for a domain the company has proved it owns. Claim and verify it first: see Verified domains.
With your own sign-in
If you sign people in yourself, a company's identity provider can still confirm who they are. Mailmus checks the identity provider's answer and gives your server the profile; you open the session in your application.
- Add your callback address to SSO return addresses (Project → Organizations, or
ssoRedirectUrlsinPATCH /projects/{projectId}/b2b/settings). - From your application, start the sign-in with your publishable key, and send the browser to
redirectUrl:
// POST /projects/{projectId}/b2b/sso/start
// { "email": "sophie@acme.com", "redirectUri": "https://app.example.com/sso/callback", "nonce": "…" }
// → { "ssoRequired": true, "redirectUrl": "https://acme.okta.com/…" }- The person comes back to your address with
?code=…&state=…. Check thatstateis yournonce, then exchange the code from your server, with your secret key:
// POST /projects/{projectId}/b2b/sso/profile
// { "code": "…", "redirectUri": "https://app.example.com/sso/callback" }
// → { organizationId, connectionId, customerId, email, idpSubject, attributes }- Sign the person in your way. They are now a member of the organization, and the sign-in is on its history as
organization.sso_profile_exchanged.
The code works once, for 5 minutes, and only with the address the sign-in started with. No Mailmus session is created.
Next
- Sessions — signed-in devices, and
organizationIdin the tokens. - API Reference — the
organizations/*endpoints.