All docsHelpTutorialsAdd a static map to an email receipt, part 2: back-end geocoding

Add a static map to an email receipt, part 2: back-end geocoding

Intermediate
JavaScript, Command line
Prerequisite

Familiarity with Node.js and command-line tools.

Tutorial series: Receipt map

This series of tutorials teaches you how to use the Mapbox Static Images API and the Mapbox Geocoding API to add a customized static map to an email receipt.

Adding location context, like receipt maps, to transaction emails helps customers quickly recognize where a transaction occurred, decreasing payment disputes. Static maps from the Mapbox Static Images API are a lightweight, email-friendly way to add location context to email receipts.

Static maps need a pair of latitude and longitude coordinates. This tutorial uses the Mapbox Geocoding API to geocode merchant addresses for a static map in an email receipt. Since we can’t use front-end JavaScript in emails to geocode addresses, we’ll write Node.js code to geocode merchant addresses before sending emails using the Twilio SendGrid Mail Send API.

Part 2 of this tutorial series does not cover email receipt template design and static map customization. See Part 1 of this tutorial for the front-end code to create static maps for email receipts.

Getting started

To complete this tutorial, you will need:

  • A Mapbox account and access token. Sign up for an account at mapbox.com/signup. You can find your access tokens on your account page.
  • Node.js. This tutorial uses Node.js to make API calls to the Mapbox Geocoding API and Twilio SendGrid Mail Send API. This tutorial requires Node version 14 or higher.
  • A Twilio SendGrid account, verified sender identity, and API key. Sign up for a Twilio SendGrid account at signup.sendgrid.com and follow the steps in Twilio’s Email API Quickstart for Node.js guide. While this tutorial uses Twilio SendGrid to send receipt emails, you are welcome to use your email delivery system of choice.
  • Starter code. This guide uses an HTML email template with sample transaction JSON data that you will copy and paste into Twilio SendGrid.
Download starter code

Project set up

Our restaurant transaction JSON data only includes a merchant address (476 K St NW). We need latitude and longitude coordinates for our Mapbox Static Images API call. We will use the Mapbox Geocoding API to convert our merchant address into a latitude and longitude coordinate pair ([-77.0187, 38.90227]).

Before we write any code to geocode addresses, we need to set up our project environment.

Back-end programming language

While this tutorial uses Node.js, you are welcome to rewrite this tutorial in your back-end language of choice.

  1. Create a new folder for this project called receipt-map: mkdir receipt-map
  2. Switch to your new project folder: cd receipt-map
  3. If you’re using Node version 14, run npm init to create a new package.json file. Use any values for the question prompts. If you’re using Node version 16 or higher, you can skip this step.
  4. Install got, which we will use to make HTTP requests to the Mapbox Geocoding API: npm install --save got
  5. Install the SendGrid helper library npm package: npm install --save @sendgrid/mail. We will use this package later in the tutorial.
  6. Add "type": "module", above dependencies in your package.json file. We need this to import the got library, which is a native ECMAScript module (ESM). If you’re using Node v16 or higher your package.json file should look like this, but with potentially different version numbers:
{
 "type": "module",
 "dependencies": {
   "@sendgrid/mail": "^7.6.2",
   "got": "^12.0.4"
 }
}
  1. Create a new environment variable in your shell called MAPBOX_ACCESS_TOKEN: export MAPBOX_ACCESS_TOKEN=YOUR_MAPBOX_ACCESS_TOKEN
  2. Create a new environment variable in your shell called SENDGRID_API_KEY: export SENDGRID_API_KEY=YOUR_SENDGRID_API_KEY
  3. Copy transaction.json from the starter code zip file into your receipt-map folder.
  4. Create a new file called email.js: touch email.js
  5. Add the following code to your email.js file:
import got from 'got';
import sendgrid from '@sendgrid/mail';
import { createRequire } from 'module';

const mapboxToken = process.env.MAPBOX_ACCESS_TOKEN;
sendgrid.setApiKey(process.env.SENDGRID_API_KEY);

const require = createRequire(import.meta.url);
const transaction = require('./transaction.json');

This code imports dependencies, requires sample transaction JSON data, and sets access tokens and API keys for the Mapbox Geocoding API and Twilio SendGrid Mail Send API.

Geocode merchant addresses

The syntax for the Mapbox forward geocoding endpoint is https://api.mapbox.com/geocoding/v5/{endpoint}/{search_text}.json?access_token={access_token}. A successful Mapbox Geocoding API call requires at least three parameters:

  • Endpoint: Since we’re displaying results on a Mapbox map and not permanently storing results, we’ll use mapbox.places for {endpoint}.
  • Search text: The {search_text} is the address of the restaurant, stored in transaction.merchant_data.address.
  • Access token: The Mapbox Geocoding API requires a public access token for billing. We can store our access token in the mapboxToken constant and add it to the end of our API request via access_token=${mapboxToken}. While public access tokens are not sensitive, we’ll store the Mapbox access token in our environment in process.env.MAPBOX_ACCESS_TOKEN as a best practice anyway to avoid hard coding tokens.
Visit the Geocoding API Playground to test out different queries

We can also add a few more optional parameters to further optimize our Geocoding API calls for the email receipt map use case:

  • Types: We are only geocoding merchant addresses, not other search data types. We can add types=address to improve the relevancy of results.
  • Limit: We’re only interested in the first result from the Mapbox Geocoding API. We can add limit=1 to our request to decrease the size of network requests and improve performance.
  • Country: Our sample payment processor only operates in the United States. We can add country=US to improve the relevancy of results.
  • Autocomplete: Our users aren’t typing addresses into an interactive geocoder control like mapbox-gl-geocoder. We can disable autocomplete with autocomplete=false to improve the accuracy of results.

Add the following code to your email.js file:

async function geocodeAddress(transaction) {
  const geocodeURL = `https://api.mapbox.com/geocoding/v5/mapbox.places/${transaction.merchant_data.address}.json?types=address&limit=1&country=US&autocomplete=false&access_token=${mapboxToken}`;
  const geocodingResults = await got(geocodeURL).json();
  const coordinates = geocodingResults.features[0].geometry.coordinates;

  return { longitude: coordinates[0], latitude: coordinates[1] };
}

The asynchronous geocodeAddress function uses got to forward geocode merchant addresses. Requests to the forward geocoding endpoint return an array of features, and we want the first result (geocodingResults.features[0]). We want the latitude and longitude coordinate pair stored in geometry.coordinates. Finally, we return the coordinate pair as an object. The two separate longitude and latitude properties are easier to use in Handlebars in Twilio email templates than a single array.

A second function called addCoordinates updates the transaction object to include the geocoded coordinates. Since we want to chain multiple functions together, addCoordinates returns a promise. Add this code to the bottom of your email.js file:

function addCoordinates(transaction, coordinates) {
  const geocodedTransaction = transaction;
  geocodedTransaction.merchant_data.coordinates = coordinates;

  return Promise.resolve(geocodedTransaction);
}

Finally, we run the asynchronous geocodeTransaction function, passing the promise (coordinates) to the addCoordinates function. Add this code to the bottom of your email.js file:

const geocodeTransaction = geocodeAddress(transaction);

geocodeTransaction
  .then((coordinates) => addCoordinates(transaction, coordinates))
  .then((geocodedTransaction) => console.log(geocodedTransaction));

Your email.js file should look like this:

import got from 'got';
import sendgrid from '@sendgrid/mail';
import { createRequire } from 'module';

const mapboxToken = process.env.MAPBOX_ACCESS_TOKEN;
sendgrid.setApiKey(process.env.SENDGRID_API_KEY);

const require = createRequire(import.meta.url);
const transaction = require('./transaction.json');

async function geocodeAddress(transaction) {
  const geocodeURL = `https://api.mapbox.com/geocoding/v5/mapbox.places/${transaction.merchant_data.address}.json?types=address&limit=1&country=US&autocomplete=false&access_token=${mapboxToken}`;
  const geocodingResults = await got(geocodeURL).json();
  const coordinates = geocodingResults.features[0].geometry.coordinates;

  return { longitude: coordinates[0], latitude: coordinates[1] };
}

function addCoordinates(transaction, coordinates) {
  const geocodedTransaction = transaction;
  geocodedTransaction.merchant_data.coordinates = coordinates;

  return Promise.resolve(geocodedTransaction);
}

const geocodeTransaction = geocodeAddress(transaction);

geocodeTransaction
  .then((coordinates) => addCoordinates(transaction, coordinates))
  .then((geocodedTransaction) => console.log(geocodedTransaction));

Run node email.js. You should see the transaction JSON data in the console but with a new coordinates property with a value of { longitude: -77.0187, latitude: 38.90227 }.

Create an email template

We need to create an email template to send emails with Twilio SendGrid. The template.html file from the starter code zip file contains the email receipt template we created in Part 1 of this tutorial series.

Follow the steps below to create a dynamic transactional template in Twilio SendGrid:

  1. Download the starter code zip file from the Getting Started section.
  2. Unzip the zip file.
  3. Open template.html and transaction.json from the zip file in a text editor.
  4. Login to Twilio SendGrid in a web browser.
  5. Click on Email API.
  6. Click on Dynamic Templates.
  7. Click on Create a Dynamic Template.
  8. Type Receipt for the Dynamic Template Name.
  9. Click Create.
  10. Click on Receipt under Template.
  11. Click on Add Version.
  12. Select Blank Template.
  13. Select the Code Editor option.
  14. Click on Test Data.
  15. Copy and paste transaction.json into the Test Data panel.
  16. Click on Code.
  17. Delete the pre-populated HTML code in the Code panel.
  18. Copy and paste template.html into the Code panel. Your email receipt template should not have a static map yet.
  19. Replace YOUR_MAPBOX_ACCESS_TOKEN in line 126 with your Mapbox access token.
  20. Click on Settings.
  21. Type v1 under Version Name.
  22. Type Your receipt for the Subject.
  23. Click on Save.
  24. Click on the left arrow in the top left-hand corner to exit the template editor.
  25. Click on Receipt.
  26. You should see the template ID in the menu. Copy this value and save it for the next step.

Send emails

Now that we have a template in Twilio SendGrid, we can use our transaction data - with geocoded coordinates - as dynamic template data.

Add the following code after line 24 of your email.js file. Make sure to update the code with your testing email address, verified sender email address, and dynamic template ID.

function sendEmail(transaction) {
  const message = {
    to: 'customer@example.com', // replace with your testing email address
    from: 'sender@example.com', // replace with your verified sender email address
    templateId: 'd-bee9c2f190964fc6b7d5e9c1ea8b9cb4', // replace with your template ID
    dynamicTemplateData: transaction
  };

  sendgrid
    .send(message)
    .then((response) => {
      console.log(response[0].statusCode);
      console.log(response[0].headers);
    })
    .catch((error) => {
      console.error(error);
    });
}

Finally, to run all three functions in the correct order (geocode, update transaction object, then send an email with the updated transaction object), replace lines 47-49 with the following code:

geocodeTransaction
  .then((coordinates) => addCoordinates(transaction, coordinates))
  .then((geocodedTransaction) => sendEmail(geocodedTransaction));

Run node email.js to send your email receipt with a static map. You should see logs for a 202 status code and headers in the console. You should also receive your test email with the subject “Your Receipt” in the inbox of your testing email address.

Final product

In part 1 of this tutorial series, you built an email receipt for a restaurant transaction with a static map using the Mapbox Static Images API. In part 2, you used the Mapbox Geocoding API to geocode merchant addresses for static maps in email receipts.

import got from 'got';
import sendgrid from '@sendgrid/mail';
import { createRequire } from 'module';

const mapboxToken = process.env.MAPBOX_ACCESS_TOKEN;
sendgrid.setApiKey(process.env.SENDGRID_API_KEY);

const require = createRequire(import.meta.url);
const transaction = require('./transaction.json');

async function geocodeAddress(transaction) {
  const geocodeURL = `https://api.mapbox.com/geocoding/v5/mapbox.places/${transaction.merchant_data.address}.json?limit=1&country=US&autocomplete=false&access_token=${mapboxToken}`;
  const geocodingResults = await got(geocodeURL).json();
  const coordinates = geocodingResults.features[0].geometry.coordinates;

  return { longitude: coordinates[0], latitude: coordinates[1] };
}

function addCoordinates(transaction, coordinates) {
  const geocodedTransaction = transaction;
  geocodedTransaction.merchant_data.coordinates = coordinates;

  return Promise.resolve(geocodedTransaction);
}

function sendEmail(transaction) {
  const message = {
    to: 'customer@example.com', // replace with your testing email address
    from: 'sender@example.com', // replace with your verified sender email address
    templateId: 'd-bee9c2f190964fc6b7d5e9c1ea8b9cb4', // replace with your template ID
    dynamicTemplateData: transaction
  };
  sendgrid
    .send(message)
    .then((response) => {
      console.log(response[0].statusCode);
      console.log(response[0].headers);
    })
    .catch((error) => {
      console.error(error);
    });
}

const geocodeTransaction = geocodeAddress(transaction);

geocodeTransaction
  .then((coordinates) => addCoordinates(transaction, coordinates))
  .then((geocodedTransaction) => sendEmail(geocodedTransaction));

Next steps

Learn how to customize your receipt map further and build for finance or retail use cases.