To integrate Replit with Mailgun, verify your sending domain in Mailgun, store your API key and domain in Replit Secrets (lock icon π), then use the Mailgun REST API or SDK to send transactional emails from your Python or Node.js server. Deploy on Autoscale to receive delivery webhooks.
Why Use Mailgun with Replit?
Transactional emails β password resets, order confirmations, account notifications, and alerts β are a core feature of almost every web application. Mailgun provides a reliable REST API for sending these emails with built-in bounce handling, spam protection, and delivery analytics. Unlike consumer email providers, Mailgun is designed specifically for programmatic sending at scale, with pay-per-email pricing that makes it economical for apps at any stage.
Mailgun's REST API is one of the simplest transactional email APIs to use. You authenticate with an API key and send a POST request with your recipient, subject, and body. Mailgun handles the SMTP infrastructure, ISP relationships, and bounce processing on your behalf. The sandbox domain lets you test sending immediately without any DNS setup, while verified custom domains are required for production emails that reach real users' inboxes.
The combination of Mailgun with Replit is particularly useful for apps that need email as a feature β user authentication flows, automated reports, customer notifications. Replit's Autoscale deployment is well-suited for Mailgun webhook receivers that process delivery events asynchronously, updating your database when emails are delivered, bounced, or marked as spam. With Replit Secrets securing your API key and a straightforward REST integration, you can add production email sending to your app in under 30 minutes.
Integration method
Replit connects to Mailgun via its REST API using an API key stored in Replit Secrets. Your server-side Express or Flask app sends HTTP requests to the Mailgun API to deliver transactional emails. For tracking delivery events like opens, clicks, and bounces, you register a Mailgun webhook pointing to your deployed Replit URL.
Prerequisites
- A Mailgun account (free sandbox available, paid plan needed for custom domain)
- A domain you control for sending production emails (optional for sandbox testing)
- A Replit account with a Node.js or Python Repl created
- Basic knowledge of HTTP requests and environment variables
Step-by-step guide
Set up your Mailgun domain and get your API key
Set up your Mailgun domain and get your API key
Log into your Mailgun account at app.mailgun.com. For initial testing, Mailgun provides a sandbox domain (something like sandbox{hash}.mailgun.org) that you can use immediately β you can only send to pre-verified email addresses on the sandbox, but it's perfect for development. For production sending to real users, go to Sending > Domains and click 'Add New Domain'. Enter your domain (e.g., mail.yourdomain.com β a subdomain is recommended to protect your root domain's email reputation). Mailgun will provide you with DNS records to add: typically two TXT records for SPF and DKIM, and optionally a CNAME for click/open tracking. Add these records at your DNS provider and wait for Mailgun to verify them β this can take anywhere from a few minutes to 24 hours. While waiting for DNS, you can use the sandbox for development. To get your API key, go to Settings > API Keys and copy the 'Private API key'. Also note your sending domain name. In the US, API requests go to api.mailgun.net; if your account is on the EU region, use api.eu.mailgun.net.
Pro tip: Use a subdomain like mail.yourdomain.com rather than your root domain for sending. This protects your root domain's email reputation if your sending domain ever gets flagged.
Expected result: Mailgun account created with either a sandbox domain available for testing or a custom domain with DNS records submitted for verification.
Store Mailgun credentials in Replit Secrets
Store Mailgun credentials in Replit Secrets
Open your Repl and click the lock icon (π) in the left sidebar to open the Secrets pane. You need to add two secrets: your API key and your sending domain. Click 'New Secret', enter MAILGUN_API_KEY as the key, and paste your private API key as the value. Click 'Add Secret'. Then add a second secret: MAILGUN_DOMAIN with your sending domain (e.g., mail.yourdomain.com or your sandbox domain) as the value. Having the domain as a secret makes it easy to switch between sandbox and production without changing your code. If your Mailgun account is on the EU region, also add MAILGUN_BASE_URL with value https://api.eu.mailgun.net/v3 β this way you can configure the endpoint via Secrets rather than hardcoding the region. Replit Secrets are AES-256 encrypted and injected as environment variables at runtime β they are never stored in code files or visible to other users who view your Repl.
1# Python β verify Mailgun secrets are loaded2import os34required_secrets = ["MAILGUN_API_KEY", "MAILGUN_DOMAIN"]5for secret in required_secrets:6 value = os.environ.get(secret)7 if not value:8 raise EnvironmentError(f"{secret} not found in Replit Secrets. Add it via the lock icon in the sidebar.")9 print(f"{secret}: loaded ({len(value)} chars)")1011print("All Mailgun secrets verified.")Expected result: Both MAILGUN_API_KEY and MAILGUN_DOMAIN appear in Replit Secrets. The verification script confirms both are loaded without errors.
Send your first email using the Mailgun REST API
Send your first email using the Mailgun REST API
Mailgun's REST API accepts form-encoded POST requests to https://api.mailgun.net/v3/{domain}/messages. The required fields are 'from' (must be an address on your sending domain), 'to' (recipient), 'subject', and either 'text' or 'html' for the body. Authentication uses HTTP Basic auth with the username 'api' and your API key as the password. In Node.js, you can use the form-data package to construct the multipart form body, or use the official mailgun.js SDK. In Python, the requests library handles basic auth natively with a tuple (username, password). The Mailgun SDK abstracts these details but adds a dependency β for simple sending, direct HTTP calls are straightforward and transparent. Always include both text and html body versions β many email clients display plain text as a fallback, and Mailgun's deliverability is slightly better when both are provided.
1// Node.js β Send email via Mailgun REST API (mailgun.js)2const MAILGUN_API_KEY = process.env.MAILGUN_API_KEY;3const MAILGUN_DOMAIN = process.env.MAILGUN_DOMAIN;4const MAILGUN_BASE_URL = process.env.MAILGUN_BASE_URL || 'https://api.mailgun.net/v3';56async function sendEmail({ to, subject, text, html, from }) {7 const sender = from || `noreply@${MAILGUN_DOMAIN}`;89 const formData = new URLSearchParams();10 formData.append('from', sender);11 formData.append('to', to);12 formData.append('subject', subject);13 if (text) formData.append('text', text);14 if (html) formData.append('html', html);1516 const credentials = Buffer.from(`api:${MAILGUN_API_KEY}`).toString('base64');1718 const response = await fetch(`${MAILGUN_BASE_URL}/${MAILGUN_DOMAIN}/messages`, {19 method: 'POST',20 headers: {21 'Authorization': `Basic ${credentials}`,22 'Content-Type': 'application/x-www-form-urlencoded'23 },24 body: formData.toString()25 });2627 if (!response.ok) {28 const error = await response.text();29 throw new Error(`Mailgun error ${response.status}: ${error}`);30 }3132 return response.json();33}3435module.exports = { sendEmail };Pro tip: On the Mailgun sandbox domain, you can only send to email addresses that are explicitly added as 'Authorized Recipients' in your Mailgun dashboard. Add your own email address to test.
Expected result: The sendEmail function sends a POST to Mailgun and returns a response with a message ID. The email appears in your inbox (or the authorized recipient's inbox on sandbox).
Build an Express server with email sending endpoint
Build an Express server with email sending endpoint
With the Mailgun helper in place, build a proper Express server that accepts email send requests from your application. The server should validate required fields, sanitize inputs, and return clear error messages. For production apps, add rate limiting to the email endpoint to prevent abuse β users shouldn't be able to trigger unlimited email sends. You'll also want to log each send attempt with the recipient, timestamp, and Mailgun message ID so you have an audit trail. If you're using Mailgun templates (created in the Mailgun dashboard under Sending > Templates), use the 'template' form field instead of 'html' and pass variable values using 'h:X-Mailgun-Variables' or by appending template variable fields. Templates support Handlebars syntax for variable substitution.
1// Node.js β Express email API server (server.js)2const express = require('express');3const { sendEmail } = require('./mailgun');4const app = express();5app.use(express.json());67app.post('/send-email', async (req, res) => {8 const { to, subject, text, html } = req.body;910 if (!to || !subject || (!text && !html)) {11 return res.status(400).json({12 error: 'Required fields: to, subject, and text or html'13 });14 }1516 // Basic email validation17 const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;18 if (!emailRegex.test(to)) {19 return res.status(400).json({ error: 'Invalid email address' });20 }2122 try {23 const result = await sendEmail({ to, subject, text, html });24 console.log(`Email sent to ${to}, message ID: ${result.id}`);25 res.json({ success: true, messageId: result.id, message: result.message });26 } catch (err) {27 console.error('Email send failed:', err.message);28 res.status(500).json({ error: 'Failed to send email', details: err.message });29 }30});3132app.listen(3000, '0.0.0.0', () => console.log('Email server running on port 3000'));Expected result: POST /send-email with valid to, subject, and text fields sends the email via Mailgun and returns the Mailgun message ID.
Receive Mailgun delivery webhooks
Receive Mailgun delivery webhooks
Mailgun can notify your Replit app when emails are delivered, bounced, opened, or clicked. This is essential for tracking email deliverability and cleaning your email list of bad addresses. To set up webhooks, your app must be deployed β webhook calls go to a stable URL, not the development editor URL which sleeps. Deploy your app using Replit's Deploy button (Autoscale is appropriate for webhooks since it handles intermittent traffic). Copy your deployment URL (e.g., https://your-app.your-username.replit.app). In Mailgun, go to Sending > Webhooks and add a new webhook for the event types you want (delivered, bounced, complained, opened, clicked). Enter your Replit URL plus the webhook path, for example https://your-app.replit.app/mailgun-webhook. Mailgun signs webhook requests with an HMAC-SHA256 signature using your webhook signing key (found in Settings > Webhooks). Always verify this signature before processing webhook data β it prevents spoofed events from triggering your logic.
1// Node.js β Mailgun webhook receiver (add to server.js)2const crypto = require('crypto');34const MAILGUN_WEBHOOK_KEY = process.env.MAILGUN_WEBHOOK_KEY;56function verifyMailgunSignature(timestamp, token, signature) {7 if (!MAILGUN_WEBHOOK_KEY) return true; // Skip in dev mode8 const value = timestamp + token;9 const hash = crypto10 .createHmac('sha256', MAILGUN_WEBHOOK_KEY)11 .update(value)12 .digest('hex');13 return hash === signature;14}1516app.post('/mailgun-webhook', express.json(), (req, res) => {17 const { signature, 'event-data': eventData } = req.body;1819 if (!verifyMailgunSignature(20 signature.timestamp,21 signature.token,22 signature.signature23 )) {24 return res.status(401).json({ error: 'Invalid webhook signature' });25 }2627 const { event, recipient, message } = eventData;28 console.log(`Email event: ${event}, recipient: ${recipient}`);2930 // Handle specific events31 switch (event) {32 case 'delivered':33 console.log(`Delivered to ${recipient}, message ID: ${message.headers['message-id']}`);34 break;35 case 'bounced':36 console.log(`Bounced for ${recipient}: ${eventData['delivery-status']?.description}`);37 break;38 case 'complained':39 console.log(`Spam complaint from ${recipient}`);40 break;41 }4243 res.json({ received: true });44});Pro tip: Store your Mailgun webhook signing key as MAILGUN_WEBHOOK_KEY in Replit Secrets. It's different from your API key β find it in Mailgun Settings > Webhooks.
Expected result: Mailgun sends webhook events to your deployed Replit URL and your server logs the event type and recipient for each delivery notification.
Python alternative: Flask with requests
Python alternative: Flask with requests
The Python implementation uses the requests library with HTTP Basic authentication to call the Mailgun REST API. Requests handles the auth tuple natively β pass ('api', your_api_key) as the auth parameter and a dictionary as the data parameter for the form fields. For HTML email templates, you can use Python's built-in string formatting or a template library like Jinja2 to compose the HTML body. Flask's before_request hook can enforce simple API key authentication on your email endpoint to prevent unauthorized use. The mailgun-python package is also available on PyPI but is not officially maintained by Mailgun β the direct requests approach is more reliable and easier to debug.
1# Python β Flask Mailgun integration (app.py)2import os3import requests4from flask import Flask, request, jsonify56app = Flask(__name__)78MAILGUN_API_KEY = os.environ["MAILGUN_API_KEY"]9MAILGUN_DOMAIN = os.environ["MAILGUN_DOMAIN"]10MAILGUN_BASE_URL = os.environ.get("MAILGUN_BASE_URL", "https://api.mailgun.net/v3")1112def send_email(to, subject, text=None, html=None, sender=None):13 if not sender:14 sender = f"noreply@{MAILGUN_DOMAIN}"15 payload = {16 "from": sender,17 "to": to,18 "subject": subject19 }20 if text:21 payload["text"] = text22 if html:23 payload["html"] = html2425 response = requests.post(26 f"{MAILGUN_BASE_URL}/{MAILGUN_DOMAIN}/messages",27 auth=("api", MAILGUN_API_KEY),28 data=payload,29 timeout=1030 )31 response.raise_for_status()32 return response.json()3334@app.route("/send-email", methods=["POST"])35def api_send_email():36 data = request.get_json()37 to = data.get("to")38 subject = data.get("subject")39 text = data.get("text")40 html = data.get("html")4142 if not to or not subject or not (text or html):43 return jsonify({"error": "Required: to, subject, and text or html"}), 4004445 try:46 result = send_email(to=to, subject=subject, text=text, html=html)47 return jsonify({"success": True, "messageId": result.get("id")})48 except requests.exceptions.HTTPError as e:49 return jsonify({"error": str(e)}), 5005051if __name__ == "__main__":52 app.run(host="0.0.0.0", port=3000)Expected result: Flask app sends emails via Mailgun when POST /send-email is called with valid parameters.
Common use cases
Transactional Password Reset Emails
When a user requests a password reset, generate a secure token, store it in your database, and use Mailgun to send a reset link to their email address. The Replit backend handles token generation and calls the Mailgun API to deliver the email immediately, with tracking so you can see whether the email was received.
Build a Flask endpoint that accepts a POST request with an email address, generates a secure password reset token, stores it in a database table with an expiry time, and sends a reset email via the Mailgun API using MAILGUN_API_KEY and MAILGUN_DOMAIN from Replit Secrets.
Copy this prompt to try it in Replit
Order Confirmation Notifications
After a successful purchase in your app, send a structured order confirmation email with the customer's order details, item list, and total. Use a Mailgun template to maintain consistent branding, and pass order data as template variables so each email is personalized.
Build an Express route that receives order data via POST, validates the required fields, and sends an order confirmation email using the Mailgun API with a template named order-confirmation. Pass order_id, customer_name, and items as template variables.
Copy this prompt to try it in Replit
Delivery Status Webhook Processor
Subscribe to Mailgun's webhook events to track whether your emails are being delivered, opened, clicked, or bounced. Your Replit app receives these events and updates your database, allowing you to identify email addresses with delivery problems and suppress future sends to reduce bounce rates.
Build an Express server that receives Mailgun webhook POST requests, verifies the HMAC signature using your webhook signing key, parses the event type (delivered, bounced, complained), and updates an email_events database table with the status and timestamp.
Copy this prompt to try it in Replit
Troubleshooting
401 Unauthorized when calling the Mailgun API
Cause: The API key is incorrect, using the wrong key type (public vs private), or the MAILGUN_API_KEY secret name doesn't match the variable name in your code.
Solution: In Mailgun Settings > API Keys, confirm you're copying the 'Private API key' (not the public validation key). Verify in Replit Secrets that the key name is exactly MAILGUN_API_KEY. The authorization header should use HTTP Basic auth with username 'api' and your key as the password β not a Bearer token.
1// Debug auth header construction2const credentials = Buffer.from(`api:${process.env.MAILGUN_API_KEY}`).toString('base64');3console.log('Auth:', `Basic ${credentials.substring(0, 20)}...`);Emails send successfully from sandbox but fail on custom domain with 'Domain not found' error
Cause: The MAILGUN_DOMAIN secret contains the sandbox domain name, but you're trying to send from your custom domain that may not be fully verified yet.
Solution: Update MAILGUN_DOMAIN in Replit Secrets to your custom domain name. Verify the domain status in Mailgun Sending > Domains β it should show 'Active'. If it shows 'Unverified', check that all four DNS records (SPF TXT, DKIM TXT, CNAME, and MX) are correctly entered at your DNS provider.
Emails go to spam instead of the inbox
Cause: Missing or incorrectly configured SPF, DKIM, or DMARC DNS records on the sending domain, or sending from a brand new domain without a warmup period.
Solution: In Mailgun Sending > Domains, click your domain and verify all DNS records show green checkmarks. Ensure your SPF record includes Mailgun's servers, DKIM is configured, and you have a DMARC policy. For new domains, start with low volume and gradually increase β sudden high-volume sends from new domains trigger spam filters.
Mailgun webhooks returning 401 or not reaching the server
Cause: Either the webhook is pointing to the development Repl URL (which is not always active) instead of the deployment URL, or webhook signature verification is rejecting valid Mailgun requests.
Solution: Deploy your app (Autoscale) and use the .replit.app deployment URL in Mailgun webhook settings. If signature verification fails, check that MAILGUN_WEBHOOK_KEY is set in Replit Secrets with the webhook signing key from Mailgun Settings > Webhooks (different from your API key).
Best practices
- Store MAILGUN_API_KEY and MAILGUN_DOMAIN in Replit Secrets (lock icon π) β never hardcode them in source files
- Use a subdomain (e.g., mail.yourdomain.com) for sending rather than your root domain to protect root domain email reputation
- Always include both text and HTML versions of emails β plain text fallback improves deliverability and accessibility
- Verify Mailgun webhook signatures using HMAC-SHA256 with your webhook signing key to prevent spoofed delivery events
- Handle bounced and complained webhook events by suppressing future sends to those addresses β ISPs penalize senders who repeatedly email bad addresses
- Deploy as Autoscale for webhook receivers β it handles Mailgun's event bursts while scaling to zero during quiet periods
- Set up DMARC in addition to SPF and DKIM for maximum deliverability and to prevent your domain from being spoofed in phishing emails
- Log the Mailgun message ID returned from each send call β it lets you trace delivery issues using the Mailgun Logs dashboard
Alternatives
SendGrid has native Replit Agent integration for faster setup and a broader free tier, making it a better choice if you want Replit's Agent to scaffold the integration automatically.
Mailchimp is better if you need marketing email campaigns, list management, and audience segmentation alongside transactional sending.
Constant Contact is a simpler option for small businesses that need basic email marketing without the developer-focused feature set of Mailgun.
Frequently asked questions
How do I connect Replit to Mailgun?
Get your Private API key from Mailgun Settings > API Keys and your sending domain. Store them in Replit Secrets as MAILGUN_API_KEY and MAILGUN_DOMAIN (click the lock icon π in the sidebar). Then use HTTP Basic auth in your server-side code to POST to the Mailgun messages endpoint.
Can I use Mailgun in Replit for free?
Mailgun's free trial includes 100 emails per day for the first 3 months. After that, paid plans start at $35/month for 50,000 emails, or you can use the Flex (pay-as-you-go) plan. Replit's free tier can run the integration in development mode using Mailgun's sandbox domain.
Why do my Mailgun emails go to spam?
Missing DNS records are the most common cause β ensure SPF, DKIM, and DMARC records are all configured correctly on your sending domain. In Mailgun's dashboard under Sending > Domains, all records should show green checkmarks. New domains also need a warm-up period of gradually increasing volume before reaching full deliverability.
How do I receive Mailgun webhooks on Replit?
Deploy your Replit app (use the Deploy button, choose Autoscale), copy the deployment URL ending in .replit.app, and register it in Mailgun Sending > Webhooks. Development Repl URLs are not reliable for webhooks because they sleep when the editor is closed. Always use the deployed URL.
What is the difference between Mailgun and SendGrid for Replit?
SendGrid has native Replit Agent integration, which means Replit's AI agent can scaffold the email integration for you automatically. Mailgun requires manual setup but offers more flexible domain configuration and a pay-as-you-go pricing model that can be cheaper for low-volume sending.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation