Email infrastructure for Inngest workflows

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.

Built for Inngest developers

Human approval without polling hacks

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.

Per-step audit trail

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.

Identity verification before sending

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.

Graduated autonomy across workflow stages

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.

Inbound email as Inngest events

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.


Get started in minutes

Send email inside a durable step
typescript
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.

Human-in-the-loop approval with waitForEvent
typescript
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.

Inbound email triggers an Inngest function
typescript
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.

Scheduled digest with MultiMail inbox check
typescript
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.


Step by step

1

Install Inngest and get a MultiMail API key

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.

bash
npm install inngest
"cm"># Add to your environment:
"cm"># MULTIMAIL_API_KEY=mm_live_...
2

Create a MultiMail mailbox

Create the mailbox your Inngest functions will send from. You can use a `@multimail.dev` subdomain or bring your own domain.

bash
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"}'
3

Send email from a step

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.

bash
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();
});
4

Forward MultiMail webhooks as Inngest events

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.

bash
// 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');
}
5

Set your oversight mode per function

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.

bash
// 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' })

Common questions

Does MultiMail have a native Inngest integration or SDK?
No. MultiMail integrates with Inngest via its REST API. You call `https://api.multimail.dev` endpoints directly inside `step.run()` blocks using `fetch`. This keeps the integration lightweight and compatible with any Inngest deployment (Inngest Cloud, self-hosted, or local dev).
What happens if MultiMail returns an error and Inngest retries the step?
Pass an `Idempotency-Key` header on every `send_email` call, derived from the Inngest event ID and step name. MultiMail will deduplicate requests with the same key within 24 hours, so Inngest retries are safe and won't produce duplicate emails.
How do I implement human-in-the-loop approval without polling?
Send the email with `oversight_mode: 'gated_send'`, then use `step.waitForEvent('multimail/email.decided', { timeout: '48h' })`. Configure your MultiMail webhook to POST approval events to your webhook endpoint, which forwards them to Inngest via `inngest.send()`. The function resumes automatically when the event arrives.
Can I use MultiMail with Inngest's fan-out (step.sendEvent)?
Yes. A parent Inngest function can fan out to child functions via `step.sendEvent()`, and each child can independently call the MultiMail API. Use distinct idempotency keys per child (include the fan-out index or child event ID) to ensure each send is deduplicated correctly.
What oversight mode should I use for transactional email like receipts and password resets?
Use `autonomous` for transactional email where the content is deterministic and machine-generated. Reserve `gated_send` or `gated_all` for agent-drafted content where a human should review before delivery.
How do I test MultiMail calls in Inngest's local dev environment?
Use a `mm_test_...` API key from the MultiMail dashboard. Test keys route through MultiMail's sandbox: emails are recorded and visible in your dashboard but are never delivered to real recipients. The same REST API endpoints and response shapes apply, so no code changes are needed when switching from test to live.

Explore more

The only agent email with a verifiable sender

Email infrastructure built for AI agents. Verifiable identity, graduated oversight, and a 38-tool MCP server. Formally verified in Lean 4.