Mailgun Connector
Connect Mailgun to Brevo through Tajo to unify your transactional and marketing email data, sync delivery events and engagement metrics, and consolidate your email infrastructure into a single customer view.
Overview
| Property | Value |
|---|---|
| Platform | Mailgun (by Sinch) |
| Category | Email Marketing |
| Setup Complexity | Easy |
| Official Integration | No |
| Data Synced | Events, Contacts, Deliverability, Campaigns |
| Auth Method | API Key (HTTP Basic Auth) |
Features
- Delivery event sync - Track delivered, bounced, opened, and clicked events
- Engagement metrics - Sync open and click rates to Brevo contact attributes
- Bounce management - Automatically suppress bounced addresses in Brevo
- Complaint handling - Sync spam complaints for list hygiene
- Domain reputation - Monitor sending domain health and deliverability
- Transactional email tracking - Correlate transactional sends with marketing data
Prerequisites
Before you begin, ensure you have:
- A Mailgun account with a verified sending domain
- A Mailgun API key from the Mailgun Dashboard
- A Brevo account with API access
- A Tajo account with connector permissions
Authentication
API Key Authentication
Mailgun uses HTTP Basic Authentication with api as the username and your API key as the password:
# Get your API key from https://app.mailgun.com/settings/api_securityexport MAILGUN_API_KEY=key-your-api-keyexport MAILGUN_DOMAIN=your-domain.comexport TAJO_API_KEY=your_tajo_api_keyexport BREVO_API_KEY=your_brevo_api_key// HTTP Basic Auth formatconst headers = { 'Authorization': `Basic ${Buffer.from( `api:${process.env.MAILGUN_API_KEY}` ).toString('base64')}`};
// Or using curl// curl -s --user 'api:YOUR_API_KEY' ...API Key Types
Mailgun provides domain-specific sending keys and account-level API keys. Use domain sending keys for message operations and the account API key for management operations.
Configuration
Basic Setup
connectors: mailgun: enabled: true api_key: "${MAILGUN_API_KEY}" domain: "${MAILGUN_DOMAIN}" region: "us" # or "eu" for EU region
sync: events: true contacts: true bounces: true complaints: true schedule: "*/15 * * * *" # Every 15 minutes
webhook: signing_key: "${MAILGUN_WEBHOOK_SIGNING_KEY}"
lists: engaged: 30 bounced: 31 complained: 32Field Mapping
field_mapping: email: email first_name: FIRSTNAME last_name: LASTNAME open_rate: MG_OPEN_RATE click_rate: MG_CLICK_RATE last_delivered: MG_LAST_DELIVERED bounce_type: MG_BOUNCE_TYPE engagement_score: MG_ENGAGEMENT unsubscribed: MG_UNSUBSCRIBEDAPI Endpoints
| Endpoint | Method | Description |
|---|---|---|
https://api.mailgun.net/v3/{domain}/messages | POST | Send email messages |
https://api.mailgun.net/v3/{domain}/events | GET | Query event logs |
https://api.mailgun.net/v3/{domain}/bounces | GET | List bounces |
https://api.mailgun.net/v3/{domain}/complaints | GET | List complaints |
https://api.mailgun.net/v3/{domain}/unsubscribes | GET | List unsubscribes |
https://api.mailgun.net/v3/{domain}/tags | GET | List tags |
https://api.mailgun.net/v3/{domain}/tags/{tag}/stats | GET | Get tag statistics |
https://api.mailgun.net/v3/lists | GET | List mailing lists |
https://api.mailgun.net/v3/domains | GET | List domains |
https://api.mailgun.net/v4/address/validate | POST | Validate email address |
EU Region
For EU-based Mailgun accounts, use https://api.eu.mailgun.net instead of https://api.mailgun.net for all API endpoints.
Code Examples
Initialize Connector
import { TajoClient } from '@tajo/sdk';
const tajo = new TajoClient({ apiKey: process.env.TAJO_API_KEY, brevoApiKey: process.env.BREVO_API_KEY});
await tajo.connectors.connect('mailgun', { apiKey: process.env.MAILGUN_API_KEY, domain: process.env.MAILGUN_DOMAIN, region: 'us'});Send a Message via Mailgun API
// Send an email using Mailgun's Messages APIconst formData = new URLSearchParams();formData.append('from', `Your App <noreply@${domain}>`);formData.append('subject', 'Welcome to our platform');formData.append('html', '<h1>Welcome!</h1><p>Thanks for signing up.</p>');formData.append('o:tag', 'welcome-email');formData.append('o:tracking', 'yes');
const response = await fetch( `https://api.mailgun.net/v3/${domain}/messages`, { method: 'POST', headers: { 'Authorization': `Basic ${Buffer.from(`api:${apiKey}`).toString('base64')}` }, body: formData });
const result = await response.json();// { id: '<[email protected]>', message: 'Queued. Thank you.' }Sync Email Events to Brevo
// Query Mailgun events and sync engagement dataconst eventsResponse = await fetch( `https://api.mailgun.net/v3/${domain}/events?` + new URLSearchParams({ begin: lastSyncDate, ascending: 'yes', limit: 300, event: 'delivered OR opened OR clicked' }), { headers: { 'Authorization': `Basic ${Buffer.from(`api:${apiKey}`).toString('base64')}` } });
const { items, paging } = await eventsResponse.json();
for (const event of items) { const email = event.recipient;
switch (event.event) { case 'delivered': await tajo.contacts.update(email, { attributes: { MG_LAST_DELIVERED: event.timestamp } }); break; case 'opened': await tajo.events.track({ email, event: 'email_opened', properties: { subject: event.message.headers.subject } }); break; case 'clicked': await tajo.events.track({ email, event: 'email_clicked', properties: { url: event.url } }); break; }}
// Follow pagination for more eventsif (paging.next) { // Fetch next page using paging.next URL}Handle Mailgun Webhooks
const crypto = require('crypto');
app.post('/webhooks/mailgun', async (req, res) => { // Verify webhook signature const { timestamp, token, signature } = req.body.signature; const encodedToken = crypto .createHmac('sha256', process.env.MAILGUN_WEBHOOK_SIGNING_KEY) .update(timestamp.concat(token)) .digest('hex');
if (encodedToken !== signature) { return res.status(401).send('Unauthorized'); }
const eventData = req.body['event-data']; const event = eventData.event; const email = eventData.recipient;
await tajo.connectors.handleWebhook('mailgun', { topic: event, payload: eventData });
// Handle bounce suppression if (event === 'failed' && eventData.severity === 'permanent') { await tajo.contacts.update(email, { attributes: { MG_BOUNCE_TYPE: 'hard_bounce' }, emailBlacklisted: true }); }
res.status(200).send('OK');});Sync Bounces and Complaints
// Sync bounced addresses for list hygieneconst bouncesResponse = await fetch( `https://api.mailgun.net/v3/${domain}/bounces?limit=100`, { headers: { 'Authorization': `Basic ${Buffer.from(`api:${apiKey}`).toString('base64')}` } });
const { items: bounces } = await bouncesResponse.json();
for (const bounce of bounces) { await tajo.contacts.update(bounce.address, { attributes: { MG_BOUNCE_TYPE: bounce.error.includes('550') ? 'hard_bounce' : 'soft_bounce', MG_BOUNCE_DATE: bounce.created_at }, emailBlacklisted: bounce.error.includes('550') });}Rate Limits
| Endpoint | Limit | Notes |
|---|---|---|
| Messages API | Varies by plan | 100/hr (free), unlimited (paid) |
| Events API | No explicit limit | Use pagination with 300 items max |
| Validation API | Based on plan | Pay-per-validation |
| Webhooks | Real-time | No rate limit on delivery |
| Suppressions API | No explicit limit | Standard rate limiting applies |
Sending Limits
Mailgun enforces sending limits based on your plan and domain reputation. New domains start with lower limits that increase as your sender reputation improves. Monitor your domain stats in the Mailgun dashboard.
Troubleshooting
| Issue | Cause | Solution |
|---|---|---|
| 401 Unauthorized | Invalid API key | Verify API key in Mailgun dashboard |
| Domain not verified | DNS records missing | Add required TXT, CNAME, MX records |
| Webhook not received | URL not accessible | Ensure webhook URL is publicly reachable |
| Events missing | Time range too narrow | Expand begin/end parameters |
| Low deliverability | Domain reputation | Check domain stats and authentication |
Debug Mode
connectors: mailgun: debug: true log_level: verbose log_webhooks: true log_events: trueBest Practices
- Verify sending domains - Complete DNS verification for optimal deliverability
- Use webhooks for events - Real-time webhook delivery vs. polling the Events API
- Handle bounces proactively - Suppress hard bounces immediately in Brevo
- Tag your messages - Use tags to categorize and analyze email performance
- Monitor domain reputation - Track deliverability metrics in Mailgun dashboard
- Use email validation - Validate addresses before adding to Brevo lists
Security
- HTTP Basic Auth - API key transmitted via Authorization header
- Webhook signatures - HMAC-SHA256 signature verification
- Domain verification - SPF, DKIM, and DMARC DNS authentication
- IP whitelisting - Available for dedicated IP plans
- TLS encryption - All API endpoints require HTTPS
- Key rotation - Rotate API keys periodically via Mailgun dashboard
Related Resources
Open-Source Implementation Map
This section is derived from official or public repository material discovered for the Mailgun connector. Use it as the engineering companion to the setup guide above: it shows where the API surface lives, what implementation assets exist, and how Tajo should translate them into reliable Brevo sync behavior.
Repository Snapshot
| Repository | Commit | Languages / formats | Files |
|---|---|---|---|
| mailgun/mailgun-mcp-server | 3495e24 | TypeScript (10), JSON (5), Markdown (4), YAML (1), gitignore (1), npmignore (1) | 26 |
Integration Shape
graph LR Source["Mailgun API / repository"] --> Auth["Auth and scopes"] Source --> Objects["Objects, events, and schemas"] Auth --> Tajo["Tajo connector runtime"] Objects --> Tajo Tajo --> Brevo["Brevo contacts, attributes, lists, campaigns"] Tajo --> Ops["Backfill, cursor, retries, logs"]What To Reuse
- Mailgun MCP Server
- Overview
- Capabilities
-
- Messaging — Send emails, retrieve stored messages, resend messages
-
- Domains — View domain details, verify DNS configuration, manage tracking settings (click, open, unsubscribe)
Tajo Revamp Checklist
- Keep authentication setup aligned with the vendor docs and the public repository’s current API shape.
- Map primary resources into explicit Tajo sync objects with stable external IDs.
- Prefer cursor-based or updated-at incremental sync where the API exposes it; otherwise document the fallback.
- Treat webhook handlers as idempotent and replay-safe, especially for order, contact, ticket, and campaign events.
- Capture pagination, rate limits, retry headers, and partial-failure behavior in connector smoke tests.
- Keep examples small and runnable against sandbox or test-mode accounts.