Send email from Discord bots without losing oversight

Wire Discord slash commands and interactions to MultiMail's approval queue so every bot-triggered email send goes through the right gate before delivery.


Discord bots have become a standard interface for internal team workflows — support queues, moderation actions, member onboarding, and outreach requests all flow through bot commands. When those commands need to trigger outbound email, the gap between "bot says send" and "email actually delivered" is where mistakes happen.

MultiMail sits between your Discord bot and your email infrastructure. A slash command fires, the bot calls MultiMail's REST API, and depending on your oversight mode, the message either queues for human approval or sends immediately with a notification. Either way, you get a full audit trail of who triggered what and when.

This pattern is common in community management tools, support bots, and internal tooling where a Discord interaction is the user-facing trigger but the actual effect — an email to a customer, a partner, or a mailing list — needs to be controlled and traceable.

Built for Discord developers

Approval queue for chat-triggered sends

Discord commands make email outreach dangerously easy. MultiMail's gated_send mode holds every bot-triggered send in a review queue. A human approves or rejects via the MultiMail dashboard or API before any message leaves your domain.

Recipient verification before delivery

MultiMail checks recipients against your contact list and domain policy before queuing a send. A typo in a Discord command won't result in email to an unintended address.

Audit trail tied to Discord interactions

Every send, approval, and rejection is logged with the originating interaction ID. You can trace any email back to the exact Discord command that triggered it, including the user, channel, and timestamp.

Inbox reads without credential sharing

Discord bots can query MultiMail inboxes via the REST API without needing direct IMAP or SMTP credentials. The bot gets read access scoped to specific mailboxes with no path to credential leakage.

Webhook callbacks to Discord channels

MultiMail can post delivery confirmations, bounce alerts, and approval requests back to specific Discord channels via webhooks, closing the loop without requiring the bot to poll.


Get started in minutes

Slash command that queues an email for approval
javascript
import { Client, GatewayIntentBits, SlashCommandBuilder } from 'discord.js';

const client = new Client({ intents: [GatewayIntentBits.Guilds] });

client.on('interactionCreate', async (interaction) => {
  if (!interaction.isChatInputCommand()) return;
  if (interaction.commandName !== 'sendemail') return;

  const to = interaction.options.getString('to', true);
  const subject = interaction.options.getString('subject', true);
  const body = interaction.options.getString('body', true);

  await interaction.deferReply({ ephemeral: true });

  const response = await fetch('https://api.multimail.dev/v1/send_email', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.MULTIMAIL_API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      from: '[email protected]',
      to,
      subject,
      body,
      oversight_mode: 'gated_send',
      metadata: {
        discord_user: interaction.user.tag,
        discord_channel: interaction.channelId,
        discord_guild: interaction.guildId,
      },
    }),
  });

  const result = await response.json();

  if (result.status === 'queued') {
    await interaction.editReply(
      `Email queued for approval (ID: \`${result.message_id}\`). A reviewer will approve or reject it in the MultiMail dashboard.`
    );
  } else {
    await interaction.editReply(`Failed to queue email: ${result.error}`);
  }
});

client.login(process.env.DISCORD_TOKEN);

A /sendemail slash command handler that calls MultiMail's send_email endpoint in gated_send mode. The email is held until a human approves it in the MultiMail dashboard.

Bot command to check a mailbox inbox
javascript
import { Client, GatewayIntentBits } from 'discord.js';

const client = new Client({ intents: [GatewayIntentBits.Guilds] });

client.on('interactionCreate', async (interaction) => {
  if (!interaction.isChatInputCommand()) return;
  if (interaction.commandName !== 'checkinbox') return;

  const mailbox = interaction.options.getString('mailbox') ?? '[email protected]';

  await interaction.deferReply({ ephemeral: true });

  const response = await fetch(
    `https:"cm">//api.multimail.dev/v1/check_inbox?mailbox=${encodeURIComponent(mailbox)}&limit=5`,
    {
      headers: {
        'Authorization': `Bearer ${process.env.MULTIMAIL_API_KEY}`,
      },
    }
  );

  const { emails } = await response.json();

  if (!emails.length) {
    await interaction.editReply(`No unread messages in ${mailbox}.`);
    return;
  }

  const lines = emails.map((e) =>
    `• **${e.subject}** — from \`${e.from}\` (${new Date(e.received_at).toLocaleString()})`
  );

  await interaction.editReply(
    `**${emails.length} unread in ${mailbox}:**\n${lines.join('\n')}`
  );
});

client.login(process.env.DISCORD_TOKEN);

A /checkinbox command that calls MultiMail's check_inbox endpoint and posts a summary of recent messages back to the user in Discord.

MultiMail webhook posting approval requests to a Discord channel
javascript
import express from 'express';

const app = express();
app.use(express.json());

app.post('/webhooks/multimail', async (req, res) => {
  const event = req.body;

  if (event.type !== 'message.pending_approval') {
    return res.json({ ok: true });
  }

  const { message_id, to, subject, from, metadata } = event.data;

  const discordPayload = {
    content: '**Email approval required**',
    embeds: [
      {
        title: subject,
        fields: [
          { name: 'From', value: from, inline: true },
          { name: 'To', value: to, inline: true },
          { name: 'Triggered by', value: metadata?.discord_user ?? 'unknown', inline: true },
          { name: 'Message ID', value: `\`${message_id}\``, inline: false },
        ],
        description: `Approve at https:"cm">//app.multimail.dev/approvals/${message_id}`,
        color: 0xf59e0b,
      },
    ],
  };

  await fetch(process.env.DISCORD_WEBHOOK_URL, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(discordPayload),
  });

  res.json({ ok: true });
});

app.listen(3000);

A lightweight HTTP handler that receives MultiMail approval webhooks and posts a formatted message to a Discord channel with approve/reject context.

Python bot that reads and replies to email threads via Discord
python
import os
import discord
from discord.ext import commands
import httpx

bot = commands.Bot(command_prefix=&"cm">#039;!')
MULTIMAIL_BASE = &"cm">#039;https://api.multimail.dev/v1'
HEADERS = {&"cm">#039;Authorization': f'Bearer {os.environ["MULTIMAIL_API_KEY"]}'}

@bot.command(name=&"cm">#039;reply')
async def reply_email(ctx, thread_id: str, *, reply_body: str):
    """Reply to an email thread: !reply <thread_id> <body>"""
    async with httpx.AsyncClient() as client:
        resp = await client.post(
            f&"cm">#039;{MULTIMAIL_BASE}/reply_email',
            headers=HEADERS,
            json={
                &"cm">#039;thread_id': thread_id,
                &"cm">#039;body': reply_body,
                &"cm">#039;oversight_mode': 'gated_send',
                &"cm">#039;metadata': {
                    &"cm">#039;discord_user': str(ctx.author),
                    &"cm">#039;discord_channel': str(ctx.channel.id),
                },
            },
        )
        result = resp.json()

    if result.get(&"cm">#039;status') == 'queued':
        await ctx.reply(f&"cm">#039;Reply queued for approval (`{result["message_id"]}`). Awaiting review.')
    else:
        await ctx.reply(f&"cm">#039;Error: {result.get("error", "unknown")}')

bot.run(os.environ[&"cm">#039;DISCORD_TOKEN'])

A Python Discord bot command that fetches a thread from MultiMail and posts a reply, keeping the full email context in the MultiMail audit log.


Step by step

1

Create a MultiMail mailbox and get your API key

Sign up at multimail.dev and create a mailbox (e.g., [email protected] or a @multimail.dev address). Copy your API key from the dashboard — it starts with mm_live_ for production or mm_test_ for test mode.

bash
curl -X POST https://api.multimail.dev/v1/create_mailbox \
  -H &"cm">#039;Authorization: Bearer $MULTIMAIL_API_KEY...' \
  -H &"cm">#039;Content-Type: application/json' \
  -d &"cm">#039;{"address": "[email protected]"}'
2

Install discord.js and register your slash commands

Install the Discord library and register the slash commands your bot will expose. Use the Discord Developer Portal to create an application and bot token.

bash
npm install discord.js

"cm"># Register a /sendemail slash command via the Discord REST API
curl -X POST https://discord.com/api/v10/applications/$APP_ID/commands \
  -H "Authorization: Bot $DISCORD_TOKEN" \
  -H "Content-Type: application/json" \
  -d &"cm">#039;{
    "name": "sendemail",
    "description": "Queue an email for approval",
    "options": [
      {"name": "to", "description": "Recipient address", "type": 3, "required": true},
      {"name": "subject", "description": "Email subject", "type": 3, "required": true},
      {"name": "body", "description": "Email body", "type": 3, "required": true}
    ]
  }&"cm">#039;
3

Wire the interaction handler to MultiMail

In your bot's interactionCreate handler, call MultiMail's send_email endpoint with oversight_mode set to gated_send. Pass Discord metadata so every queued message is traceable back to its source.

bash
const response = await fetch(&"cm">#039;https://api.multimail.dev/v1/send_email', {
  method: &"cm">#039;POST',
  headers: {
    &"cm">#039;Authorization': `Bearer ${process.env.MULTIMAIL_API_KEY}`,
    &"cm">#039;Content-Type': 'application/json',
  },
  body: JSON.stringify({
    from: &"cm">#039;[email protected]',
    to: interaction.options.getString(&"cm">#039;to'),
    subject: interaction.options.getString(&"cm">#039;subject'),
    body: interaction.options.getString(&"cm">#039;body'),
    oversight_mode: &"cm">#039;gated_send',
    metadata: { discord_user: interaction.user.tag },
  }),
});
4

Set up a webhook listener to close the loop

Register a webhook URL in the MultiMail dashboard under Settings > Webhooks. Point it at your bot's HTTP endpoint. When an email is approved, rejected, or bounces, MultiMail will POST an event that your bot can relay back to the originating Discord channel.

bash
"cm"># In your MultiMail dashboard, register:
"cm"># Webhook URL: https://yourbot.yourdomain.com/webhooks/multimail
"cm"># Events: message.pending_approval, message.delivered, message.bounced

"cm"># Then in your bot server:
app.post(&"cm">#039;/webhooks/multimail', (req, res) => {
  const { type, data } = req.body;
  if (type === &"cm">#039;message.delivered') {
    channel.send(`Email ${data.message_id} delivered to ${data.to}`);
  }
  res.json({ ok: true });
});

Common questions

Can I use MultiMail to send emails from a Discord bot without any human approval?
Yes. Set oversight_mode to 'autonomous' or 'monitored' in your send_email call. With 'autonomous', emails send immediately with no approval step. With 'monitored', emails send immediately but a notification is posted to your configured channel. For most bots handling outbound email to external recipients, 'gated_send' is the safer default.
How do I prevent Discord users from sending email to arbitrary addresses?
Use MultiMail's recipient allowlist feature to restrict the 'to' field to a set of verified addresses or domains. Any send attempt to an address outside the allowlist will be rejected with a 403 before it enters the approval queue. Configure the allowlist in the MultiMail dashboard under mailbox settings.
Does MultiMail support Discord webhooks directly, or do I need a separate server?
MultiMail emits webhooks to any HTTPS endpoint you register. You need a small HTTP listener to receive them — a Cloudflare Worker, a Vercel function, or a Node.js server all work. The listener receives the event payload and can then post to Discord using a Discord webhook URL or the bot's channel.send() method.
Can I read emails from a MultiMail inbox and display them in Discord?
Yes. Use the check_inbox endpoint to fetch unread messages and read_email to retrieve the full body of a specific message. Both are standard REST calls your Discord bot can make in an interaction handler. Be mindful of Discord's message length limits (2000 characters) — truncate or summarize long email bodies before posting.
How do I associate a Discord user with an email action in the audit log?
Pass a metadata object in your send_email or reply_email call with fields like discord_user, discord_channel, and discord_guild. MultiMail stores this metadata alongside the email record and includes it in webhook payloads and dashboard views, giving you a full trace from Discord interaction to email delivery.
What happens if an approved email bounces after a Discord-triggered send?
MultiMail emits a message.bounced webhook event. Register a webhook handler that forwards the bounce notification back to the Discord channel where the original command was issued. Include the message_id in the notification so the team can correlate it with the approval they reviewed.

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.