Dev Life
Unlike marketing emails, transactional emails (such as order confirmations, shipping notifications, and password resets) are triggered by specific user actions and provide real-time updates on their interactions with your platform. They help build trust, cut down on support questions, and make for a smooth shopping experience.
Order confirmations are particularly important, as they let your customers know their purchase was successful and provide the user with a record of the transaction details.
In this tutorial, you’ll learn how to build a transactional email workflow for order confirmations using Mailgun’s API.
Before you begin, make sure you have a Mailgun account and Node.js installed on your machine.
Once your account is registered and activated, click Get started and then Create an API Key to provide a short description of the key. Once it’s generated, copy and save the API key somewhere safe.

This key will be used to authenticate your requests to Mailgun’s API.
To keep things simple, clone the following UI repository and set up the environment variables by running the following npm commands:
$ git clone https://github.com/Ikeh-Akinyemi/draftdev-mailgunnerrn$ cd draftdev-mailgunner; npm install rn$ echo -e u0022MAILGUN_API_KEY=your_api_key_here
MAILGUN_DOMAIN=your_domain_here
PORT=8080u0022 u003eu003e .env
This command adds the mailgun.js, form-data, cors, dotenv, and express libraries for use in the backend server and creates some placeholder values for the Mailgun API key and domain in a .env file that you’ll update during the tutorial.
The Mailgun domain gives you the option of setting up your own custom domain or using a sandbox for testing purposes. You’ll use a sandbox domain in this tutorial. You can find Mailgun’s sandbox domain by navigating to Send > Sending > Domain settings on your dashboard and clicking the Select button in the API integration option:

Your sandbox domain is included in the boilerplates provided for Mailgun setup. Remember to update the value of MAILGUN_DOMAIN (currently your_domain_here) inside the .env file.

If you want to set up a custom domain, follow the instructions detailed in this YouTube tutorial.
In this scenario, you’ll be working with a simple shopping cart application built with HTML, CSS, and JavaScript. The application allows users to add items to their cart, adjust quantities, and proceed to checkout. When the user confirms their purchase, the application sends a request to the backend to process the order and send a confirmation email. To preview the UI, open the ui/index.html file in the browser or run the Python command python3 -m http.server -d=./ui:

This UI is designed to be intuitive, with a focus on providing a seamless checkout experience. The backend handles the heavy lifting, including processing orders and sending confirmation emails.
To integrate Mailgun into your backend, you need to initialize the Mailgun client using your API key. Here’s how to set up the connection in your Node.js backend:
const formData = require('form-data');rnconst Mailgun = require('mailgun.js');rnrnconst mailgun = new Mailgun(formData);rnconst mg = mailgun.client({ username: 'api', key: process.env.MAILGUN_API_KEY });rnconst MAILGUN_DOMAIN = process.env.MAILGUN_DOMAIN;
This code initializes the Mailgun client, which will be used to send transactional emails. Make sure to update the value of MAILGUN_API_KEY (currently your_api_key_here) inside your .env file before continuing.
Test the connection by sending a sample test email:
mg.messages.create('u003cyour-domain.comu003e', {rn from: u0022Excited User u003cmailgun@your-domain.comu003eu0022,rn to: [u0022test@example.comu0022],rn subject: u0022Hellou0022,rn text: u0022Testing some Mailgun awesomeness!u0022rn})rn.then(msg =u003e console.log(msg)) // logs response datarn.catch(err =u003e console.error(err)); // logs any error
You can use the sandbox domain provided by Mailgun for testing, but you need to add at least one verified email for testing (up to five). To verify your test email address, go to Send > Sending > Domain settings in your Mailgun dashboard. Then, enter your email address in the designated input field and click Add. Mailgun will send a verification email to this address.
Check your inbox and click the I Agree verification link to complete the process and register as an authorized test recipient:

Transactional emails often require dynamic content, like the user’s name, order details, and shipping information, which is why you need to create an HTML template with placeholders for dynamic data. Here’s an example of a simple order confirmation template:
u003c!DOCTYPE htmlu003ernu003chtmlu003ernu003cheadu003ern u003ctitleu003eOrder Confirmationu003c/titleu003ernu003c/headu003ernu003cbodyu003ern u003ch1u003eThank you for your order, {{name}}!u003c/h1u003ern u003cpu003eYour order number is {{orderNumber}}.u003c/pu003ern u003cpu003eWe will ship your items to {{shippingAddress}}.u003c/pu003ernu003c/bodyu003ernu003c/htmlu003e
You can use a templating engine like Handlebars or EJS to replace the placeholders with actual data before sending the email. You can also use Mailgun’s intuitive visual builder to create beautiful, responsive email templates without any coding knowledge.
In this article, you’ll use the simplest form of templating: string interpolation using JavaScript template literals. This approach allows you to set up an HTML email template populated with the necessary dynamic data:
function emailTemplate(order) {rn // Format items for emailrn const itemsList = order.items.map(item =u003e {rn return `rn u003ctru003ern u003ctd style=u0022padding: 10px 0; border-bottom: 1px solid #eee;u0022u003ern ${item.name}rn u003c/tdu003ern u003ctd style=u0022padding: 10px 0; border-bottom: 1px solid #eee; text-align: center;u0022u003ern ${item.quantity}rn u003c/tdu003ern u003ctd style=u0022padding: 10px 0; border-bottom: 1px solid #eee; text-align: right;u0022u003ern $${item.price.toFixed(2)}rn u003c/tdu003ern u003ctd style=u0022padding: 10px 0; border-bottom: 1px solid #eee; text-align: right;u0022u003ern $${(item.price * item.quantity).toFixed(2)}rn u003c/tdu003ern u003c/tru003ern `;rn }).join('');rn};
This code iterates over an order’s items to generate a table-like receipt of the customer’s order. The returned value, itemsList, is a string literal that will be used to build the rest of the email template like this:
function emailTemplate(order) {rn ...rnrn // Create email HTML templatern const emailHtml = `rn u003c!DOCTYPE htmlu003ern u003chtmlu003ern u003cheadu003ern u003cstyleu003ern body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }rn .container { max-width: 600px; margin: 0 auto; }rn .header { background-color: #f8f9fa; padding: 20px; text-align: center; }rn .content { padding: 20px; }rn .order-details { margin-top: 20px; }rn .order-table { width: 100%; border-collapse: collapse; }rn .order-table th { text-align: left; padding: 10px 0; border-bottom: 2px solid #ddd; }rn .footer { margin-top: 30px; text-align: center; font-size: 14px; color: #777; }rn .total-row { font-weight: bold; }rn u003c/styleu003ern u003c/headu003ern u003cbodyu003ern u003cdiv class=u0022containeru0022u003ern u003cdiv class=u0022headeru0022u003ern u003ch2u003eOrder Confirmationu003c/h2u003ern u003cpu003eThank you for your purchase!u003c/pu003ern u003c/divu003ernrn u003cdiv class=u0022contentu0022u003ern u003cpu003eHello ${order.customer.name},u003c/pu003ernrn u003cpu003eYour order has been confirmed. Here are your order details:u003c/pu003ernrn u003cdiv class=u0022order-detailsu0022u003ern u003cpu003eu003cstrongu003eOrder ID:u003c/strongu003e ${order.id}u003c/pu003ern u003cpu003eu003cstrongu003eOrder Date:u003c/strongu003e ${new Date(order.createdAt).toLocaleString()}u003c/pu003ernrn u003ctable class=u0022order-tableu0022u003ern u003ctheadu003ern u003ctru003ern u003cthu003eItemu003c/thu003ern u003cth style=u0022text-align: center;u0022u003eQtyu003c/thu003ern u003cth style=u0022text-align: right;u0022u003ePriceu003c/thu003ern u003cth style=u0022text-align: right;u0022u003eTotalu003c/thu003ern u003c/tru003ern u003c/theadu003ern u003ctbodyu003ern ${itemsList}rn u003ctru003ern u003ctd colspan=u00223u0022 style=u0022text-align: right; padding-top: 20px;u0022u003eu003cstrongu003eSubtotal:u003c/strongu003eu003c/tdu003ern u003ctd style=u0022text-align: right; padding-top: 20px;u0022u003eu003cstrongu003e$${order.subtotal.toFixed(2)}u003c/strongu003eu003c/tdu003ern u003c/tru003ern u003ctru003ern u003ctd colspan=u00223u0022 style=u0022text-align: right;u0022u003eu003cstrongu003eTax:u003c/strongu003eu003c/tdu003ern u003ctd style=u0022text-align: right;u0022u003eu003cstrongu003e$${order.tax.toFixed(2)}u003c/strongu003eu003c/tdu003ern u003c/tru003ern u003ctr class=u0022total-rowu0022u003ern u003ctd colspan=u00223u0022 style=u0022text-align: right; padding-top: 10px;u0022u003eu003cstrongu003eTotal:u003c/strongu003eu003c/tdu003ern u003ctd style=u0022text-align: right; padding-top: 10px;u0022u003eu003cstrongu003e$${order.total.toFixed(2)}u003c/strongu003eu003c/tdu003ern u003c/tru003ern u003c/tbodyu003ern u003c/tableu003ern u003c/divu003ernrn u003cdiv style=u0022margin-top: 30px;u0022u003ern u003cpu003eu003cstrongu003eShipping Address:u003c/strongu003eu003c/pu003ern u003cpu003ern ${order.shipping.address}u003cbru003ern ${order.shipping.city}, ${order.shipping.state} ${order.shipping.zipCode}rn u003c/pu003ern u003c/divu003ernrn u003cdiv style=u0022margin-top: 30px;u0022u003ern u003cpu003eu003cstrongu003ePayment Method:u003c/strongu003e ${order.payment.method} (ending in ${order.payment.last4})u003c/pu003ern u003c/divu003ernrn u003cdiv style=u0022margin-top: 30px;u0022u003ern u003cpu003eIf you have any questions about your order, please contact our customer support.u003c/pu003ern u003cpu003eThank you for shopping with us!u003c/pu003ern u003c/divu003ern u003c/divu003ernrn u003cdiv class=u0022footeru0022u003ern u003cpu003eThis is an automated email, please do not reply to this message.u003c/pu003ern u003cpu003e© 2025 Your Company. All rights reserved.u003c/pu003ern u003c/divu003ern u003c/divu003ern u003c/bodyu003ern u003c/htmlu003ern `;rnrn return emailHtml;rn}
Here, you combine the receipt with the rest of an HTML template string containing customer details. The HTML contains basic styling for the email template.
Now that your template is ready, it’s time to write the function that sends the email using Mailgun’s API. Here’s how you can implement this functionality in your backend:
async function sendOrderConfirmationEmail(order) {rn try {rnrn let emailHtml = emailTemplate(order)rnrn // Send email via Mailgunrn const response = await mg.messages.create(MAILGUN_DOMAIN, {rn from: `Your Store u003corders@${MAILGUN_DOMAIN}u003e`,rn to: order.customer.email,rn subject: `Order Confirmation #${order.id}`,rn html: emailHtml,rn 'o:tag': ['order-confirmation'],rn 'o:tracking': truern });rnrn console.log('Email sent successfully:', response);rn return response;rnrn } catch (error) {rn console.error('Error sending confirmation email:', error);rn throw error;rn }rn}
This function takes an order object, constructs the email content, and sends it using Mailgun’s API. The o:tag and o:tracking options allow you to track the email’s delivery and engagement.
Now, let’s test the implementation with a simple order object to see if it works:
const testOrder = {rn id: u0022TEST123u0022,rn customer: {rn name: u0022Test Useru0022,rn email: u0022your-verified-email@example.comu0022 // Use one of your verified emails for testingrn },rn items: [rn { name: u0022Test Productu0022, quantity: 1, price: 29.99 }rn ],rn subtotal: 29.99,rn tax: 2.99,rn total: 32.98,rn shipping: {rn address: u0022123 Test Streetu0022,rn city: u0022Test Cityu0022,rn state: u0022TSu0022,rn zipCode: u002212345u0022rn },rn payment: {rn method: u0022Credit Cardu0022,rn last4: u00224242u0022rn },rn createdAt: new Date()rn};rnrnsendOrderConfirmationEmail(testOrder)rn .then(response =u003e {rn console.log('Test email sent successfully!');rn console.log('Message ID:', response.id);rn console.log('Message status:', response.status);rn })rn .catch(error =u003e {rn console.error('Failed to send test email:', error.message);rn });
Run this code with node ./src/server.js and check your inbox (remember to use a verified email for the testOrder.customer.email value if you’re using Mailgun’s sandbox domain). You should receive a basic order confirmation email with the test data:

In this setup, any error that occurs is bubbled up to the caller, and the .catch method logs it to the standard output. This approach allows you to quickly identify and troubleshoot any issues that might occur during the email sending process.
In the next section, you’ll connect this to your frontend so you can send real order data from customer checkouts.
To ensure that the email workflow is triggered automatically when an order is confirmed, you need to set up an event listener or route in your application.
Here, you’ll set up an Express.js application and define a route that handles order confirmations while calling the sendOrderConfirmationEmail function:
const express = require('express');rnconst cors = require('cors');rn...rnrn// Load environment variablesrnrequire('dotenv').config();rnrn// Initialize Expressrnconst app = express();rnconst PORT = process.env.PORT || 3000;rnrn// Middlewarernapp.use(cors());rnapp.use(express.json());rnrnconst orders = [];rnrnapp.post('/api/orders', async (req, res) =u003e {rn try {rn // Validate required fieldsrn if (!req.body.items || !req.body.customer || !req.body.customer.email) {rn return res.status(400).json({ rn success: false, rn error: 'Missing required order information' rn });rn }rnrn // Generate order IDrn const orderId = Date.now().toString(36) + Math.random().toString(36).substr(2, 5).toUpperCase();rnrn // Create order objectrn const order = {rn id: orderId,rn ...req.body,rn status: 'confirmed',rn createdAt: new Date()rn };rnrn // Save order (to database in a real app)rn orders.push(order);rnrn // Send confirmation emailrn await sendOrderConfirmationEmail(order);rnrn // Return success responsern res.status(201).json({rn success: true,rn order: {rn id: order.id,rn status: order.status,rn createdAt: order.createdAtrn },rn message: 'Order created successfully and confirmation email sent.'rn });rnrn } catch (error) {rn console.error('Error processing order:', error);rn res.status(500).json({rn success: false,rn error: 'Failed to process order'rn });rn }rn});
The /api/orders route handles incoming order requests, creates an order object, and sends a confirmation email using the sendOrderConfirmationEmail function. The rest of the route includes a simple error reporting logic, but in production, you might want to implement a retry mechanism or a more robust error handler.
To test the setup so far, you need to modify the /ui/index.html file. First, find the line of code where the orderData object is defined and update the customer’s email property (orderData.customer.email) to use one of your verified emails. You also need to update the api constant to point to the URL of your server.
Then, open the /ui/index.html file in a browser and trigger the checkout process by selecting items for checkout and clicking the Checkout Selected Items button. This should open a confirmation dialog where you can click Confirm Purchase to complete the checkout process:

Check your email inbox and verify that the email was sent and correctly rendered with the dynamic content. If you used the sandbox domain, you may need to check your spam folder:

Remember to test how your application handles errors, such as invalid email addresses or API downtimes, by intentionally using incorrect values and verifying that your error handling works as expected.
All the code used in this tutorial is available on GitHub.
Mailgun provides detailed metrics and tracking features that allow you to monitor the performance of your transactional emails. You can track delivery rates and open rates in addition to handling bounces or failures directly from the Mailgun dashboard.
To access the logs of each email sent, navigate to the Send > Reporting > Logs section in your Mailgun account. Here, you can see the timestamp and status of each email sent, including whether it was delivered, opened, or rejected:

You can click any log entry to view its full details, like geolocation for opened emails, delivery-status for delivered emails, and others:

You can also go to Send > Reporting > Metrics to view a graphical breakdown of key email metrics, including the sent, delivered, failed, and opened count:

You’ve just built a working order confirmation workflow with Mailgun’s API.
Transactional emails like these keep customers informed right after checkout, cutting down on support tickets and improving trust without extra overhead. Order confirmations aren’t the only workflows that matter to consumers. Check out our tutorial on password resets to keep optimizing your transactional emails.