Skip to main content
Technology & EngineeringEmail Services236 lines

Mailgun

Send transactional and marketing email with Mailgun. Use this skill when the

Quick Summary33 lines
You are an email operations specialist who integrates Mailgun into projects. Mailgun
provides a developer-friendly API for transactional and marketing email with strong
support for mailing lists, inbound routing, and email validation.

## Key Points

- Use recipient variables for batch personalization — more efficient than individual sends
- Validate email addresses before adding to mailing lists
- Use tags on every send for filtering events and analytics
- Set up custom tracking domains for branded click/open tracking
- Use Mailgun routes for inbound email processing instead of polling
- Monitor events dashboard for delivery issues
- Use dedicated IPs when sending 50K+ emails/day
- Sending to mailing lists without unsubscribe handling
- Not checking email validation results before bulk sends
- Using the sandbox domain in production
- Ignoring permanent failure webhooks — address will keep bouncing
- Sending marketing email through transactional infrastructure without separation

## Quick Example

```bash
npm install mailgun.js form-data
```

```typescript
const validation = await mg.validate.get('user@example.com');
if (validation.result === 'undeliverable') {
  // Don't send — address is bad
}
```
skilldb get email-services-skills/MailgunFull skill: 236 lines
Paste into your CLAUDE.md or agent config

Mailgun Email Integration

You are an email operations specialist who integrates Mailgun into projects. Mailgun provides a developer-friendly API for transactional and marketing email with strong support for mailing lists, inbound routing, and email validation.

Core Philosophy

API and SMTP parity

Mailgun offers identical functionality through REST API and SMTP. Use the API for modern applications and SMTP for legacy systems. Both support tags, variables, and tracking — choose based on your architecture, not feature needs.

Mailing lists are first-class

Unlike services where you bolt on list management, Mailgun's mailing lists are deeply integrated. You can send to a list address, manage subscribers, and handle unsubscribes natively without external tools.

Routes enable email-driven workflows

Mailgun's route system matches incoming email by recipient or header pattern and forwards, stores, or webhooks the message. This enables reply processing, support ticket creation, and email-driven automation.

Setup

Install

npm install mailgun.js form-data

Initialize

import Mailgun from 'mailgun.js';
import formData from 'form-data';

const mailgun = new Mailgun(formData);
const mg = mailgun.client({
  username: 'api',
  key: process.env.MAILGUN_API_KEY,
  url: 'https://api.mailgun.net', // or https://api.eu.mailgun.net for EU
});

Key Techniques

Simple send

await mg.messages.create('yourdomain.com', {
  from: 'App <noreply@yourdomain.com>',
  to: ['user@example.com'],
  subject: 'Your password reset link',
  text: 'Reset: https://...',
  html: '<p>Click <a href="...">here</a> to reset.</p>',
  'o:tag': ['security', 'password_reset'],
  'v:userId': '123',
});

Template send

Create templates in the Mailgun dashboard or via API, then send with variables.

// Create template via API
await mg.domains.domainTemplates.create('yourdomain.com', {
  name: 'password-reset',
  description: 'Password reset email',
  template: '<p>Hi {{name}}, click <a href="{{resetUrl}}">here</a></p>',
});

// Send with template
await mg.messages.create('yourdomain.com', {
  from: 'App <noreply@yourdomain.com>',
  to: ['user@example.com'],
  subject: 'Reset your password',
  template: 'password-reset',
  'h:X-Mailgun-Variables': JSON.stringify({
    name: 'Alice',
    resetUrl: 'https://...',
  }),
});

Batch send with recipient variables

Send personalized email to many recipients in one API call (up to 1,000).

await mg.messages.create('yourdomain.com', {
  from: 'App <noreply@yourdomain.com>',
  to: ['user1@example.com', 'user2@example.com'],
  subject: 'Your weekly summary, %recipient.name%',
  html: '<p>Hi %recipient.name%, here is your summary...</p>',
  'recipient-variables': JSON.stringify({
    'user1@example.com': { name: 'Alice', id: '1' },
    'user2@example.com': { name: 'Bob', id: '2' },
  }),
});

Scheduled send

await mg.messages.create('yourdomain.com', {
  from: 'App <noreply@yourdomain.com>',
  to: ['user@example.com'],
  subject: 'Trial ending tomorrow',
  html: trialEndingHtml,
  'o:deliverytime': 'Thu, 25 Dec 2025 09:00:00 UTC',
});

Mailing lists

// Create list
await mg.lists.create({
  address: 'newsletter@yourdomain.com',
  name: 'Newsletter',
  access_level: 'readonly',
});

// Add member
await mg.lists.members.createMember('newsletter@yourdomain.com', {
  address: 'user@example.com',
  name: 'Alice',
  vars: JSON.stringify({ plan: 'pro' }),
  subscribed: true,
});

// Send to list
await mg.messages.create('yourdomain.com', {
  from: 'Updates <updates@yourdomain.com>',
  to: ['newsletter@yourdomain.com'],
  subject: 'What shipped this week',
  html: newsletterHtml,
});

Email validation

Verify addresses before sending to reduce bounces.

const validation = await mg.validate.get('user@example.com');
if (validation.result === 'undeliverable') {
  // Don't send — address is bad
}

Inbound routes

// Create a route that forwards matching inbound email to your webhook
await mg.routes.create({
  priority: 0,
  expression: 'match_recipient("support@yourdomain.com")',
  action: ['forward("https://yourdomain.com/api/webhooks/mailgun-inbound")', 'store()'],
  description: 'Support inbox',
});

Webhook Processing

EventAction
deliveredMark delivered in message log
failed (permanent)Suppress address
failed (temporary)Log, monitor for repeated failures
complainedSuppress from non-essential sends
openedUpdate engagement metrics
clickedUpdate engagement metrics
unsubscribedUpdate subscription state
export async function POST(req: Request) {
  const form = await req.formData();
  const event = form.get('event') as string;
  const recipient = form.get('recipient') as string;

  switch (event) {
    case 'failed':
      const severity = form.get('severity') as string;
      if (severity === 'permanent') {
        await suppressAddress(recipient, 'hard_bounce');
      }
      break;
    case 'complained':
      await suppressAddress(recipient, 'complaint');
      break;
    case 'unsubscribed':
      await updateSubscription(recipient, false);
      break;
  }

  return new Response('OK');
}

EU vs US Region

Mailgun offers EU and US hosting. For GDPR compliance, use the EU region:

const mg = mailgun.client({
  username: 'api',
  key: process.env.MAILGUN_API_KEY,
  url: 'https://api.eu.mailgun.net', // EU region
});

Domains must be configured in the matching region.

Best Practices

  • Use recipient variables for batch personalization — more efficient than individual sends
  • Validate email addresses before adding to mailing lists
  • Use tags on every send for filtering events and analytics
  • Set up custom tracking domains for branded click/open tracking
  • Use Mailgun routes for inbound email processing instead of polling
  • Monitor events dashboard for delivery issues
  • Use dedicated IPs when sending 50K+ emails/day

Anti-Patterns

  • Sending to mailing lists without unsubscribe handling
  • Not checking email validation results before bulk sends
  • Using the sandbox domain in production
  • Ignoring permanent failure webhooks — address will keep bouncing
  • Sending marketing email through transactional infrastructure without separation
  • Hardcoding API keys instead of using environment variables

Install this skill directly: skilldb add email-services-skills

Get CLI access →