Send a Coupon Expiration Reminder Email
Introduction
Scheduled Triggers allows you to schedule business, or other logic to occur at specific times or intervals. In this guide, we'll walk through how to use Scheduled Triggers on an e-commerce type application to send a reminder email to users when their coupon is about to expire. Nudges like this can help increase conversion rates and improve the overall user experience.
DOCS E-COMMERCE SAMPLE APP
This quickstart/recipe is dependent upon the docs e-commerce sample app. If you haven't already deployed the sample app, you can do so with one click below. If you've already deployed the sample app, simply use your existing project.
Prerequisites
Before getting started, ensure that you have the following in place:
- The docs e-commerce sample app deployed to Hasura Cloud.
- A working SMTP server or email-sending service that you can integrate with to send emails.
If you plan on using a webhook endpoint hosted on your own machine, ensure that you have a tunneling service such as ngrok set up so that your Cloud Project can communicate with your local machine.
Our model
When sending transactional emails such as this, there are three fundamental components to consider:
- Your data source: In your database, which table contains the value that you want to use to determine whether or not to send the email?
- Your querying logic: In your webhook, how will you query your database to determine whether or not to send the email? How will you return information so that you have the correct data to include in the email?
- Your email templating: How will you generate and send the email containing the information you want to send?
Our sample app's database contains, among others, two tables: users
and coupons
. The users
table contains the
details of our users, and the coupons
table contains the details of the coupons that we want to send reminders for.
Step 1: Create the Scheduled Event
Head to your the Hasura Console of your project and navigate to the "Events" tab. From there, click on the
Cron Triggers
item in the sidebar. Then, click Create
:
Step 2: Configure the Scheduled Event
First, provide a name for your trigger, e.g., send_coupon_expiration_email
. Then, enter a webhook URL that will be
called when the event is triggered. This webhook will be responsible for sending the email and can be hosted anywhere,
and written in any language, you like.
Enter the URL of the webhook to allow Hasura to communicate with it:
https://<your-webhook-url>/expiration_check
You'll need to use a tunneling service such as ngrok to expose a webhook endpoint running on your local machine to the internet and Hasura Cloud. This will give you a public URL that will forward requests to your local machine and the server which we'll configure below.
You'll need to modify your webhook URL to use the public URL provided by ngrok.
After installing ngrok and authenticating, you can do this by running:
ngrok http 4000
Then, copy the Forwarding
value for use in our webhook 🎉
https://<your-webhook-url>/expiration_check
If you're not running your Hasura instance on the same machine as your webhook endpoint, you'll need to use a tunneling service such as ngrok to expose your webhook endpoint to the internet. This will allow you to expose a public URL that will forward requests to your local machine and the server we'll configure below.
You'll need to modify your webhook URL to use the public URL provided by ngrok.
Next, we'll configure the cron expression that will trigger the event. In this example, we want to send requests at midnight. We can do that with the following cron expression:
0 0 * * *
Our trigger must also have a payload. This payload will be sent to the webhook endpoint when the event is triggered. We
don't have to include any data in the payload, but we can if we want to. In this example, we'll simply send a
trigger_type
property categorizing the event as a coupon_reminder
. In the Payload
section, enter the following:
{
"trigger_type": "coupon_reminder"
}
Under Advanced Settings
, we can configure the headers that will be sent with the request. We'll add an
authentication
header to prevent abuse of the endpoint and ensure that only Hasura can trigger the event. Set the
Key
as secret-authorization-string
and the Value
as super_secret_string_123
.
Also, change the Request Transform Options
to POST
so that the payload is sent in the request body.
Finally, click the Add Cron Trigger
button to create the Scheduled Event.
Step 3: Create a webhook to handle the request
Whenever a cron job is triggered, Hasura will send a request to the webhook URL you provided. In this example, we're
simply going to send a POST
request. Our webhook will needs to do three things when triggered:
- Ensure the auth header is correct.
- Query the database to see which coupons are expiring in two days.
- For each coupon returned, send an email to the user.
Below, we've written an example of webhook. As we established earlier, this runs on port 4000
. If you're attempting to
run this locally, follow the instructions below. If you're running this in a hosted environment, use this code as a
guide to write your own webhook.
- JavaScript
- Python
Init a new project with npm init
and install the following dependencies:
npm install express nodemailer
Then, create a new file called index.js
and add the following code:
const express = require('express');
const nodemailer = require('nodemailer');
const app = express();
// Create a Nodemailer transporter using Ethereal email service
// Ideally, this configuration would be stored somewhere else
nodemailer.createTestAccount((err, account) => {
if (err) {
console.error('Failed to create a testing account. ' + err.message);
return process.exit(1);
}
// If all goes as planned, here's the console telling us we're 👍
console.log('Credentials obtained, listening on the webhook...');
// Create a transporter object for nodemailer
const transporter = nodemailer.createTransport({
host: 'smtp.ethereal.email',
port: 587,
secure: false,
auth: {
user: account.user,
pass: account.pass,
},
});
// helper function to turn timestamp into a date
function timestampToDate(timestamp) {
const date = new Date(timestamp);
return date;
}
// Our route for the webhook
app.post('/expiration_check', async (req, res) => {
// confirm the auth header is correct — ideally, you'd keep the secret in an environment variable
const authHeader = req.headers['secret-authorization-string'];
if (authHeader !== 'super_secret_string_123') {
return res.status(401).json({
message: 'Unauthorized',
});
}
// get our date ready for the query
const today = new Date();
const twoDaysFromNow = today.setDate(today.getDate() + 2);
const twoDaysFromNowTimestamp = new Date(twoDaysFromNow).toISOString();
// Fetch the data from our Hasura instance
async function getExpiringCoupons() {
const response = await fetch('<YOUR_CLOUD_PROJECT_ENDPOINT>', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-hasura-admin-secret': '<YOUR_ADMIN_SECRET>',
},
body: JSON.stringify({
query: `
query GetExpiringCoupons($two_days_from_now: timestamptz!) {
coupons(where: {expiration_date: {_lte: $two_days_from_now, _gte: today}}) {
id
code
expiration_date
user {
id
name
email
}
}
}
`,
variables: {
two_days_from_now: twoDaysFromNowTimestamp,
},
}),
});
const { data } = await response.json();
return data.coupons;
}
let coupons = await getExpiringCoupons();
// map over the data and send an email to each customer
async function sendCouponReminder(coupons) {
let outcomes = [];
coupons.map(async coupon => {
// Create a message object
const message = {
from: 'SuperStore.com <[email protected]>',
to: `${coupon.user.name} <${coupon.user.email}>`,
subject: `You've got a coupon expiring soon, ${coupon.user.name.split(' ')[0]}!`,
text: `Yo ${coupon.user.name.split(' ')[0]},\n\nYour coupon code, ${
coupon.code
}, is expiring soon! Use it before ${timestampToDate(coupon.expiration_date)}.\n\nThanks,\nSuperStore.com`,
};
// Send the message using the Nodemailer transporter
const info = await transporter.sendMail(message);
// Log the message info
console.log(`\nMessage sent to ${coupon.user.name}: ${nodemailer.getTestMessageUrl(info)}`);
// add the info to the outcomes array
outcomes.push({
messageId: info.messageId,
previewUrl: nodemailer.getTestMessageUrl(info),
});
return outcomes;
});
}
await sendCouponReminder(coupons);
// Return a JSON response to the client
res.json({
message: 'Coupons sent!',
});
});
// Start the server
app.listen(4000, () => {
console.log('Server started on port 4000');
});
});
You can run the server by running node index.js
in your terminal.
Make sure you have the necessary dependencies installed. You can use pip to install them:
pip install Flask secure-smtplib requests
Then, create a new file called index.py
and add the following code:
from flask import Flask, request, jsonify
import smtplib
from smtplib import SMTPException
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
import requests
from datetime import datetime, timedelta
app = Flask(__name__)
# Create an SMTP server connection
def create_smtp_server():
try:
server = smtplib.SMTP(host='smtp.ethereal.email', port=587)
server.starttls()
server.login('[email protected]', 'samplePassword123')
return server
except SMTPException as e:
print(f"An error occurred while creating the SMTP server: {e}\nWe'll print the message to the terminal\n")
return None
def get_expired_coupons():
today = datetime.now()
two_days_from_now = today + timedelta(days=2)
formatted_date = two_days_from_now.isoformat()
# Prepare the request headers
headers = {
'Content-Type': 'application/json',
'x-hasura-admin-secret': '<YOUR_ADMIN_SECRET>',
}
graphql_query = '''
query GetExpiringCoupons($two_days_from_now: timestamptz!) {
coupons(where: {expiration_date: {_lte: $two_days_from_now, _gte: today}}) {
id
expiration_date
code
user {
email
id
name
}
}
}
'''
graphql_variables = {
'two_days_from_now': formatted_date
}
# Prepare the request payload as a dictionary
payload = {
'query': graphql_query,
'variables': graphql_variables
}
# Make the POST request to the GraphQL endpoint
response = requests.post('<YOUR_PROJECT_ENDPOINT>', json=payload, headers=headers)
# Check the response status
if response.status_code == 200:
res = response.json()
return res['data']['coupons']
else:
return []
@app.route('/expiration_check', methods=['POST'])
def expiration_check():
# Confirm the secret header is correct
auth_header = request.headers.get('secret-authorization-string')
if auth_header != 'super_secret_string_123':
return jsonify({'message': 'Unauthorized'}), 401
# Fetch expired_coupons from your Hasura
expired_coupons = get_expired_coupons()
# Send email reminders
for coupon in expired_coupons:
email = coupon['user']['email']
# Format the timestamp into a readable format (e.g., "October 15, 2023")
parsed_date = datetime.fromisoformat(coupon['expiration_date'])
readable_date = parsed_date.strftime("%B %d, %Y")
# Create reminder message
msg = MIMEMultipart()
msg['From'] = 'SuperStore.com <[email protected]>'
msg['To'] = f"{coupon['user']['name']} <{email}>"
msg['Subject'] = f"You've got a coupon expiring soon, {coupon['user']['name'].split(' ')[0]}!"
message_body = f"Yo {coupon['user']['name'].split(' ')[0]},\n\nYour coupon code, {coupon['code']}, is expiring soon! Use it before {readable_date}.\n\nThanks,\nSuperStore.com"
msg.attach(MIMEText(message_body, 'plain'))
server = create_smtp_server()
if server is not None:
# Send the email if server is running
server.sendmail('[email protected]', email, msg.as_string())
server.quit()
else:
# or just print the message to the terminal
print(f"From: {msg['From']}\nTo: {msg['To']}\nSubject: {msg['Subject']}\n{message_body}")
return jsonify({'message': 'Coupons sent!'})
if __name__ == '__main__':
app.run(port=4000)
You can run the server by running python index.py
in your terminal.
If you see the message Webhook server is running on port 4000
, you're good to go!
Step 4: Test the setup
With your server running, Hasura should start hitting our endpoint. As we set our cron expression to 0 0 * * *
, the
webhook will be triggered at midnight every day. We don't want to wait that long to test it. For now, update the
expression to * * * * *
to trigger the webhook every minute. Then, check out your invocation logs in the Hasura
Console to verify that the webhook was triggered successfully and your terminal to see the outputted information and
link to a handy email 🎉
Feel free to customize the webhook implementation based on your specific requirements and chosen email sending service. Remember to handle error scenarios, implement necessary validations, and add appropriate security measures to your webhook endpoint.