Email: domains, mailboxes & sending
Tandem gives an app three ways to do email:
- Real mailboxes — full IMAP + SMTP + JMAP accounts (send, receive, store).
- Transactional send API — a Resend-style HTTP lane for sending mail with no mailbox.
- Inbound webhooks — receive a mailbox’s mail as HTTP POSTs instead of over IMAP.
Email is custom-domain-only: every mailbox and every From address lives on a domain the org owns, and that domain must be onboarded first.
Step 1 — Onboard a domain
{ "name": "add_email_domain", "arguments": { "projectId": "…", "domain": "example.com" } }
add_email_domain registers an AWS SES sending identity, tells the mail server to accept the domain, and returns the DNS records to publish: MX (inbound), plus DKIM CNAMEs, SPF, and DMARC (outbound). If the domain’s DNS is managed by Tandem, the records are created automatically; otherwise the response gives a copy-paste record list for the tenant’s DNS provider.
Then verify:
{ "name": "verify_email_domain", "arguments": { "domainId": "…" } }
verify_email_domain re-checks SES (identity + DKIM) and DNS (inbound MX). Mail only sends/receives once the domain is fully verified; if something is still failing the response re-lists the records to add. list_email_domains shows the status of every onboarded domain.
Step 2a — Provision a mailbox
{ "name": "provision_email",
"arguments": { "projectId": "…", "domain": "example.com", "localPart": "hello", "attachServiceId": "…" } }
This creates a real mailbox (hello@example.com) on the self-hosted mail server and, when attachServiceId is set, injects the EMAIL_* runtime env vars (IMAP + SMTP + JMAP) on that service.
attach_email_to_service/detach_email_from_serviceinject or remove theEMAIL_*vars on any service (idempotent).reveal_email_credentialsreturns the full connection profile (IMAP/SMTP host/port/secure/username/password + JMAP url/token) for manual setup in a mail app. Audit-logged.rotate_email_credentialssets or auto-generates a new password and re-injects it into every attached service.list_emailslists mailboxes with address, DNS status, and attachments (never passwords).
Step 2b — Or use the transactional send API (no mailbox)
Create a key and send over HTTP:
{ "name": "create_email_api_key",
"arguments": { "projectId": "…", "name": "app-sender", "allowedDomains": ["example.com"] } }
The key is org-scoped, returned once (store it now), and optionally restricted to specific From domains via allowedDomains. Use it as a Bearer token:
curl -s https://portal.launchtandem.com/api/email/send \
-H "Authorization: Bearer <the-key>" \
-H "Content-Type: application/json" \
-d '{ "from": "hello@example.com", "to": "user@dest.com",
"subject": "Hi", "html": "<p>Hello</p>" }'
The From domain must be onboarded first. list_email_api_keys lists keys (name, prefix, allowed domains, last used, revoked) without the secret; revoke_email_api_key disables one immediately.
Step 3 — Receive mail over HTTP (inbound webhooks)
Instead of polling IMAP, have Tandem POST parsed mail to your app:
{ "name": "set_email_inbound_webhook",
"arguments": { "mailboxId": "…", "url": "https://app.example.com/inbound" } }
When mail arrives, the platform POSTs a JSON copy:
{ "type": "email.inbound", "mailbox": "hello@example.com",
"message": { "from": "…", "to": "…", "cc": "…", "subject": "…",
"text": "…", "html": "…", "messageId": "…",
"receivedAt": "…", "attachments": [] } }
set_email_inbound_webhook returns a signing secret once — verify the X-Tandem-Signature header (sha256=<hex HMAC-SHA256 of the raw body>) with it. get_email_inbound_webhook shows the config (secret never returned; call set again to rotate). remove_email_inbound_webhook stops forwarding — mail still lands in the mailbox.
Tearing down
remove_email_domaindeletes the SES identity and Tandem-managed DNS. It refuses while any mailboxes still exist on the domain — delete those first.delete_emaildeletes a mailbox (all stored mail is lost), removes theEMAIL_*vars from attached services, and tears down the domain’s DNS if it was the last mailbox. No undo. See Deleting resources.