Mailgun Email Skill
Complete Mailgun REST API integration for transactional email, templates, delivery tracking, and suppression management.
Quick Start
# Load credentials
source /root/gitrepos/.claude/skills/mailgun-email/.env
# Send plain text email
curl -s --user "api:${MAILGUN_API_KEY}" \
"https://api.mailgun.net/v3/${MAILGUN_DOMAIN}/messages" \
--form-string "from=${MAILGUN_FROM_NAME} <${MAILGUN_FROM_EMAIL}>" \
--form-string "to=recipient@example.com" \
--form-string "subject=Hello from Mailgun" \
--form-string "text=This is a test email."
# Send HTML email
curl -s --user "api:${MAILGUN_API_KEY}" \
"https://api.mailgun.net/v3/${MAILGUN_DOMAIN}/messages" \
--form-string "from=${MAILGUN_FROM_NAME} <${MAILGUN_FROM_EMAIL}>" \
--form-string "to=recipient@example.com" \
--form-string "subject=HTML Email" \
--form-string "html=<html><body><h1>Hello</h1><p>This is HTML content.</p></body></html>"
IMPORTANT: Use
--form-stringinstead of-Ffor text fields containing special characters like<>(e.g., email addresses in "Name " format, HTML content). The-Fflag interprets<as file input which causes failures. Use-Fonly for file attachments (-F attachment=@/path/to/file).
Prerequisites & Authentication
Credentials Location
source /root/gitrepos/.claude/skills/mailgun-email/.env
Environment Variables
The .env file contains:
MAILGUN_API_KEY- API key for authenticationMAILGUN_DOMAIN- Sending domain (e.g.,mg.shakudo.email)MAILGUN_FROM_EMAIL- Default sender emailMAILGUN_FROM_NAME- Default sender name
Authentication
All API calls use HTTP Basic Auth with username api:
curl --user "api:${MAILGUN_API_KEY}" ...
Base URL
- US Region:
https://api.mailgun.net/v3 - EU Region:
https://api.eu.mailgun.net/v3
Shakudo uses US region.
Part 1: Sending Messages
Send Plain Text Email
curl -s --user "api:${MAILGUN_API_KEY}" \
"https://api.mailgun.net/v3/${MAILGUN_DOMAIN}/messages" \
--form-string "from=Sender Name <sender@${MAILGUN_DOMAIN}>" \
--form-string "to=recipient@example.com" \
--form-string "subject=Your Subject Here" \
--form-string "text=Plain text body content here"
Response:
{
"id": "<20260128120000.abc123@mg.shakudo.email>",
"message": "Queued. Thank you."
}
Send HTML Email
curl -s --user "api:${MAILGUN_API_KEY}" \
"https://api.mailgun.net/v3/${MAILGUN_DOMAIN}/messages" \
--form-string "from=Sender Name <sender@${MAILGUN_DOMAIN}>" \
--form-string "to=recipient@example.com" \
--form-string "subject=HTML Email Subject" \
--form-string "html=<html><body><h1>Welcome</h1><p>This is <strong>HTML</strong> content.</p></body></html>"
Send Both Text and HTML (Multipart)
Best practice - include both for email clients that don't render HTML:
curl -s --user "api:${MAILGUN_API_KEY}" \
"https://api.mailgun.net/v3/${MAILGUN_DOMAIN}/messages" \
--form-string "from=Sender <sender@${MAILGUN_DOMAIN}>" \
--form-string "to=recipient@example.com" \
--form-string "subject=Multipart Email" \
--form-string "text=Plain text fallback for non-HTML clients" \
--form-string "html=<html><body><h1>HTML Version</h1></body></html>"
Send with CC and BCC
curl -s --user "api:${MAILGUN_API_KEY}" \
"https://api.mailgun.net/v3/${MAILGUN_DOMAIN}/messages" \
-F from="Sender <sender@${MAILGUN_DOMAIN}>" \
-F to="primary@example.com" \
-F cc="cc1@example.com" \
-F cc="cc2@example.com" \
-F bcc="hidden@example.com" \
-F subject="Email with CC/BCC" \
-F text="This email has CC and BCC recipients."
Send to Multiple Recipients
# Comma-separated in single field
curl -s --user "api:${MAILGUN_API_KEY}" \
"https://api.mailgun.net/v3/${MAILGUN_DOMAIN}/messages" \
-F from="Sender <sender@${MAILGUN_DOMAIN}>" \
-F to="user1@example.com, user2@example.com, user3@example.com" \
-F subject="Bulk Email" \
-F text="Message to multiple recipients."
# Or multiple -F to= flags
curl -s --user "api:${MAILGUN_API_KEY}" \
"https://api.mailgun.net/v3/${MAILGUN_DOMAIN}/messages" \
-F from="Sender <sender@${MAILGUN_DOMAIN}>" \
-F to="user1@example.com" \
-F to="user2@example.com" \
-F subject="Bulk Email" \
-F text="Message to multiple recipients."
Send with Attachments
# Single attachment
curl -s --user "api:${MAILGUN_API_KEY}" \
"https://api.mailgun.net/v3/${MAILGUN_DOMAIN}/messages" \
-F from="Sender <sender@${MAILGUN_DOMAIN}>" \
-F to="recipient@example.com" \
-F subject="Email with Attachment" \
-F text="Please see the attached file." \
-F attachment=@/path/to/report.pdf
# Multiple attachments
curl -s --user "api:${MAILGUN_API_KEY}" \
"https://api.mailgun.net/v3/${MAILGUN_DOMAIN}/messages" \
-F from="Sender <sender@${MAILGUN_DOMAIN}>" \
-F to="recipient@example.com" \
-F subject="Multiple Attachments" \
-F text="Multiple files attached." \
-F attachment=@/path/to/file1.pdf \
-F attachment=@/path/to/file2.xlsx
Send with Inline Images
curl -s --user "api:${MAILGUN_API_KEY}" \
"https://api.mailgun.net/v3/${MAILGUN_DOMAIN}/messages" \
-F from="Sender <sender@${MAILGUN_DOMAIN}>" \
-F to="recipient@example.com" \
-F subject="Email with Inline Image" \
-F html='<html><body><h1>Logo</h1><img src="cid:logo.png"></body></html>' \
-F inline=@/path/to/logo.png
Schedule Email for Later Delivery
# Send at specific time (RFC 2822 format, up to 7 days in future)
curl -s --user "api:${MAILGUN_API_KEY}" \
"https://api.mailgun.net/v3/${MAILGUN_DOMAIN}/messages" \
-F from="Sender <sender@${MAILGUN_DOMAIN}>" \
-F to="recipient@example.com" \
-F subject="Scheduled Email" \
-F text="This was scheduled to arrive later." \
-F o:deliverytime="Fri, 31 Jan 2026 09:00:00 -0500"
# Using Unix timestamp
curl -s --user "api:${MAILGUN_API_KEY}" \
"https://api.mailgun.net/v3/${MAILGUN_DOMAIN}/messages" \
-F from="Sender <sender@${MAILGUN_DOMAIN}>" \
-F to="recipient@example.com" \
-F subject="Scheduled Email" \
-F text="Scheduled message." \
-F o:deliverytime="1767180000"
Send with Tags (for Analytics)
curl -s --user "api:${MAILGUN_API_KEY}" \
"https://api.mailgun.net/v3/${MAILGUN_DOMAIN}/messages" \
-F from="Sender <sender@${MAILGUN_DOMAIN}>" \
-F to="recipient@example.com" \
-F subject="Tagged Email" \
-F text="Email with tags for tracking." \
-F o:tag="campaign-2026-01" \
-F o:tag="newsletter"
Send with Tracking Options
curl -s --user "api:${MAILGUN_API_KEY}" \
"https://api.mailgun.net/v3/${MAILGUN_DOMAIN}/messages" \
-F from="Sender <sender@${MAILGUN_DOMAIN}>" \
-F to="recipient@example.com" \
-F subject="Tracked Email" \
-F html="<html><body><a href='https://example.com'>Click here</a></body></html>" \
-F o:tracking=yes \
-F o:tracking-clicks=yes \
-F o:tracking-opens=yes
Test Mode (Validate Without Sending)
# Process message but don't actually deliver
curl -s --user "api:${MAILGUN_API_KEY}" \
"https://api.mailgun.net/v3/${MAILGUN_DOMAIN}/messages" \
-F from="Sender <sender@${MAILGUN_DOMAIN}>" \
-F to="recipient@example.com" \
-F subject="Test Mode Email" \
-F text="This will be validated but not sent." \
-F o:testmode=yes
Send with Custom Headers
curl -s --user "api:${MAILGUN_API_KEY}" \
"https://api.mailgun.net/v3/${MAILGUN_DOMAIN}/messages" \
-F from="Sender <sender@${MAILGUN_DOMAIN}>" \
-F to="recipient@example.com" \
-F subject="Custom Headers" \
-F text="Email with custom headers." \
-F h:X-Custom-Header="custom-value" \
-F h:Reply-To="reply@example.com"
Send with Recipient Variables (Mail Merge)
curl -s --user "api:${MAILGUN_API_KEY}" \
"https://api.mailgun.net/v3/${MAILGUN_DOMAIN}/messages" \
-F from="Sender <sender@${MAILGUN_DOMAIN}>" \
-F to="alice@example.com, bob@example.com" \
-F subject="Hello %recipient.name%" \
-F text="Dear %recipient.name%, your ID is %recipient.id%." \
-F recipient-variables='{"alice@example.com": {"name": "Alice", "id": "A001"}, "bob@example.com": {"name": "Bob", "id": "B002"}}'
Python Example
import httpx
response = httpx.post(
f"https://api.mailgun.net/v3/{MAILGUN_DOMAIN}/messages",
auth=("api", MAILGUN_API_KEY),
data={
"from": f"{MAILGUN_FROM_NAME} <{MAILGUN_FROM_EMAIL}>",
"to": "recipient@example.com",
"subject": "Hello from Python",
"html": "<html><body><h1>Hello!</h1></body></html>"
}
)
print(response.json())
Sending Error Codes
| Code | Meaning | Fix |
|---|---|---|
| 400 | Bad Request | Check required parameters (from, to, subject, text/html) |
| 401 | Unauthorized | Verify MAILGUN_API_KEY is correct |
| 402 | Payment Required | Account billing issue |
| 404 | Not Found | Verify MAILGUN_DOMAIN is active |
| 413 | Payload Too Large | Attachment exceeds 25MB limit |
| 429 | Too Many Requests | Rate limit hit (default 300/min), implement backoff |
Part 2: Tracking & Events
Get Recent Events
curl -s --user "api:${MAILGUN_API_KEY}" \
"https://api.mailgun.net/v3/${MAILGUN_DOMAIN}/events" | jq '.items[:5]'
Filter Events by Type
# Delivered emails
curl -s --user "api:${MAILGUN_API_KEY}" \
-G "https://api.mailgun.net/v3/${MAILGUN_DOMAIN}/events" \
--data-urlencode "event=delivered" \
--data-urlencode "limit=10"
# Failed/bounced emails
curl -s --user "api:${MAILGUN_API_KEY}" \
-G "https://api.mailgun.net/v3/${MAILGUN_DOMAIN}/events" \
--data-urlencode "event=failed" \
--data-urlencode "limit=10"
# Opens
curl -s --user "api:${MAILGUN_API_KEY}" \
-G "https://api.mailgun.net/v3/${MAILGUN_DOMAIN}/events" \
--data-urlencode "event=opened" \
--data-urlencode "limit=10"
# Clicks
curl -s --user "api:${MAILGUN_API_KEY}" \
-G "https://api.mailgun.net/v3/${MAILGUN_DOMAIN}/events" \
--data-urlencode "event=clicked" \
--data-urlencode "limit=10"
Filter Events by Recipient
curl -s --user "api:${MAILGUN_API_KEY}" \
-G "https://api.mailgun.net/v3/${MAILGUN_DOMAIN}/events" \
--data-urlencode "recipient=user@example.com" \
--data-urlencode "limit=20"
Filter Events by Date Range
# Events in last 24 hours
curl -s --user "api:${MAILGUN_API_KEY}" \
-G "https://api.mailgun.net/v3/${MAILGUN_DOMAIN}/events" \
--data-urlencode "begin=$(date -d '24 hours ago' +%s)" \
--data-urlencode "end=$(date +%s)" \
--data-urlencode "limit=50"
Filter by Message ID
curl -s --user "api:${MAILGUN_API_KEY}" \
-G "https://api.mailgun.net/v3/${MAILGUN_DOMAIN}/events" \
--data-urlencode "message-id=20260128120000.abc123@mg.shakudo.email"
Get Domain Stats
# Total stats
curl -s --user "api:${MAILGUN_API_KEY}" \
"https://api.mailgun.net/v3/${MAILGUN_DOMAIN}/stats/total" | jq '.'
# Stats with filters
curl -s --user "api:${MAILGUN_API_KEY}" \
-G "https://api.mailgun.net/v3/${MAILGUN_DOMAIN}/stats/total" \
--data-urlencode "event=delivered" \
--data-urlencode "event=opened" \
--data-urlencode "event=clicked" \
--data-urlencode "duration=7d"
Event Types Reference
| Event | Description |
|---|---|
accepted |
Message accepted by Mailgun |
delivered |
Message delivered to recipient's server |
failed |
Message could not be delivered (permanent) |
rejected |
Message rejected by Mailgun |
opened |
Recipient opened the email (requires tracking) |
clicked |
Recipient clicked a link (requires tracking) |
unsubscribed |
Recipient unsubscribed |
complained |
Recipient marked as spam |
stored |
Message stored for retrieval |
Part 3: Templates
Create a Template
curl -s --user "api:${MAILGUN_API_KEY}" \
"https://api.mailgun.net/v3/${MAILGUN_DOMAIN}/templates" \
-F name="welcome-email" \
-F description="Welcome email for new users" \
-F template='<html><body><h1>Welcome, {{name}}!</h1><p>Thanks for joining us.</p></body></html>'
List Templates
curl -s --user "api:${MAILGUN_API_KEY}" \
"https://api.mailgun.net/v3/${MAILGUN_DOMAIN}/templates" | jq '.items[] | {name, description}'
Get Template Details
curl -s --user "api:${MAILGUN_API_KEY}" \
"https://api.mailgun.net/v3/${MAILGUN_DOMAIN}/templates/welcome-email" | jq '.'
Update a Template
curl -s --user "api:${MAILGUN_API_KEY}" \
-X PUT "https://api.mailgun.net/v3/${MAILGUN_DOMAIN}/templates/welcome-email" \
-F description="Updated welcome email" \
-F template='<html><body><h1>Welcome, {{name}}!</h1><p>We are glad to have you.</p></body></html>'
Delete a Template
curl -s --user "api:${MAILGUN_API_KEY}" \
-X DELETE "https://api.mailgun.net/v3/${MAILGUN_DOMAIN}/templates/welcome-email"
Send Using a Template
curl -s --user "api:${MAILGUN_API_KEY}" \
"https://api.mailgun.net/v3/${MAILGUN_DOMAIN}/messages" \
-F from="Sender <sender@${MAILGUN_DOMAIN}>" \
-F to="recipient@example.com" \
-F subject="Welcome to Our Service" \
-F template="welcome-email" \
-F h:X-Mailgun-Variables='{"name": "John Doe"}'
Template Variables (Handlebars Syntax)
Templates use Handlebars syntax:
{{variable}}- Simple variable{{#if condition}}...{{/if}}- Conditionals{{#each items}}...{{/each}}- Loops
Part 4: Suppressions Management
Suppressions are addresses that should not receive emails (bounces, unsubscribes, complaints).
List Bounces
curl -s --user "api:${MAILGUN_API_KEY}" \
"https://api.mailgun.net/v3/${MAILGUN_DOMAIN}/bounces" | jq '.items'
Get Bounce for Specific Address
curl -s --user "api:${MAILGUN_API_KEY}" \
"https://api.mailgun.net/v3/${MAILGUN_DOMAIN}/bounces/bounced@example.com"
Add a Bounce (Manual Suppression)
curl -s --user "api:${MAILGUN_API_KEY}" \
"https://api.mailgun.net/v3/${MAILGUN_DOMAIN}/bounces" \
-F address="bad-address@example.com" \
-F code="550" \
-F error="Mailbox not found"
Delete a Bounce (Re-enable Sending)
curl -s --user "api:${MAILGUN_API_KEY}" \
-X DELETE "https://api.mailgun.net/v3/${MAILGUN_DOMAIN}/bounces/bad-address@example.com"
List Unsubscribes
curl -s --user "api:${MAILGUN_API_KEY}" \
"https://api.mailgun.net/v3/${MAILGUN_DOMAIN}/unsubscribes" | jq '.items'
Add Unsubscribe
curl -s --user "api:${MAILGUN_API_KEY}" \
"https://api.mailgun.net/v3/${MAILGUN_DOMAIN}/unsubscribes" \
-F address="user@example.com" \
-F tag="newsletter"
Delete Unsubscribe
curl -s --user "api:${MAILGUN_API_KEY}" \
-X DELETE "https://api.mailgun.net/v3/${MAILGUN_DOMAIN}/unsubscribes/user@example.com"
List Complaints (Spam Reports)
curl -s --user "api:${MAILGUN_API_KEY}" \
"https://api.mailgun.net/v3/${MAILGUN_DOMAIN}/complaints" | jq '.items'
Add Complaint
curl -s --user "api:${MAILGUN_API_KEY}" \
"https://api.mailgun.net/v3/${MAILGUN_DOMAIN}/complaints" \
-F address="complainer@example.com"
Delete Complaint
curl -s --user "api:${MAILGUN_API_KEY}" \
-X DELETE "https://api.mailgun.net/v3/${MAILGUN_DOMAIN}/complaints/complainer@example.com"
Part 5: Domain Management
List Domains
curl -s --user "api:${MAILGUN_API_KEY}" \
"https://api.mailgun.net/v3/domains" | jq '.items[] | {name, state, type}'
Get Domain Details
curl -s --user "api:${MAILGUN_API_KEY}" \
"https://api.mailgun.net/v3/domains/${MAILGUN_DOMAIN}" | jq '.'
Verify Domain DNS
curl -s --user "api:${MAILGUN_API_KEY}" \
-X PUT "https://api.mailgun.net/v3/domains/${MAILGUN_DOMAIN}/verify"
Get Domain Credentials
curl -s --user "api:${MAILGUN_API_KEY}" \
"https://api.mailgun.net/v3/domains/${MAILGUN_DOMAIN}/credentials"
Part 6: Mailing Lists
Create a Mailing List
curl -s --user "api:${MAILGUN_API_KEY}" \
"https://api.mailgun.net/v3/lists" \
-F address="newsletter@${MAILGUN_DOMAIN}" \
-F name="Newsletter Subscribers" \
-F description="Monthly newsletter list"
List All Mailing Lists
curl -s --user "api:${MAILGUN_API_KEY}" \
"https://api.mailgun.net/v3/lists" | jq '.items[] | {address, name, members_count}'
Add Member to List
curl -s --user "api:${MAILGUN_API_KEY}" \
"https://api.mailgun.net/v3/lists/newsletter@${MAILGUN_DOMAIN}/members" \
-F subscribed=true \
-F address="subscriber@example.com" \
-F name="John Doe" \
-F vars='{"age": 30, "city": "NYC"}'
Bulk Add Members
curl -s --user "api:${MAILGUN_API_KEY}" \
"https://api.mailgun.net/v3/lists/newsletter@${MAILGUN_DOMAIN}/members.json" \
-F members='[{"address":"user1@example.com","name":"User One"},{"address":"user2@example.com","name":"User Two"}]' \
-F upsert=true
List Members
curl -s --user "api:${MAILGUN_API_KEY}" \
"https://api.mailgun.net/v3/lists/newsletter@${MAILGUN_DOMAIN}/members?limit=100"
Remove Member
curl -s --user "api:${MAILGUN_API_KEY}" \
-X DELETE "https://api.mailgun.net/v3/lists/newsletter@${MAILGUN_DOMAIN}/members/subscriber@example.com"
Send to Mailing List
curl -s --user "api:${MAILGUN_API_KEY}" \
"https://api.mailgun.net/v3/${MAILGUN_DOMAIN}/messages" \
-F from="Newsletter <newsletter@${MAILGUN_DOMAIN}>" \
-F to="newsletter@${MAILGUN_DOMAIN}" \
-F subject="Monthly Newsletter" \
-F html="<h1>Hello %recipient.name%!</h1><p>Your personalized content here.</p>"
Delete Mailing List
curl -s --user "api:${MAILGUN_API_KEY}" \
-X DELETE "https://api.mailgun.net/v3/lists/newsletter@${MAILGUN_DOMAIN}"
Part 7: Webhooks
Create Webhook
curl -s --user "api:${MAILGUN_API_KEY}" \
"https://api.mailgun.net/v3/domains/${MAILGUN_DOMAIN}/webhooks" \
-F id="delivered" \
-F url="https://your-app.com/webhooks/mailgun/delivered"
List Webhooks
curl -s --user "api:${MAILGUN_API_KEY}" \
"https://api.mailgun.net/v3/domains/${MAILGUN_DOMAIN}/webhooks" | jq '.'
Update Webhook
curl -s --user "api:${MAILGUN_API_KEY}" \
-X PUT "https://api.mailgun.net/v3/domains/${MAILGUN_DOMAIN}/webhooks/delivered" \
-F url="https://your-app.com/webhooks/new-endpoint"
Delete Webhook
curl -s --user "api:${MAILGUN_API_KEY}" \
-X DELETE "https://api.mailgun.net/v3/domains/${MAILGUN_DOMAIN}/webhooks/delivered"
Webhook Event Types
| Event | Description |
|---|---|
accepted |
Message accepted by Mailgun |
delivered |
Message delivered successfully |
failed |
Delivery failed (permanent or temporary) |
opened |
Recipient opened email |
clicked |
Recipient clicked link |
unsubscribed |
Recipient unsubscribed |
complained |
Marked as spam |
Verify Webhook Signature (Python)
import hmac
import hashlib
def verify_mailgun_webhook(timestamp, token, signature, api_key):
"""Verify Mailgun webhook signature"""
message = f"{timestamp}{token}".encode('utf-8')
computed = hmac.new(
key=api_key.encode('utf-8'),
msg=message,
digestmod=hashlib.sha256
).hexdigest()
return hmac.compare_digest(computed, signature)
Quick Reference
Endpoints Summary
| Action | Method | Endpoint |
|---|---|---|
| Send Message | POST | /{domain}/messages |
| Get Events | GET | /{domain}/events |
| Get Stats | GET | /{domain}/stats/total |
| List Templates | GET | /{domain}/templates |
| Create Template | POST | /{domain}/templates |
| Delete Template | DELETE | /{domain}/templates/{name} |
| List Bounces | GET | /{domain}/bounces |
| Delete Bounce | DELETE | /{domain}/bounces/{address} |
| List Unsubscribes | GET | /{domain}/unsubscribes |
| List Complaints | GET | /{domain}/complaints |
| List Domains | GET | /domains |
| Get Domain | GET | /domains/{domain} |
| List Mailing Lists | GET | /lists |
| Create Mailing List | POST | /lists |
| List Members | GET | /lists/{address}/members |
| Add Member | POST | /lists/{address}/members |
| List Webhooks | GET | /domains/{domain}/webhooks |
| Create Webhook | POST | /domains/{domain}/webhooks |
Message Parameters
| Parameter | Required | Description |
|---|---|---|
from |
Yes | Sender address (must match domain) |
to |
Yes | Recipient(s), comma-separated or multiple fields |
subject |
Yes | Email subject line |
text |
One of | Plain text body |
html |
One of | HTML body |
cc |
No | Carbon copy recipients |
bcc |
No | Blind carbon copy recipients |
attachment |
No | File attachment(s) |
inline |
No | Inline image(s) for HTML |
o:tag |
No | Tag(s) for analytics |
o:deliverytime |
No | Scheduled delivery time |
o:tracking |
No | Enable tracking (yes/no) |
o:tracking-clicks |
No | Track clicks (yes/no/htmlonly) |
o:tracking-opens |
No | Track opens (yes/no) |
h:X-Custom-Header |
No | Custom headers (h: prefix) |
recipient-variables |
No | JSON for mail merge |
template |
No | Template name to use |
o:testmode |
No | Validate without sending (yes/no) |
Rate Limits
| Limit Type | Default |
|---|---|
| Messages per minute | 300 |
| Messages per day | Varies by plan |
| API calls per minute | 100 |
Best Practices
- Always include both text and HTML for better deliverability
- Use tags for campaign tracking and analytics
- Check suppressions before bulk sends to avoid bounces
- Handle 429 errors with exponential backoff
- Validate email addresses before sending
- Use templates for consistent branding