brainstorming
brainstorming
You MUST use this before any creative work - creating features, building components, adding functionality, modifying behavior, or planning anything new, in software or out of it (a talk, a business, a renovation).
mailgunMailgun is an email API for sending, receiving and tracking transactional and bulk email. Use when a user asks to send email through Mailgun, send batch emails with per-recipient variables, set up inbound routes, validate email addresses, or verify Mailgun webhooks for delivery, open, click and bounce events.
Independent GitHub snapshot · Automated classification · Original documentation governs
Mailgun is a developer-focused email API (closed source, hosted) for sending, receiving and tracking email over HTTPS or SMTP. It offers batch sending with per-recipient variables, inbound routes, address validation, event webhooks and logs. Accounts live in one of two regions, US (https://api.mailgun.net) or EU (https://api.eu.mailgun.net); the region is fixed per domain and the API key only works against its own region. Checked against the Mailgun documentation and mailgun.js 14.0.1 (August 2026).
Add a sending domain in the dashboard, preferably a subdomain such as mg.acmeshop.io, and publish the SPF and DKIM DNS records Mailgun shows until the domain verifies. A sandbox domain works without DNS but only delivers to recipients you authorize. Use a separate subdomain for marketing mail so its reputation does not affect receipts and password resets. Keep the Private API key in MAILGUN_API_KEY and the HTTP webhook signing key (a different secret) in MAILGUN_WEBHOOK_SIGNING_KEY.
npm install mailgun.js
Since version 3 the constructor takes a FormData implementation; on Node 18+ the built-in global FormData works, no form-data package needed.
// lib/mailgun.ts
import Mailgun from 'mailgun.js'
const mailgun = new Mailgun(FormData)
export const mg = mailgun.client({
username: 'api',
key: process.env.MAILGUN_API_KEY!,
url: 'https://api.eu.mailgun.net', // omit for a US-region account
})
await mg.messages.create('mg.acmeshop.io', {
from: 'Acme Shop <noreply@mg.acmeshop.io>',
to: ['dana.ortiz@fastmail.com'],
subject: 'Your order #4821 has shipped',
text: 'Order #4821 shipped today.',
html: '<h1>Order #4821 has shipped</h1>',
'o:tag': ['order-shipped'],
'o:tracking-opens': 'yes',
'o:tracking-clicks': 'yes',
})
The first argument is the sending domain, not the From address. A resolved promise only means Mailgun accepted the message; delivery is reported by events.
Up to 1,000 recipients per call. Pass recipient-variables (JSON keyed by address) or every recipient sees the whole to list; reference values as %recipient.name%.
await mg.messages.create('mg.acmeshop.io', {
from: 'Acme Shop <news@mg.acmeshop.io>',
to: ['dana.ortiz@fastmail.com', 'li.wei@protonmail.com'],
subject: 'Hi %recipient.first%, your points expire soon',
text: 'You have %recipient.points% points left.',
'recipient-variables': JSON.stringify({
'dana.ortiz@fastmail.com': { first: 'Dana', points: 320 },
'li.wei@protonmail.com': { first: 'Li', points: 90 },
}),
})
const check = await mg.validate.get('dana.ortiz@fastmail.com')
console.log(check.result, check.risk) // e.g. "deliverable" "low"
if (check.result === 'undeliverable') { /* do not send */ }
Result values include deliverable, undeliverable, do_not_send, catch_all and unknown; risk is low, medium, high or unknown. Validation is a separately metered service, so confirm your plan includes it before running it over a list.
await mg.routes.create({
priority: 1,
description: 'Support mailbox to the helpdesk',
expression: 'match_recipient("support@mg.acmeshop.io")',
action: ['forward("https://acmeshop.io/inbound/mailgun")', 'stop()'],
})
The domain's MX records must point to Mailgun for routes to receive mail.
Configure webhook URLs per event (delivered, opened, clicked, failed, complained, unsubscribed) in the dashboard or API. Each JSON payload has signature (timestamp, token, signature) and event-data. The signature is a hex HMAC-SHA256 of timestamp + token keyed with the webhook signing key.
// routes/webhooks/mailgun.ts
import crypto from 'crypto'
const seenTokens = new Set<string>() // use Redis with a TTL in production
function isValid({ timestamp, token, signature }: { timestamp: string; token: string; signature: string }) {
const expected = crypto.createHmac('sha256', process.env.MAILGUN_WEBHOOK_SIGNING_KEY!)
.update(timestamp + token).digest('hex')
const fresh = Math.abs(Date.now() / 1000 - Number(timestamp)) < 300
return fresh && !seenTokens.has(token) &&
expected.length === signature.length &&
crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature))
}
export async function handleMailgunWebhook(req: { body: any }) {
const { signature, 'event-data': event } = req.body
if (!isValid(signature)) return { status: 401 }
seenTokens.add(signature.token)
switch (event.event) {
case 'failed':
if (event.severity === 'permanent') await suppressAddress(event.recipient)
break
case 'complained':
case 'unsubscribed':
await unsubscribeUser(event.recipient)
break
case 'delivered':
await markDelivered(event.message.headers['message-id'])
break
}
return { status: 200 }
}
User request: "Send a shipping confirmation from our checkout service with Mailgun. We are on the EU region."
The agent installs mailgun.js, creates lib/mailgun.ts with url: 'https://api.eu.mailgun.net' and the key from MAILGUN_API_KEY, and calls mg.messages.create('mg.acmeshop.io', {...}) with from, to, subject, text, html and o:tag: ['order-shipped']. A successful call returns an object like { id: '<20261003091500.1.4F2A@mg.acmeshop.io>', message: 'Queued. Thank you.' }; the Mailgun log then shows the delivered event.
User request: "Hard bounces keep hurting our sender score. Wire Mailgun webhooks into our user table."
The agent adds the signed-webhook handler above on POST /webhooks/mailgun, registers the URL for failed and complained events, and on a permanent failure or complaint sets email_suppressed = true for the recipient. Sending a test event from the dashboard returns 200, and a request with a wrong signature fails authentication.
o:tracking-opens) are unreliable because mail clients preload images; use clicks and deliveries for decisions.o:testmode: 'yes' to exercise the API without delivering mail.