Inbound mail & inboxes (BYOD)
Bring Your Own Domain for receiving mail. Claim a domain you own, add a receiving (inbound) capability, point its MX at That's My Mail, then create inboxes on it. An inbox user signs in to the webmail app to read and send.
Inbound is one capability of a claimed domain (see the API reference → Domains). A domain is claimed by a project, platform-wide unique (one owner across the whole platform), apex first. All endpoints require a session with access to the project (any project member) or the platform god-key.
Receiving vs sending. Receiving needs an MX record (the domain is already proven via the claim's ownership TXT). Sending from the same domain is a separate outbound capability that adds a DKIM record. A domain can have either or both.
The flow
- Claim the domain → publish the ownership TXT it returns → verify.
- Add inbound to the claim → publish the MX record → verify inbound.
- Create inboxes (e.g.
info@your-domain.com). The initial password is generated for you and shown until the user's first login. - The inbox user signs in at the webmail app and is forced to change it.
Endpoints
All paths are under the customer API host, scoped to a project. Auth = a session
token for a project member, or the god-key. :projectId is the project's id;
:domainId is the claimed domain's id. See the API reference for the full
table; the essentials:
Claim + verify a domain
POST /console/v1/projects/:projectId/domains { "domain": "acme.com" }
POST /console/v1/projects/:projectId/domains/:domainId/verify
Add + verify receiving (inbound / MX)
You can add receiving on the apex or any subdomain (optional subdomain body
field) — as many as you like under the one claim. Each capability has its own
capId, returned in the claim's inbound[] array.
POST /console/v1/projects/:projectId/domains/:domainId/inbound { "subdomain": "mail" }
POST /console/v1/projects/:projectId/domains/:domainId/inbound/:capId/verify
Inboxes (under one receiving capability)
POST /console/v1/projects/:projectId/domains/:domainId/inbound/:capId/inboxes { "localpart": "info", "type": "individual" }
GET /console/v1/projects/:projectId/domains/:domainId/inbound/:capId/inboxes
DELETE /console/v1/projects/:projectId/domains/:domainId/inbound/:capId/inboxes/:inboxId
POST …/inboxes returns { inbox, initialPassword } — the password is generated
server-side and returned once; it also stays viewable via GET …/inboxes
(initialPassword field) until the user first logs in, then becomes null.
type is individual or group (group fan-out behavior is a later iteration).
Inbox storage
Each inbox includes 1 GB free. Every GET …/inboxes item carries its
storageUsedBytes + storageLimitBytes. Upgrade an inbox in whole-GB steps:
POST /console/v1/projects/:projectId/domains/:domainId/inbound/:capId/inboxes/:inboxId/storage
{ "limitGb": 5 }
Returns { storage: { usedBytes, limitBytes } }. Storage above the free 1 GB
bills €0.50/GB/month (rolled into the org's inboxUsage.storageBilledCents).
When an inbox is full, incoming mail is rejected (552 mailbox is full)
until you raise the limit — so upgrading is how you make room.
Import existing mail (Mail Transfer)
Migrating from another provider? Push the old messages into an inbox — they appear in the webmail like any other mail (folders, read/unread, original dates preserved).
POST /console/v1/projects/:projectId/domains/:domainId/inbound/:capId/inboxes/:inboxId/import
{
"messages": [
{ "raw": "<base64 RFC822/MIME>", "folder": "inbox", "read": true, "date": "2021-06-01T10:00:00Z" },
{ "raw": "<base64 RFC822/MIME>", "folder": "sent" }
]
}
Each raw is a base64-encoded .eml (full MIME). Per message you may override
folder (inbox|sent|drafts|trash, default inbox), read, and date
(else the message's own Date: header is used). Up to 200 messages per call —
send a big mailbox in several calls. Response is per-batch counts:
{ "total": 2, "imported": 2, "skipped": 0, "failed": 0 }
Idempotent: re-sending the same messages imports 0 new — dedup is by the
message's Message-ID (or a content hash when it has none), so an agent can safely
retry. skipped = already present; failed = couldn't be parsed.
Today's import is push (you upload the MIME — get it from the old provider via IMAP/export yourself). A future iteration will let TMM pull straight from the old mailbox over IMAP, on the same import core.
SDK
const tmm = new TmmClient({ baseUrl, sessionToken });
// Claim → verify → add inbound → verify → create an inbox
const claim = await tmm.console.projects.domains.claim(projectId, { domain: "acme.com" });
// publish claim.dnsRecords.ownership, then:
await tmm.console.projects.domains.verify(projectId, claim.id);
const updated = await tmm.console.projects.domains.addInbound(projectId, claim.id, { subdomain: "mail" });
const cap = updated.inbound.find((i) => i.domain === "mail.acme.com")!;
// publish cap.dnsRecords.mx, then verify THAT capability by its id:
await tmm.console.projects.domains.verifyInbound(projectId, claim.id, cap.id);
const { inbox, initialPassword } = await tmm.console.projects.domains.inboxes.create(
projectId,
claim.id,
cap.id,
{ localpart: "info", type: "individual" },
);
// hand `inbox.localpart@acme.com` + initialPassword to the user; they change it on first login.
// Migrate existing mail into the new inbox (idempotent — safe to retry)
const oldMail = [{ raw: emlBase64, folder: "inbox", read: true }];
const r = await tmm.console.projects.domains.inboxes.import(projectId, claim.id, cap.id, inbox.id, {
messages: oldMail,
});
// r = { total, imported, skipped, failed }
See also: the API reference (the full Domains table — claims, sending, receiving, inboxes) and the SDK reference.