Skip to content
Back to skills

Mailgun

ASecurity

Mailgun 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.

  • 142 stars
  • 0 votes
  • 0 copies
  • 2 views
  • Added May 29, 2026
developmenttypescriptgobashnodeexpressapidocumentation

Works with

  • terminal
  • cli
  • api

Security analysis

A96/100
  • mediumInstalls packages at runtime which could introduce malicious dependencies

Pro scans all 2 files and shows the line behind each finding

Scanned October 4, 2026

npx -y skills add TerminalSkills/skills --skill mailgun --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Mailgun?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Mailgun
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/terminalskills-mailgun/badge)](https://www.skillsdirectory.com/skills/terminalskills-mailgun)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: mailgun
description: >-
  Mailgun 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.
license: Apache-2.0
compatibility: 'Any language (REST API); official Node SDK mailgun.js needs Node 18+'
metadata:
  author: terminal-skills
  version: 1.1.0
  category: business
  tags:
    - mailgun
    - email
    - api
    - bulk
    - marketing
---

# Mailgun

## Overview

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).

## Instructions

### Step 1: Prepare a domain

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`.

### Step 2: Send with mailgun.js

```bash
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.

```typescript
// 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.

### Step 3: Batch sending

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%`.

```typescript
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 },
  }),
})
```

### Step 4: Validate addresses

```typescript
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.

### Step 5: Inbound routes

```typescript
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.

### Step 6: Webhooks

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.

```typescript
// 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 }
}
```

## Examples

### Example 1: Order-shipped email from a Node app

**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.

### Example 2: Stop mailing bounced and complaining users

**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.

## Guidelines

- Match the API base URL to the account region; a US key against the EU endpoint fails authentication.
- Verify every webhook with the signing key, reject stale timestamps and reuse of a token, and compare in constant time.
- Suppress permanent bounces and complaints; Mailgun also keeps its own suppression lists.
- Tracking pixels (`o:tracking-opens`) are unreliable because mail clients preload images; use clicks and deliveries for decisions.
- Use `o:testmode: 'yes'` to exercise the API without delivering mail.
- The free plan is limited (100 emails per day at last check) and plans and prices change; read mailgun.com/pricing instead of hard-coding limits.
- Never put the private API key in front-end code or the repository.

Files in this skill

  • SKILL.md3.3 KB
  • _scores.json2 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…