We build custom applications 5x faster and cheaper 🚀
Book a Free Consultation
Stuck on an error? Book a 30-minute call with an engineer and get a direct fix + next steps. No pressure, no commitment.
To integrate Replit with Mailgun, you send emails from your Repl by calling Mailgun’s REST API and you keep your Mailgun API key and domain inside Replit Secrets. There is no native or automatic integration — you explicitly make HTTPS requests from your backend code. On Replit, this usually means running a small server (Node.js, Python, etc.) that calls Mailgun’s API using your secret key. For webhooks (like tracking bounces, deliveries, opens), you expose a route on your Repl’s server and give Mailgun the public URL that Replit provides after running your app. That’s the entire real-world pattern: outbound API calls + inbound webhooks, all secured through Replit Secrets.
You set up Mailgun (domain + API key), store the key in Replit Secrets, write code that calls Mailgun’s REST API, run a server bound to 0.0.0.0, and if you need webhooks, expose a route and register that URL in Mailgun’s dashboard. This is practical, explicit, and matches how Replit actually runs full‑stack projects.
// server.js
// Fully real, working example of sending an email with Mailgun from a Replit server
import express from "express";
import fetch from "node-fetch";
const app = express();
app.use(express.json()); // Needed for JSON webhooks if you use them
// Load secrets from Replit environment variables
const MAILGUN_API_KEY = process.env.MAILGUN_API_KEY;
const MAILGUN_DOMAIN = process.env.MAILGUN_DOMAIN; // e.g. mg.example.com
app.get("/", (req, res) => {
res.send("Mailgun Integration Running");
});
app.get("/send-test", async (req, res) => {
const url = `https://api.mailgun.net/v3/${MAILGUN_DOMAIN}/messages`;
const form = new URLSearchParams();
form.append("from", "Your App <mailgun@" + MAILGUN_DOMAIN + ">");
form.append("to", "[email protected]"); // Replace with any email
form.append("subject", "Hello from Replit + Mailgun!");
form.append("text", "This message was sent using Mailgun's REST API.");
// Mailgun uses HTTP Basic Auth with "api" as the username
const authString = "Basic " + Buffer.from("api:" + MAILGUN_API_KEY).toString("base64");
const resp = await fetch(url, {
method: "POST",
headers: { Authorization: authString },
body: form
});
const data = await resp.json();
res.json(data);
});
// OPTIONAL: Webhook endpoint for Mailgun delivery events
app.post("/mailgun/webhook", (req, res) => {
// Mailgun sends JSON describing the event
console.log("Webhook received:", req.body);
// Always respond 200 OK or Mailgun will retry
res.status(200).end();
});
// Replit requirement: bind to 0.0.0.0
app.listen(3000, "0.0.0.0", () => {
console.log("Server running on port 3000");
});
In your Repl:
Secrets automatically turn into environment variables inside your running Repl. Never hardcode the API key in your code.
If you want Mailgun to notify your app about events (delivered, bounced, unsubscribed), you expose a server route and point Mailgun to it.
When the Repl sleeps or restarts, the public URL stays stable, so webhooks remain valid. Just make sure the server is running.
This setup—explicit API call, explicit webhook, secrets stored safely—is the correct and reliable way to integrate Replit with Mailgun.
1
Â
A Replit-hosted web app can send transactional emails (sign‑up confirmations, password resets, receipts) using Mailgun’s REST API. The backend runs inside a Repl, binds to 0.0.0.0, and uses Replit Secrets to store the Mailgun API key and domain. Whenever a user triggers an action, the server makes an outbound HTTPS request to Mailgun to deliver the email. This works reliably because the email sending is offloaded to Mailgun, so even if the Repl restarts, the mail still goes out.
// Example using Node.js + Express inside Replit
import express from "express";
import fetch from "node-fetch";
const app = express();
app.use(express.json());
app.post("/signup", async (req, res) => {
await fetch(`https://api.mailgun.net/v3/${process.env.MAILGUN_DOMAIN}/messages`, {
method: "POST",
headers: {
Authorization: "Basic " + Buffer.from("api:" + process.env.MAILGUN_API_KEY).toString("base64"),
"Content-Type": "application/x-www-form-urlencoded"
},
body: new URLSearchParams({
from: "My App <noreply@" + process.env.MAILGUN_DOMAIN + ">",
to: req.body.email,
subject: "Welcome!",
text: "Thanks for signing up!"
})
});
res.json({ ok: true });
});
app.listen(3000, "0.0.0.0");
2
Â
You can expose a Webhook endpoint from a Replit server to receive delivery, open, spam, and bounce notifications from Mailgun. Replit assigns a public URL for the running Repl, and you register that URL in Mailgun’s dashboard. Your endpoint must verify Mailgun’s signature (HMAC) so you trust the events. This lets your app keep an accurate email status log without polling Mailgun.
// Mailgun webhook endpoint
import crypto from "crypto";
app.post("/mailgun/webhook", express.urlencoded({ extended: false }), (req, res) => {
const { timestamp, token, signature } = req.body;
const expected = crypto
.createHmac("sha256", process.env.MAILGUN_API_KEY)
.update(timestamp + token)
.digest("hex");
if (expected !== signature) return res.status(401).end();
// Process the event safely
console.log("Mailgun Event:", req.body.event);
res.status(200).end();
});
3
Â
Replit Workflows allow you to run scheduled or on-demand scripts without keeping a server online. You can create a workflow that loads your contact list (from a file, database, or external API) and sends bulk but controlled email batches through Mailgun. Each run is stateless and reproducible, ideal for newsletters, status updates, or periodic system notifications.
# Example workflow step (run with Replit Workflows)
node send-batch.js
// send-batch.js
import fetch from "node-fetch";
const recipients = ["[email protected]", "[email protected]"]; // demo list
for (const to of recipients) {
await fetch(`https://api.mailgun.net/v3/${process.env.MAILGUN_DOMAIN}/messages`, {
method: "POST",
headers: {
Authorization: "Basic " + Buffer.from("api:" + process.env.MAILGUN_API_KEY).toString("base64"),
"Content-Type": "application/x-www-form-urlencoded"
},
body: new URLSearchParams({
from: "Batch Sender <noreply@" + process.env.MAILGUN_DOMAIN + ">",
to,
subject: "Scheduled Update",
text: "Hello from Replit Workflows!"
})
});
}
Speak one‑on‑one with a senior engineer about your no‑code app, migration goals, and budget. In just half an hour you’ll leave with clear, actionable next steps—no strings attached.
1
Mailgun calls fail on Replit mainly because the request never reaches Mailgun: Replit blocks outbound traffic on port 25/465/587, so using SMTP fails even with a valid key. Mailgun must be called through their HTTPS REST API, and failures often come from wrong domain, missing REPLIT secrets, or sending from an unverified domain.
Mailgun SMTP ports are restricted, so the connection is dropped. The REST API works, but only if the MAILGUN_API_KEY and MAILGUN\_DOMAIN are stored in Replit Secrets and you call the correct regional endpoint. Also, Mailgun rejects emails if your “from” domain is not verified.
// Working REST call from Replit
import fetch from "node-fetch"
fetch("https://api.mailgun.net/v3/YOUR_DOMAIN/messages", {
method: "POST",
headers: { Authorization: "Basic " + Buffer.from(`api:${process.env.MAILGUN_API_KEY}`).toString("base64") },
body: new URLSearchParams({ from:"[email protected]", to:"[email protected]", subject:"Hi", text:"OK" })
})
2
The right way is to put your Mailgun keys into Replit Secrets and read them with process.env at runtime. You never hard‑code keys in your files, and you never commit them. Secrets automatically load into the environment each time the Repl or Deployment starts, so your Mailgun client can access them safely.
Open the Secrets panel in Replit, create keys like MAILGUN_API_KEY and MAILGUN\_DOMAIN, and Replit will expose them as environment variables while the app runs. Your code must read them dynamically.
// Example: using Mailgun with Replit Secrets
import formData from "form-data"
import Mailgun from "mailgun.js"
const mg = new Mailgun(formData).client({
username: "api",
key: process.env.MAILGUN_API_KEY // pulled from Replit Secrets
})
mg.messages.create(process.env.MAILGUN_DOMAIN, {
from: "[email protected]",
to: "[email protected]",
subject: "Hello",
text: "Works!"
})
3
A Replit server times out or gets a 403 with Mailgun because the request never leaves Replit correctly or Mailgun blocks it. Most often the Repl tries to call Mailgun through the wrong endpoint, uses an invalid API key, or stalls because outbound SMTP isn’t allowed in the Replit environment. Mailgun expects HTTPS REST calls with proper authentication, and if anything is off, Replit shows a timeout or Mailgun returns 403.
Your Repl must call Mailgun’s REST API over HTTPS. Replit does not allow raw outbound SMTP, so using SMTP configs leads to silent stalls and timeouts. If you use the wrong Mailgun domain, or the API key stored in Replit Secrets is incorrect, Mailgun rejects the request with 403 Forbidden. Timeouts also appear if the code waits on a connection that never succeeds.
import FormData from "form-data"
import fetch from "node-fetch"
const fd = new FormData()
fd.append("from", "Test <[email protected]>")
fd.append("to", "[email protected]")
fd.append("subject", "Hi")
fd.append("text", "Message")
fetch(`https://api.mailgun.net/v3/${process.env.MAILGUN_DOMAIN}/messages`, {
method: "POST",
headers: { Authorization: "Basic " + Buffer.from("api:" + process.env.MAILGUN_API_KEY).toString("base64") },
body: fd
})
Many developers accidentally hardcode their Mailgun API key directly in their Repl. Since Replit auto‑forks and exposes code publicly, this instantly compromises the key. Always store keys in Replit Secrets so they remain private and only accessible at runtime, not committed into the project files.
// Correct usage: pull the key from Replit Secrets
const apiKey = process.env.MAILGUN_API_KEY;
Mailgun has different API base URLs depending on region and domain type. A common mistake is pointing to api.mailgun.net when your domain is on the EU cluster, causing silent failures or 404 errors. Always match your domain’s region and use the URL shown in Mailgun’s dashboard for sending.
// Example: ensure your domain matches this base URL
const BASE_URL = "https://api.mailgun.net/v3/" + process.env.MAILGUN_DOMAIN;
Mailgun webhooks need to reach your Repl from the public internet. If you bind your Express or Flask server to localhost instead of 0.0.0.0, Mailgun simply cannot connect. Replit only exposes servers listening on 0.0.0.0, so webhooks will silently fail until the binding is fixed.
// Correct binding for Replit webhook servers
app.listen(process.env.PORT || 3000, "0.0.0.0");
Mailgun signs every webhook request, but many developers skip verification. On Replit, where URLs are public, this means anyone can hit your webhook endpoint and trigger fake events. Use Mailgun’s timestamp + token + signature verification to confirm the request is legitimate before processing it.
// Minimal verification (example logic)
const crypto = require("crypto");
const expected = crypto.createHmac("sha256", process.env.MAILGUN_SIGNING_KEY)
.update(timestamp + token)
.digest("hex");
This prompt helps an AI assistant understand your setup and guide you through the fix step by step, without assuming technical knowledge.
From startups to enterprises and everything in between, see for yourself our incredible impact.
Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We’ll discuss your project and provide a custom quote at no cost.Â