Building transactional email workflows for shipping notifications with Mailgun’s API
Customers expect real-time updates the moment their order moves. This guide walks you through building a reliable transactional email workflow for shipping notifications using Mailgun’s API and BullMQ, so your emails hit the inbox exactly when they should.
PUBLISHED ON
When you order something online, you expect to know exactly where it is and when it will arrive. Transactional email workflows help make this type of timely communication possible.
Unlike marketing emails that promote products, transactional emails are sent in response to a user's action, like placing an order or getting a shipping update. They provide real-time details that keep users in the loop every step of the way.
In this article, you'll learn how to build a transactional email workflow for shipping notifications using Mailgun's API. The workflow will incorporate BullMQ, a powerful job queue system, to queue and process email notifications separately from the main application logic and improve scalability and performance. This is useful for high-traffic applications where sending emails directly within a request-response cycle could slow down the system.
Table of contents
Table of contents
01Implementing transactional email workflows for shipping notifications with Mailgun's API
03Setting up the starter template
04Install all the dependencies using the following command:
05Designing the email templates
06Implementing the email-sending functionality
07Integrating with application events
Implementing transactional email workflows for shipping notifications with Mailgun's API
To follow along, you need these tools and accounts:
A code editor (this tutorial uses VS Code) and a web browser
Before diving into the implementation, let's take a look at the workflow:

Here's how the process works:
The shipping team (or anyone managing shipping updates) sends an HTTP request to update the order status
The Express server updates the order status in the SQLite database and queues a job in BullMQ
A BullMQ worker processes jobs from the queue and interacts with the Mailgun API to send an email to the customer
The worker automatically retries failed jobs
Mailgun delivers the email to the customer
Mailgun account setup
To use the Mailgun API, you need a Mailgun domain and an API key for authentication. Mailgun provides two types of domains: sandbox domains for testing and custom domains for use in production environments.
In this guide, you'll use a sandbox domain. Each Mailgun account is automatically provisioned with one, so you don't have to create it manually. To obtain your sandbox domain, log in to your Mailgun account, then navigate to Send > Sending > Domains in the sidebar. Copy your domain:

By default, a sandbox domain can only send emails to authorized recipients. To set up an email as an authorized recipient, click your domain name on the Domains page to view its settings. On the Setup tab, under Add authorized recipients, add the email address that you want to send emails to. Make sure to follow the instructions sent to the email address you provided to set it up as an authorized recipient:

To obtain your API key, navigate to the API Keys page and click Add new key to create a new API key. Provide a name for the key and click Create Key, then make sure to copy the value of your key because it'll only be displayed once:

Setting up the starter template
To keep this tutorial focused on building transactional email workflows, we've prepared a starter template for you to build on. Clone the template to your local machine by running the following command in your terminal:
git clone --single-branch -b starter-template https://github.com/kimanikevin254/mailgun-transactional-email-workflows.git
Here are the key files in the project:
src/controllers/order.controller.ts contains
two methods:index
renders the admin UI, which is defined insrc/views/admin.ejs
updateOrderStatus
updates the order status in the database
src/database/entity/order.entity.ts
defines the order entitysrc/database/entity/user.entity.ts
defines the user entitysrc/database/seed.ts
seeds the database with a sample user and orderdocker-compose.yml runs
Redis in Docker, which is required by BullMQ
Install all the dependencies using the following command:
npm install
Next, rename the .env.example
file
to .env
and replace the placeholder values for MAILGUN_DOMAIN
and MAILGUN_API_KEY
with the credentials you obtained from the Mailgun dashboard. For the MAIL_FROM variable, provide your email address as the value.
Next, open the src/database/seed.ts
file
and replace the placeholder details in the userInfo constant
with your details. For the email, make sure to provide one that you added as an authorized recipient.
Finally, seed the database by executing the command npm run db:seed
in your terminal. You can now run the project using the command npm run dev
and navigate to http://localhost:3000 in your browser to view and update the order status:

In the next section, you'll implement a workflow that automatically sends an email to the user once the order status is updated.
Designing the email templates
Well-designed email templates improve readability, enhance user experience, and ensure your emails look professional across different devices. In this section, you'll create templates that make your transactional emails clear, engaging, and effective.
An order goes through three states (shipped, out for delivery, and delivered), as defined in the OrderStatus type
in src/types/index.ts.
You'll create a corresponding email template for each state to keep customers informed at every step.
To do this, create a file at src/email-templates/html/shipped.html
and add the following code:
This email template is used to notify customers that their order has been shipped. It includes placeholders for the customer's name and tracking number, which will be dynamically filled by EJS when rendering the template. The templates for the other two order statuses will follow the same format.
Create a template to notify users that their order is out for delivery by creating a file named out_for_delivery.html
in the src/email-templates/html
folder and adding the following code:
Lastly, create a template to notify users that their order has been delivered by creating a file named delivered.html
in the src/email-templates/html
folder
and adding the following code:
To manage and render email templates dynamically, you need to create a class that loads and organizes templates for different order statuses. This class will retrieve the right template and render it with customer-specific data. To do this, create a new file named index.ts
in the src/email-templates folder
and add the following code:
This class initializes a private variable named templates that stores email templates in a Map for quick lookup based on order status. Using a Map ensures efficient retrieval and avoids repeatedly reading files from disk. The class also defines the following methods:
loadTemplates
(private) reads HTML templates from files and stores them in the templates Map, along with their corresponding subject lines.getTemplate
retrieves the email template for a given order status and throws an error if no template is found.renderTemplate
uses EJS to dynamically render a template with provided data.
Implementing the email-sending functionality
With the email templates set up, the next step is to implement the functionality for sending notifications. This involves queuing email jobs and processing them asynchronously.
But before you can do this, you need to install a few dependencies by executing the following command:
npm i mailgun.js@11.1.0 form-data@4.0.2 bullmq@5.41.2 ioredis@5.5.0
Here's an overview of each of these dependencies:
mailgun.js
is a Node.js client for interacting with the Mailgun API. You'll use this to send transactional emails.form-data
is a package required bymailgun.js
to handle form submissions when sending emails.bullmq
is a job queue library for handling asynchronous tasks. This allows you to queue email notifications and process them in the background.ioredis
is a Redis client for Node.js used by BullMQ to store and manage queued jobs.
Next, you need to define a Mailgun API client that you'll use to send emails. To do this, create a new file at src/config/mailgun.config.ts
and add the following code:
This code sets up and exports a Mailgun client using the mailgun.js
library and form-data
, allowing your application to send emails through the Mailgun API with the API key stored in environment variables.
Now, you need to define a Redis connection that will be used by BullMQ to store and manage background jobs. To do this, create a new file named redis.config.ts
in the src/config
folder and add the code below:
You need to set up a BullMQ queue for the shipping notifications that will handle background email notification jobs using the Redis connection you defined in the previous step. To do this, create a new file named queues.config.ts
in the src/config
folder and add the following code:
This code also defines names for the queue and job type that can be used in other parts of the application to avoid hard-coded strings.
Lastly, create a new file at src/services/notifications.service.ts
and add the following code:
This code defines a NotificationService
class that handles email notifications using a job queue system with BullMQ and Mailgun. This class initializes three private properties: queue, which is the queue where email jobs are added; mailgunClient
, which is the Mailgun client for sending emails; and templateManager
, which is an instance of the TemplateManager class that you created previously to manage and retrieve email templates. This class also defines the following methods:
setupWorker
creates a worker that listens for new email jobs and processes them. It logs job completion or failure.processNotification
fetches the correct email template, populates it with dynamic data, and sends the email using Mailgun.queueNotification
adds a new email job to the queue, with retry attempts and exponential backoff for failed attempts. This method can be called from other parts of the application to add jobs to the queue.
Integrating with application events
To send email notifications when an order status is updated, you need to queue a job in the notification system. To do this, open the src/controllers/order.controller.ts
file and add the following import statement to import the NotificationService
class:
import { NotificationService } from "../services/notifications.service";
Next, define a private property inside the OrderController
class to hold an instance of the NotificationService
class:
private notificationService: NotificationService;
Initialize this property by adding the following code inside the constructor
method:
this.notificationService = new NotificationService();
Lastly, add the following code inside the updateOrderStatus
method after await this.orderRepository.save(order);
to queue a job once the order status is successfully updated:
The transactional email workflow is now complete. You can go ahead and test if everything is working as expected.
Testing the workflow
To ensure everything is working correctly, start Redis by running the following command in your terminal:
docker compose up -d
After running this command, you can verify that Redis is up and running by listing the containers:
docker ps
This command shows a list of active containers, and you should see the redis
container running.
Next, run the Express server using the command npm run dev
and navigate to http://localhost:3000 in your browser. On the application's dashboard, update the order status to any of the available options. Once updated, you'll receive an instant response:

In your terminal, you'll get a log that notifies you that the email-sending job has been queued successfully and is being processed:
Job queued successfully
Processing job: 1
And when it's done:
Job 1 completed successfully
You'll then receive an email with the updated order status:

Every time you update the status of an order, the system will automatically send a corresponding email to the user, keeping them informed:

The full application code is available on GitHub.
Monitoring and managing emails
To ensure your transactional emails are reaching customers effectively, you need to monitor their delivery status and open rates and handle any failures. Mailgun provides a dashboard where you can track sent emails, check if they were delivered, opened, or clicked, and view bounce reports.
You can access this dashboard by navigating to Send > Reporting > Metrics in Mailgun:

However, open rates aren't tracked automatically, and you need to configure them manually via your sending domain's Settings tab:

More information on how to track open and click rates is available in the official documentation.
Additionally, you can use webhooks to receive real-time notifications about failed deliveries and take appropriate action, such as retrying the email.
By actively monitoring your email performance, you can improve deliverability, reduce bounces, and ensure important notifications reach your customers.
Wrapping up
In this article, you learned how to set up a transactional email workflow for shipping notifications using Mailgun, BullMQ, and Redis. You learned how to set up Mailgun for sending emails, configure a job queue with BullMQ, and integrate it into an order update system to ensure customers receive real-time email notifications. This approach improves communication and enhances the overall customer experience.
Was this helpful? Be sure to subscribe to our newsletter for more tutorial content and deep dives like this one.