MultiMail gives Inngest functions auditable, agent-safe email with graduated oversight — from gated approval to fully autonomous sending.
Inngest orchestrates durable, event-driven functions with built-in retries, fan-out, and scheduled execution. When those functions send email — onboarding sequences, lifecycle alerts, transactional confirmations — you need guarantees that Inngest itself doesn't provide: identity verification, human approval gates, and a full audit trail of who sent what and when.
MultiMail's REST API integrates directly inside any Inngest step. Each `step.run()` block can call `send_email`, `decide_email`, or `list_pending` without any additional SDK — just a `fetch` call with your `mm_live_...` bearer token. Inngest handles retry logic and durable state; MultiMail handles email identity, deliverability, and oversight.
The combination is especially useful for AI-driven workflows: an Inngest function can draft and queue emails through MultiMail's `gated_send` mode, pause execution until a human approves via `decide_email`, then resume and record the outcome — all within a single durable function run.
MultiMail's `gated_send` mode queues outbound email and fires an approval webhook when a human acts. Your Inngest function can `step.waitForEvent()` on that webhook, eliminating manual polling and keeping the function durable across approval delays of minutes or days.
Every MultiMail API call from inside a `step.run()` block is recorded with the message ID, sender identity, oversight mode, and decision outcome. Because Inngest replays steps on retry, MultiMail's idempotency keys prevent duplicate sends even when a step runs more than once.
MultiMail cryptographically verifies the sending domain before delivery. Inngest functions often run in background contexts where the originating identity is ambiguous — MultiMail makes the verified sender explicit on every outbound message, satisfying CAN-SPAM and DMARC alignment requirements.
You can set different oversight modes per Inngest function. An early-stage onboarding function can run in `gated_all` while a mature transactional receipt function runs in `autonomous` — both use the same MultiMail mailbox and API key.
MultiMail webhooks deliver inbound email as JSON payloads that map directly onto Inngest event schemas. A `multimail/email.received` event can trigger a classification function, a support ticket function, or an agent response function without any custom webhook adapter.
import { Inngest } from 'inngest';
const inngest = new Inngest({ id: 'my-app' });
export const sendWelcomeEmail = inngest.createFunction(
{ id: 'send-welcome-email' },
{ event: 'user/signed-up' },
async ({ event, step }) => {
const result = await step.run('send-welcome', async () => {
const res = await fetch('https://api.multimail.dev/send_email', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.MULTIMAIL_API_KEY}`,
'Content-Type': 'application/json',
'Idempotency-Key': `welcome-${event.data.userId}-${event.id}`,
},
body: JSON.stringify({
from: '[email protected]',
to: event.data.email,
subject: 'Welcome to Acme',
text: `Hi ${event.data.name}, your account is ready.`,
oversight_mode: 'autonomous',
}),
});
if (!res.ok) throw new Error(`MultiMail error: ${res.status}`);
return res.json();
});
return { messageId: result.id };
}
);Call the MultiMail REST API from inside a `step.run()` block. Inngest retries the step on failure; MultiMail's idempotency key (the Inngest run ID) prevents duplicate sends on replay.
import { Inngest } from 'inngest';
const inngest = new Inngest({ id: 'my-app' });
export const agentEmailDraft = inngest.createFunction(
{ id: 'agent-email-draft' },
{ event: 'agent/draft-ready' },
async ({ event, step }) => {
"cm">// Queue email for human approval
const queued = await step.run('queue-for-approval', async () => {
const res = await fetch('https://api.multimail.dev/send_email', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.MULTIMAIL_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
from: '[email protected]',
to: event.data.recipient,
subject: event.data.subject,
text: event.data.body,
oversight_mode: 'gated_send',
metadata: { runId: event.id },
}),
});
return res.json();
});
"cm">// Wait up to 48 hours for approval webhook
const approval = await step.waitForEvent('wait-for-approval', {
event: 'multimail/email.decided',
timeout: '48h',
match: 'data.message_id',
"cm">// data.message_id must equal queued.id
"cm">// configure your Inngest event key in the MultiMail webhook settings
});
if (!approval || approval.data.decision !== 'approved') {
return { status: 'rejected', messageId: queued.id };
}
return { status: 'sent', messageId: queued.id };
}
);Queue an outbound email in `gated_send` mode, then pause the Inngest function waiting for the MultiMail approval webhook. The function resumes automatically when the human approves or rejects.
import { Inngest } from 'inngest';
const inngest = new Inngest({ id: 'my-app' });
"cm">// In your webhook handler (e.g. a Next.js API route or Hono endpoint):
"cm">// POST /api/webhooks/multimail
"cm">// Forward the parsed payload to Inngest:
"cm">// await inngest.send({ name: 'multimail/email.received', data: req.body });
export const classifyInboundEmail = inngest.createFunction(
{ id: 'classify-inbound-email' },
{ event: 'multimail/email.received' },
async ({ event, step }) => {
const email = event.data;
"cm">// Read full email body via MultiMail API
const full = await step.run('read-email', async () => {
const res = await fetch(
`https:"cm">//api.multimail.dev/read_email?id=${email.id}`,
{ headers: { Authorization: `Bearer ${process.env.MULTIMAIL_API_KEY}` } }
);
return res.json();
});
"cm">// Tag based on subject keywords
const tag = full.subject.toLowerCase().includes('urgent') ? 'urgent' : 'normal';
await step.run('tag-email', async () => {
await fetch('https://api.multimail.dev/tag_email', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.MULTIMAIL_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ id: email.id, tags: [tag] }),
});
});
return { emailId: email.id, tag };
}
);Configure a MultiMail webhook to send `multimail/email.received` events to Inngest's event endpoint. The function receives parsed email data and can reply, tag, or route the message.
import { Inngest } from 'inngest';
const inngest = new Inngest({ id: 'my-app' });
export const dailyDigest = inngest.createFunction(
{ id: 'daily-digest' },
{ cron: '0 8 * * *' },
async ({ step }) => {
const [inbox, pending] = await step.run('fetch-email-state', async () => {
const [inboxRes, pendingRes] = await Promise.all([
fetch('https:"cm">//api.multimail.dev/[email protected]&unread=true&limit=50', {
headers: { Authorization: `Bearer ${process.env.MULTIMAIL_API_KEY}` },
}),
fetch('https:"cm">//api.multimail.dev/[email protected]', {
headers: { Authorization: `Bearer ${process.env.MULTIMAIL_API_KEY}` },
}),
]);
return Promise.all([inboxRes.json(), pendingRes.json()]);
});
const digestText = [
`Daily digest for [email protected]`,
`Unread messages: ${inbox.emails.length}`,
`Pending approvals: ${pending.messages.length}`,
'',
...pending.messages.map((m: any) => `- [PENDING] ${m.subject} → ${m.to}`),
].join('\n');
await step.run('send-digest', async () => {
await fetch('https://api.multimail.dev/send_email', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.MULTIMAIL_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
from: '[email protected]',
to: '[email protected]',
subject: `Agent email digest — ${new Date().toLocaleDateString()}`,
text: digestText,
oversight_mode: 'autonomous',
}),
});
});
return { sent: true, unread: inbox.emails.length, pending: pending.messages.length };
}
);An Inngest scheduled function that checks a MultiMail inbox daily, aggregates pending emails, and sends a digest. Uses `check_inbox` and `list_pending` to build the summary.
Install the Inngest SDK and create a MultiMail account to get your `mm_live_...` API key. You will call the MultiMail REST API directly — no additional package needed.
npm install inngest
"cm"># Add to your environment:
"cm"># MULTIMAIL_API_KEY=mm_live_...Create the mailbox your Inngest functions will send from. You can use a `@multimail.dev` subdomain or bring your own domain.
curl -X POST https://api.multimail.dev/create_mailbox \
-H "Authorization: Bearer $MULTIMAIL_API_KEY" \
-H "Content-Type: application/json" \
-d &"cm">#039;{"address": "[email protected]", "display_name": "Acme Agent"}'Call `https://api.multimail.dev/send_email` inside a `step.run()` block. Pass an `Idempotency-Key` header derived from the Inngest event ID so retries don't duplicate sends.
await step.run(&"cm">#039;send-notification', async () => {
const res = await fetch(&"cm">#039;https://api.multimail.dev/send_email', {
method: &"cm">#039;POST',
headers: {
Authorization: `Bearer ${process.env.MULTIMAIL_API_KEY}`,
&"cm">#039;Content-Type': 'application/json',
&"cm">#039;Idempotency-Key': `notif-${event.id}`,
},
body: JSON.stringify({
from: &"cm">#039;[email protected]',
to: event.data.email,
subject: &"cm">#039;Your report is ready',
text: &"cm">#039;The nightly report has been generated and attached.',
oversight_mode: &"cm">#039;gated_send',
}),
});
if (!res.ok) throw new Error(`MultiMail ${res.status}`);
return res.json();
});In your webhook endpoint, forward the MultiMail payload to Inngest using `inngest.send()`. This lets Inngest functions react to inbound email and approval decisions as typed events.
// app/api/webhooks/multimail/route.ts (Next.js App Router)
import { inngest } from &"cm">#039;@/lib/inngest';
export async function POST(req: Request) {
const payload = await req.json();
const eventName =
payload.type === &"cm">#039;email.received' ? 'multimail/email.received' :
payload.type === &"cm">#039;email.decided' ? 'multimail/email.decided' :
null;
if (eventName) {
await inngest.send({ name: eventName, data: payload });
}
return new Response(&"cm">#039;ok');
}Choose the right `oversight_mode` for each Inngest function based on risk. Transactional receipts can use `autonomous`; agent-drafted outreach should start with `gated_send` until you have confidence in the output quality.
// Low-risk transactional: autonomous
body: JSON.stringify({ ..., oversight_mode: &"cm">#039;autonomous' })
// Agent-drafted outreach: gated_send (human approves before delivery)
body: JSON.stringify({ ..., oversight_mode: &"cm">#039;gated_send' })
// All actions need approval during testing: gated_all
body: JSON.stringify({ ..., oversight_mode: &"cm">#039;gated_all' })Email infrastructure built for AI agents. Verifiable identity, graduated oversight, and a 38-tool MCP server. Formally verified in Lean 4.