Receiving Email

A mailbox is an address on your verified domain. Stack0 sends each email to it as a signed POST to your webhook.

DNS

Verify the domain for sending, then add an MX record with priority 10 and the value inbound-smtp.us-east-1.amazonaws.com. To keep your root domain's mail, use a subdomain such as mail.example.com.

Create a Mailbox

The result carries webhookSecret. Store it now: only create and rotateSecret return it.

create-mailbox.ts
import { Stack0 } from '@stack0/sdk'
const stack0 = new Stack0({ apiKey: process.env.STACK0_API_KEY! })
const mailbox = await stack0.mail.mailboxes.create({
domain: 'mail.example.com',
address: 'leads', // leads@mail.example.com
webhookUrl: 'https://example.com/api/inbound-email',
maxInboundPerDay: 1000,
})
// Save mailbox.webhookSecret as STACK0_MAILBOX_SECRET.
// Later, to replace it:
const { webhookSecret } = await stack0.mail.mailboxes.rotateSecret(mailbox.id)

Plus Addresses

Mail to leads+4821@mail.example.com goes to the mailbox leads@mail.example.com, unless a mailbox exists for the exact address. The payload's to is the address the sender used, and tag is "4821". Put a record id in the Reply-To of the emails you send, and replies come back tagged with it. Addresses compare without case.

Receive and Verify

Each delivery carries Standard Webhooks headers: webhook-id, webhook-timestamp, and webhook-signature. verifyInboundWebhook checks the signature and refuses a timestamp more than five minutes from now. Pass the raw body, not parsed JSON.

app/api/inbound-email/route.ts
import { verifyInboundWebhook, WebhookVerificationError } from '@stack0/sdk'
export async function POST(request: Request) {
let event
try {
event = await verifyInboundWebhook({
payload: await request.text(),
headers: request.headers,
secret: process.env.STACK0_MAILBOX_SECRET!,
})
} catch (err) {
if (err instanceof WebhookVerificationError) return new Response('bad signature', { status: 401 })
throw err
}
// event.id is the same on every retry: skip ids you already handled.
// event.tag, event.from, event.subject, event.text, event.html
// event.messageId, event.inReplyTo, event.references, event.headers
// event.attachments[i].url works until event.attachments[i].expiresAt
return new Response('ok')
}

Any Standard Webhooks library also verifies the signature, with the mailbox secret as the key. Deliveries also carry the older X-Stack0-Signature header (a hex HMAC-SHA256 of the body). That header does not cover a timestamp, so use webhook-signature in new code.

The Payload

email.inbound
{
"id": "6f1c2d4e-9a1b-4d8e-8f2a-3c4b5d6e7f80",
"event": "email.inbound",
"mailbox": "leads@mail.example.com",
"mailboxId": "...",
"tag": "4821",
"from": { "email": "seller@example.org", "name": "Pat" },
"to": "leads+4821@mail.example.com",
"cc": [], "bcc": [], "replyTo": null,
"subject": "Re: Your offer",
"text": "Yes, let's talk.", "html": "<p>Yes, let's talk.</p>",
"messageId": "<r1@example.org>",
"inReplyTo": "<o1@mail.example.com>",
"references": ["<o1@mail.example.com>"],
"headers": { "message-id": "<r1@example.org>", "received": ["from ...", "from ..."] },
"attachments": [
{ "filename": "photo.jpg", "contentType": "image/jpeg", "size": 48213,
"url": "https://...", "expiresAt": "2026-09-30T13:00:00.000Z" }
],
"metadata": null,
"receivedAt": "2026-09-30T12:00:00.000Z"
}
  • headers holds every header, with lowercased names. A repeated header, such as Received, is an array.
  • An attachment url works for one hour. Each retry and each getMessage call makes a new one.
  • A failed delivery retries after 1 minute, 5 minutes, 30 minutes, 2 hours, and 12 hours.

Daily Limit

A mailbox accepts maxInboundPerDay messages in any 24 hours. A message past the limit is stored and not delivered, and its webhookLastError starts with DAILY_LIMIT_REACHED.

Read Stored Messages

read-messages.ts
const { messages } = await stack0.mail.mailboxes.listMessages({ mailboxId: mailbox.id })
const message = await stack0.mail.mailboxes.getMessage(messages[0].id)
// message.headers, message.tag, message.webhookDelivered, message.webhookLastError
// cc, bcc, and references are arrays, as in the webhook payload,
// so mail.reply(message, { text }) threads a stored message too.