You shipped your app over a few weekends with Cursor or Claude Code. Receipts and password resets already go out through some email API. Then the boring part shows up: you want hello@, team@, sales@ and support@ on your own domain. The usual answer is a mailbox per person in Google Workspace or Microsoft 365, plus a shared inbox tool for support, plus a forwarding rule that breaks every few months.
For a one-person (or three-person) product, most of those mailboxes only ever receive mail. You don't need a seat for that. This post shows a setup where one domain does four jobs:
- Sends transactional email from your app (a Resend-compatible API, or SMTP).
- Receives mail for the whole domain and hands it to your code as a webhook.
-
Forwards
team@,sales@,billing@to everyone's personal inbox with email groups. - Puts
support@straight into a support inbox, next to live chat, WhatsApp and Messenger, where replies go out fromsupport@itself.
The examples use Ha Tang (I work on it). Every step comes with the prompt to give your coding agent, the code it should produce, and what to double-check.
The setup in one picture
┌── POST /emails (API or SMTP) ── receipts, resets, invoices
yourapp.com ────┤
└── MX → Ha Tang ─┬── support@ → Support inbox (chat, WhatsApp, Messenger, email in one place)
├── team@, sales@, billing@ → email groups → members' own Gmail/Outlook
└── anything else → email.received webhook → your code
Step 0: give your agent the real docs
Agents get email integrations wrong mostly by guessing field names. Point them at the docs instead:
# Claude Code: an MCP server that searches and reads the docs (no key needed for that)
claude mcp add hatang -- npx -y @hatang/mcp
# Any agent that reads AGENTS.md (Codex, Copilot, Cursor, Claude Code via CLAUDE.md)
curl -s https://hatang.dev/docs/agents/AGENTS.md >> AGENTS.md
In Cursor, add the same server to .cursor/mcp.json, or paste https://hatang.dev/docs/llms.txt into the prompt.
Step 1: verify the domain
Add the domain under Email → Domains (or hatang.domains.create({ name: "yourapp.com" })) and publish the records it lists: DKIM, SPF and the bounce MX on a send. subdomain, plus DMARC. Nothing here touches your existing mail.
Step 2: send transactional email
Prompt: "Send an order confirmation with
@hatang/sdkwhen an order is paid. Read the Ha Tang sending guide first. Use the order id as the idempotency key. The key is inHATANG_API_KEY."
// lib/email.ts (server only)
import { Hatang } from "@hatang/sdk";
const hatang = new Hatang(); // reads HATANG_API_KEY
export async function sendOrderConfirmation(order: { id: string; email: string; total: string }) {
const { data, error } = await hatang.emails.send(
{
from: "Acme <orders@yourapp.com>",
to: order.email,
reply_to: "support@yourapp.com", // replies land in the support inbox (step 5)
subject: `Order ${order.id} is confirmed`,
html: `<p>Thanks! Your order <strong>${order.id}</strong> (${order.total}) is confirmed.</p>`,
text: `Thanks! Your order ${order.id} (${order.total}) is confirmed.`,
},
{ idempotencyKey: `order-confirmation-${order.id}` }, // a retry never sends twice
);
if (error) throw new Error(`${error.name}: ${error.message}`);
return data.id;
}
The API follows Resend's shapes, so react: <OrderEmail /> works with React Email, and moving existing Resend code is mostly a new base URL and key (migration notes). Frameworks that only speak SMTP (Laravel, Django, WordPress, Nodemailer) can use smtp.hatang.dev:5870 with STARTTLS, user hatang, password = your API key.
Double-check what the agent wrote:
- The SDK is only imported in server code (route handlers, server actions, jobs), never in a client component.
- Every send that can be retried has an idempotency key derived from your own ids.
- The SDK returns
{ data, error }and does not throw for API errors: the agent must checkerror.
Step 3: receive mail for the domain
Turn Receiving on for the domain. This points the domain's MX at Ha Tang, so do it on a domain (or subdomain) that doesn't already receive mail at Google or Microsoft. If your root domain stays on Workspace for now, use a subdomain such as in.yourapp.com.
Every message to any address on the domain is stored and announced with an email.received webhook. Your app can react to the ones it cares about, for example invoices sent to bills@, or "reply to this email" threads from your users:
Prompt: "Add
app/api/hatang/webhooks/route.ts: verify the Ha Tang webhook signature on the raw body withconstructWebhookEvent, and foremail.receivedsent tobills@yourapp.com, fetch the message and its attachments and store them."
// app/api/hatang/webhooks/route.ts
import { Hatang, constructWebhookEvent, HatangSignatureError, type EmailReceivedEvent } from "@hatang/sdk";
const hatang = new Hatang();
export async function POST(req: Request) {
const raw = await req.text(); // verify the raw body, before any JSON parsing
let event;
try {
event = await constructWebhookEvent<EmailReceivedEvent>(raw, req.headers, process.env.HATANG_WEBHOOK_SECRET!);
} catch (err) {
if (err instanceof HatangSignatureError) return new Response(null, { status: 400 });
throw err;
}
if (event.type === "email.received" && event.data.to.includes("bills@yourapp.com")) {
const { data: email } = await hatang.emails.receiving.get(event.data.email_id);
const { data: files } = await hatang.emails.receiving.attachments.list(event.data.email_id);
await saveBill({ subject: email?.subject, text: email?.text, files: files?.data ?? [] }); // your code
}
return new Response(null, { status: 200 });
}
Double-check: agents love await req.json() before verifying. The signature is computed over the exact bytes, so parse after constructWebhookEvent, never before. Attachment download_urls expire after an hour: copy the file if you need it later.
Step 4: team@ and sales@ without mailbox seats
Most shared addresses of a small team only need to reach people. An email group is an address on your receiving domain whose mail is forwarded to a list of members, wherever their mailbox is:
Create them in the dashboard (Email → Email groups), or let your agent script it:
const { data: domains } = await hatang.domains.list();
const domain = domains!.data.find((d) => d.name === "yourapp.com")!;
await hatang.emailGroups.create({
domain_id: domain.id,
local_part: "sales",
name: "Sales",
members: ["you@gmail.com", "cofounder@gmail.com"],
});
What members get:
- Each member gets their own copy. The sender shows as "Alice via Sales" at
sales@yourapp.com, and Reply goes straight to Alice. - Members don't see each other's addresses. The original stays under Email → Received and still fires
email.received. - Loops, spam, auto-replies and bounces are not forwarded, and one sender can't flood a group (30 messages an hour).
The honest trade-off: when a member replies, the reply comes from their own address, not from sales@. That's fine for vendors, partners, internal mail and most early sales conversations. When the conversation is with a customer and should come from your domain, that's what the support inbox in the next step is for. Also note that every forwarded copy is a send: a message to a group of 3 uses 3 emails of your daily quota.
Step 5: support@ goes straight into the support inbox
Connect support@yourapp.com as an email channel under Customer Support → Channels → Add channel → Email. Because the domain already receives mail at Ha Tang, there is nothing else to configure: no forwarding rule, no extra MX.
What you get compared to a shared Gmail label:
- One inbox for every channel. Email threads sit next to website chat, the mobile SDKs, WhatsApp, Messenger, Instagram, Telegram, LINE, X, TikTok and Zalo, and the same customer is one profile across all of them.
-
Replies come from
support@yourapp.com, in the same thread, with delivery status on each reply (sent, delivered, opened, bounced). -
support+billing@works too: the part after+becomes a tag you can route on. - The AI assistant answers email as well, from the knowledge you give it, and hands over to you when it can't help.
- Acknowledgement emails, routing rules, tickets with SLA in business hours, and satisfaction ratings by email come with it.
Only support@ itself goes to the inbox. team@, sales@ and every other address keep working as in steps 3 and 4.
What changes on the bill
I won't quote prices because plans change, but here is what this setup removes for a small team:
| Before | After |
|---|---|
| A paid mailbox per person, mostly for shared addresses | Members keep their personal inbox; shared addresses are groups |
A shared-inbox or helpdesk tool for support@
|
support@ is a channel of the support inbox you already use for chat |
| Forwarding rules and a third-party parser for inbound mail |
email.received webhook on the same domain |
| Separate vendors for transactional email and inbound | One domain, one API key, one dashboard |
You may still want a real mailbox for yourself (for signing up to services, calendars, etc.). The point is that the shared addresses stop costing a seat each.
Watch it work
Every API call your keys make is logged with status, client and timing, which helps a lot when an agent wrote the integration:
Checklist before going live
- [ ] DKIM, SPF and the bounce MX verified; DMARC published (start with
p=none). - [ ] Receiving only on a domain or subdomain whose MX you control; not on a root domain still served by Workspace.
- [ ] No API key or webhook secret in client code or in a variable with a public prefix (
NEXT_PUBLIC_,VITE_). - [ ] Webhooks verified on the raw body; handlers return 200 quickly and are idempotent.
- [ ] Group members double-checked: everyone listed really should get every message to that address.
FAQ
I'm on Resend / SendGrid / Postmark. Sending code moves with a new base URL and key; the migration guide maps the fields.
Can my agent do this without me? With npx -y @hatang/mcp and an API key, an agent can send a test email, check whether your domain's DNS records are verified and read the docs as Markdown. Add --read-only if you only want it to look things up.
Where does the mail go out from? Amazon SES, from your own verified domain, wherever your users are. Nothing in the setup above is tied to a country: any domain, any registrar, any DNS provider.
Docs
- Send email and SMTP relay
- Receive email and Email groups
- Connect a support email address
- Use Ha Tang with AI coding agents
- A runnable starter: hatang-nextjs-starter
I work on Ha Tang.




Top comments (0)