A Q&A with Commerce Layer's CTO about our caching strategy.

Tutorials

How to send templated emails with Commerce Layer and SendGrid.

October 13, 2022 Bolaji Ayodeji

In the previous article of our Webhooks series, we discussed how you can use our webhooks technology to send order confirmation emails to customers when they make a new purchase using Zapier and SendGrid. But as we observed, the email we sent wasn’t styled nicely like the emails you receive from your favorite brands. In this article, we will further discuss how to create a styled dynamic template in SendGrid, write a lambda function inside our existing Zapier actions to intercept the payload data from Commerce Layer, and apply the template before sending the email through SendGrid API. As a bonus, we will also show you how to do this with a custom Nodejs server (or any language you choose). Sounds fun, right? Let’s get right into it, then.

Sneak Peek 😉

Prerequisites

  • You have read the first and second parts of the webhook series.
  • A general understanding of how APIs work.
  • Basic HTML, CSS, and JavaScript knowledge.
  • You should have a Commerce Layer account, have set up your organization, and have created the required commerce data resources for your market. You can follow the onboarding tutorial or manual configuration guide to achieve this.
  • Create an integration application and save your credentials (Client ID, Client secret, and Base endpoint) by following this guide in our documentation.

Getting started with SendGrid

SendGrid is a service that provides you with an Email API and some other communication APIs you can use to send emails to certain recipients. If you’re new to using SendGrid, follow the steps below to create a SendGrid account:

  1. First, create a SendGrid account if you don't have one already.
  2. Next, verify your account and set up 2FA.
  3. Finally, create and verify a Single Sender identity account (PS: this is for testing purposes; for production use, it's recommended to use the Domain Authentication Identity.
  4. That's all :)

Creating a dynamic Sendgrid template

Before we proceed to set up our Zapier integration for sending emails, we need first to create a styled template we will use to send the emails. We will then use this template and populate it with dynamic data coming in from Commerce Layer when the Zap is triggered.

Kindly follow the steps below to create and design a dynamic template:

  1. Visit the Dynamic Transactional Templates page and click the ”Create a Dynamic Template” button.
  2. Enter a unique name for the template and then click Save.
  3. Click on the created template and click the “Add Version” button.
  4. Pick a design from one of SendGrid’s official templates or select a blank template to design from scratch.
  5. Select the Design or Code editor and click Continue (you can also import an HTML email during editing).
  6. Now you can begin designing your template.


SendGrid allows you to add dynamic data using Handlebars in the template and will render the actual data via the API. When we send order confirmation emails to customers, we will be able to personalize the messages in the template by inserting dynamic data. For example, suppose you have a customer's name stored in a JSON property called "customerName". In that case, you can insert the property's value into a template by adding {{customerName}} wherever you want the customer's name to appear. If the data includes characters like ', " or &, you can use the “triple-stash” like so {{{shippingAddress}}}. The same goes for other properties like customer email, order summary details, shipment tracking URLs, etc.

This might be the most challenging part of this tutorial, as you will need some design and basic HTML/CSS knowledge to design the perfect email template. However, we believe you already have excellent designers if you're a brand. You can have them design something for all emails you want to send to your customers. Your frontend engineers can then use SendGrid to implement the designs using the drag-and-drop editor, code editor, or HTML import. All you need to do is add substitution tags anywhere you want to add dynamic data, and you can preview them using test data right inside SendGrid, as seen in the screenshots below:

The test data you will use with the Sendgrid template will be in JSON format like so:

{
  "customerName": "Bolaji",
  "orderTimeStamp": "2022-09-30T19:44:05.519Z",
  "dateFormat": "MMMM DD, YYYY",
  "shipmentNumber": "#21527675/S/001",
  "orderNumber": "21527675",
  "customerEmail": "bolaji@commercelayer.io",
  "marketName": "USA",
  "shippingAddress": "Bolaji Ayodeji, 333 Fremont Street, 94016 San Francisco CA (US) +14845219712",
  "shippingMethod": "Express Delivery",
  "paymentMethod": "Wire Transfer",
  "lineItems": "...",
  "totalAmount": "$500",
  "shippingAmount": "$50",
  "grandTotalAmount": "$550"
}

We will then use the handlebars templating language to render any property we want. If your data includes an array of objects, you can iterate over the data for the line items in the array using the {{#each }} helper function to build a list of all the objects in the array. This will look something like this depending on how you want to style your template:

{{#each lineItems}}
<div style="font-size: 14px; font-family: 'courier new', courier, monospace">
  <img
    src="{{{this.skuImage}}}"
    alt="{{{this.skuName}}}"
    style="width: 10%; vertical-align: middle"
  />
    {{{this.skuName}}} x{{{this.skuQuantity}}}
  (<span style="color: #666eff">{{{this.skuFormattedAmount}}}</span>)
</div>
{{/each}}

But since the Commerce Layer Zapier app already returns an HTML property with all the line items summary, we can use that. Also, if you require more styling options, a good tip is to use the global styles or advanced options to add styles to the head of the email because most email clients work better with inline styles than with external styles. For example, we will put the styles for the SKU image and line items table in the HTML head like so:

<style>
  .line-item-image img {
    margin-left: auto;
    margin-right: auto;
    display: block;
  }
  img {
    width: 50px;
    height: auto;
  }
  .line-items {
    width: 100%;
  }
  .line-items td {
    font-family: courier, monospace;
    border: 1px solid #666eff;
    padding: 5px;
  }
</style>

And this is pretty much all you need to do at this stage. I believe you will design something even better than this and feed it with the right data. If you want, you can download the code for the template we designed, import it into your SendGrid account, and customize it as you desire.

The next hurdle is to figure out how to receive live data from Commerce Layer, process them in Zapier, parse the data to look exactly like the test JSON data above, and write some code to send an email to the customer when a new order is placed using the dynamic data.

Getting started with Zapier

Now, let's show you how you can complete the integration with our webhooks, lambda functions, and SendGrid. To achieve this, we will utilize our private Zapier app (which is still in beta at the time of writing this article) that will allow you to easily integrate 3000+ third-party services with Commerce Layer and build any automation workflow in minutes. So, kindly follow the steps below to get started:

  1. Create a Zapier account if you don't have one yet.
  2. Use this invite link to install our private Commerce Layer Zapier app.
  3. Now, create a Zap, an automated workflow that connects apps and services (like Commerce Layer and SendGrid in our case).

Remember that when you turn your Zap on, it will run all the action steps every time the trigger event occurs. For example, the Commerce Layer webhook will be the trigger event, and the corresponding action is to run a lambda function and send some data to SendGrid, which in turn delivers that data to a customer via email.

Just for a refresher or for those reading the third part of this series first, here’s how to create the Commerce Layer trigger event:

Trigger event

  1. Search for “Commerce Layer” and select the app in the App Event field. Then select the latest version.
  2. Select a New Order Event trigger event.
  3. Click the button to sign in to your Commerce Layer account.
  4. Enter your organization hostname, Client ID, and Client Secret. You should have this already from the prerequisite step where you created an integration application earlier.
  5. Click “Yes continue,” and you should be authorized if you entered valid credentials.
  6. Click continue and select the place order event. You can choose another option based on the type of trigger you want. But since we want to send a notification when an order is placed successfully (which would have a placed status), then place event works for us. You can learn more about the anatomy of an order in our data model documentation.
  7. Click “Continue” and the test trigger button, and the app will find a recent order in your Commerce Layer organization to confirm that the correct account is connected and your trigger is set up correctly. This will return the last order we created.

Action (code by Zapier)

Now the fun part 🙃. Let's send an email to the customer who placed the order with details of their order by creating another action. In this action, we will write code to send an email with SendGrid API, include a styled template, and add dynamic variables to the email content from the Commerce Layer webhook payload. Kindly follow the steps below to set the action up:

  1. Search for “Code by Zapier” and select the app in the App Event field.
  2. Select the Run JavaScript action event (you can also select Run Python if you prefer to write your code with Python programming language).
  3. Create a SendGrid API key with Full access “Main Send” permission by navigating to the SendGrid dashboard and opening Settings > API Keys.
  4. Create some input data variables with the data from Commerce Layer, as seen in the screenshot below. These variables can be accessed as strings within code using inputData.keyName or inputData['keyName'].

Now, in the code field, add the following JavaScript code, including your template ID:

const apiKey = "YOUR_API_KEY";

await fetch("https://api.sendgrid.com/v3/mail/send", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${apiKey}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    from: {
      email: "bolaji@commercelayer.io",
      name: "Cake Store Team",
    },
    reply_to: {
      email: "bolaji@commercelayer.io",
      name: "Cake Store Customer Support",
    },
    personalizations: [
      {
        to: [
          {
            email: inputData.customerEmail,
          },
        ],
        dynamic_template_data: {
          customerName: inputData.customerName,
          orderTimeStamp: inputData.orderTimeStamp,
          dateFormat: "MMMM DD, YYYY",
          shipmentNumber: `${
            inputData.shipmentNumber.match(/#\d{8}\/S\/\d{3}/g)[0]
          }`,
          orderNumber: inputData.orderNumber,
          customerEmail: inputData.customerEmail,
          marketName: inputData.marketName,
          paymentMethod: inputData.paymentMethod,
          shippingAddress: inputData.shippingAddress,
          shippingMethod: inputData.shippingMethod,
          lineItems: inputData.lineItems,
          totalAmount: inputData.totalAmount,
          shippingAmount: inputData.shippingAmount,
          grandTotalAmount: inputData.grandTotalAmount,
        },
      },
    ],
    template_id: "d-cd0b22632dcc427cb2b98e3bgg562e5a",
  }),
})
  .then((response) => {
    output = response;
    console.log(`Email sent to ${inputData.customerEmail}!`);
    console.log(output);
  })
  .catch((error) => {
    console.error(error);
  });
You don't need to write a function for the script; the “Code by Zapier” action already wraps the code in an async function.

And that's all! You can now test the action or publish the zap. As discussed earlier, turning the zap on will make the entire automation run when the trigger occurs. If you want more flexibility, consider using the AWS Lambda Zapier action (although it’s a premium service). You should also know that your test email might be delivered to the Spam folder. To avoid this, you need to authenticate your SendGrid sender identity, as mentioned earlier.

If you want to test this yourself, I have created an SKU List and a sample MicroStore specially for YOU. With this, you can purchase some items, checkout with your email/shipping details, and select Wire Transfer as the payment method (PS: this is a test order and not real :-)). This will place a test order with no charge, and you will receive a confirmation email (the Zap will continue pending until our Zapier test account exceeds the monthly free tasks limit, so let’s call this a special treat for early readers 😉).

Conclusion

We hope you have learned more ways to utilize our webhooks feature! You can also consider other email API platforms like Postmark that provide a similar experience for sending dynamic templates. If you have followed this series so far, you would understand that you can do this entire integration without Zapier if you want more flexibility and deploy the service yourself. All you have to do is create a server, create an endpoint to serve as a callback for the webhook, receive the data from Commerce Layer, filter and format out the data you need for the email, and use one of SendGrid mail libraries (depending on your choice of programming language) to write a mail script (passing in the dynamic data inside an object). However, using serverless or Lambda functions when you deploy your custom server is wise due to scaling concerns. This would look something like the code below if written with JavaScript and Nodejs:

require("dotenv").config();
const express = require("express");
const CryptoJS = require("crypto-js");
const hmacSHA256 = require("crypto-js/hmac-sha256");

const app = express();
const port = 9000;

// Fetch RAW incoming JSON requests and put in req.rawBody
app.use(
  express.json({
    limit: "5mb",
    verify: (req, res, buf) => {
      req.rawBody = buf.toString();
    },
  })
);

app.post("/callback", (req, res) => {
  // Verify the payload coming from Commerce Layer
  const signature = req.headers["x-commercelayer-signature"];
  const hash = hmacSHA256(req.rawBody, process.env.CL_SHARED_SECRET);
  const encode = hash.toString(CryptoJS.enc.Base64);
  if (req.method === "POST" && signature === encode) {
    const payload = req.body;

    // Filter payload and fetch the required order summary
    const lineItemsArray = payload.included.filter(
      (x) => x.type === "line_items" && x.attributes.item_type === "skus"
    );

    const filteredLineItemsArray = lineItemsArray.map((item) => {
      return {
        image_url: item.attributes.image_url,
        name: item.attributes.name,
        quantity: item.attributes.quantity,
        formatted_total_amount: item.attributes.formatted_total_amount,
      };
    });

    const inputData = {
      customerName: payload.included[2].attributes.first_name,
      customerEmail: payload.data.attributes.customer_email,
      orderNumber: payload.data.attributes.number.toString(),
      orderTimeStamp: payload.data.attributes.placed_at,
      marketName: payload.included[0].attributes.name,
      paymentMethod: payload.included[4].attributes.name,
      shippingAddress: payload.included[2].attributes.full_address,
      shipmentNumber: payload.included.find((x) => x.type === "shipments")
        .attributes.number,
      shippingMethod: payload.included.find(
        (x) => x.type === "shipping_methods"
      ).attributes.name,
      lineItemsHtml:
        `<table class="line-items">` +
        filteredLineItemsArray
          .map(
            (item) =>
              `<tr class="line-item"><td class="line-item-image"><img src="${item.image_url}" alt="${item.name}" /></td><td class="line-item-name">${item.name}</td><td class="line-item-qty">${item.quantity}</td><td class="line-item-amount">${item.formatted_total_amount}</td></tr>`
          )
          .join("") +
        `</table>`,
      totalAmount: payload.data.attributes.formatted_subtotal_amount,
      shippingAmount: payload.data.attributes.formatted_shipping_amount,
      grandTotalAmount: payload.data.attributes.formatted_total_amount,
    };

    // Send Email with SendGrid
    const sgMail = require("@sendgrid/mail");
    sgMail.setApiKey(process.env.SENDGRID_API_KEY);

    sgMail
      .send({
        to: inputData.customerEmail,
        from: {
          email: "bolaji@commercelayer.io",
          name: "Cake Store Team",
        },
        reply_to: {
          email: "bolaji@commercelayer.io",
          name: "No Reply",
        },
        templateId: process.env.SENDGRID_TEMPLATE_ID,
        dynamicTemplateData: {
          customerName: inputData.customerName,
          orderTimeStamp: inputData.orderTimeStamp,
          dateFormat: "MMMM DD, YYYY",
          shipmentNumber: inputData.shipmentNumber,
          orderNumber: inputData.orderNumber,
          customerEmail: inputData.customerEmail,
          marketName: inputData.marketName,
          paymentMethod: inputData.paymentMethod,
          shippingAddress: inputData.shippingAddress,
          shippingMethod: inputData.shippingMethod,
          lineItems: inputData.lineItemsHtml,
          totalAmount: inputData.totalAmount,
          shippingAmount: inputData.shippingAmount,
          grandTotalAmount: inputData.grandTotalAmount,
        },
      })
      .then((response) => {
        console.log(response);
        res.status(200).json({
          message: `Email sent to customer (${inputData.customerEmail})!`,
          response: response,
        });
      })
      .catch((error) => {
        console.error(error);
        res.status(500).json({
          error: error,
        });
      });
  } else {
    res.status(401).json({
      error: "Unauthorized: Invalid signature",
    });
  }
});

app.listen(port, () => {
  console.log(`Listening at http://localhost:${port}`);
});

I’ll imagine you will have email servers like this for several transactional emails you will be sending to customers, like shipment confirmation, signup welcome, password reset, order cancellation, shipment updates, recommendations, promotion activation, customer group invite, order subscriptions, and lots more. In addition, you can explore this GitHub repository containing all the code used in this tutorial for further learning and code customization. Thank you for reading this far; cheers!