Sending Letters using the API

When your business requires sending mail in an effective in scalable manner, PostGrid has your solution. In this guide, we will examine how to send letters using the PostGrid API. For more information on how to use the API, be sure to consult the official documentation.

Overview

There are four stages in the process of sending letters with PostGrid:

  • Creating and designing templates
  • Creating contacts
  • Sending letters
  • Tracking letters and other analytics

Whether you have a small operation with no developers or a large operation with engineers on hand, each of these four stages have tools at a variety of different levels to accommodate. In this guide, we aim to show you how you can use our API to accomplish each of these four stages and how you can safely test out your workflow. Due to the flexibility of the API endpoints, we may actually skip the setup steps of creating templates and creating contacts but this guide will cover how to use the features as well since it generally creates good practice to have the templates and contacts easily accessible even after the mail is sent.

Live and Test Modes

To make sure that your customers are receiving the exact letter you wish to intend, PostGrid offers both a test mode and a live mode. In test mode, actual letters are not sent out but the process of creating orders is completed all the same. This allows you to ensure that variable data is assigned properly and when you preview a letter, it adheres to postal standards and looks exactly as it should. When you are ready to actually send the letters, you can switch to live mode and repeat the sending process with everything set up as before. Letters sent in test mode will not be delivered, even after switching to live mode. More details on these features will be discussed in the tracking section.

Environment and Setup

In this guide, we will be using Node.js as well as the module node-fetch to make our API calls and make it easy to understand how this can be used in the browser as well. For alternative language support, see the official documentation as well. If you have any questions concerning the JavaScript Fetch API, a great resource is the MDN Web Docs. To install this module, simply run

npm install node-fetch

Next, create a new file which we will call app.js. As we will be focused on making primarily asynchronous API calls, let's add some helper functions and constants throughout this document.

const fetch = require('node-fetch');

const POSTGRID_URL = 'https://api.postgrid.com/print-mail/v1';

const API_KEY = '' // Place your API key here

/**
 * @param {Promise<any>} x
 */
async function logAwait(x) {
    try {
        console.log(await x);
    } catch (e) {
        console.error(e);
    }
}

To only call the specific functions we want, we will be using the Node.js REPL by running the command

node -i -e "$(< app.js)"

or equivalently on Windows PowerShell

node -i -e "$(type app.js)"

which will load our functions into the REPL and can be called from there. If you name your file something else, be sure to replace app.js in the above.

In a Windows command terminal, loading files into the REPL is nowhere near as convenient but can be done if we run node to open the REPL and then run the command .load app.js. This will populate the terminal with the contents of your file line-by-line and may take a short period of time to load. As the file grows in scale, you may wish to include any logging at the end of your file as logAwait(someAsyncFunction()) and run the script in the usual way as node app.js.

Also, as you can see from the function logAwait, we will be adding annotations to our code to explain what you can expect from each of the parameters being used. For more information, be sure to have a look at JSDoc. If you are familiar with TypeScript, these annotations can be seen as a way to add typing to a JS file without using TypeScript syntax. These annotations will also allow us easy access to autocomplete within our code.

Creating Templates

With PostGrid, all letters require that you either supply a PDF or a template to form the basis of the letter. Templates allow you to dynamically create letters based upon the specifics of a given contact or any other information. For an in-depth guide on how to create templates using the API, read this guide. For an easy option of creating templates, you can check out our guide here on using PostGrid's built-in visual editor. When actually sending the letter, you only need to use the template's ID which can be found in the dashboard.

Using PDFs

If you don't need to create a template but instead have PDFs already ready to send to specific contacts, then you can move right along to the sending part. However, you will still need to read the following section to be sure of the format of the PDF letters you wish to send.

Formatting Concerns

When sending and printing mail, we need to keep in mind where the address will be placed as well as where the margins can be found. In the official API documentation (here), you can find the specifics for Canadian letters and for US & international letters. To avoid having the address cover a specific part of your letter, we will see below how we can add an additional page dedicated to just the address.

Creating Contacts

Before we can get started with sending letters, we will need to specify who the letters are from and who they are going to. Although contact creation may be easily done using the dashboard, PostGrid still offers the ability to create and view contacts directly from the API. For an in-depth guide on how to create contacts using the API, read this guide.

Sending Letters

With our contacts and templates setup, we are ready to send some (test) mail. In this section, we will cover each of the different ways you can send mail by making API calls. Over these sections, be sure to use the test key when following along to make sure that you do not send any real mail.

Sending Template Letters to Contacts

The quickest and easiest way to send mail is if you have the contact IDs and template ID for the mail you wish to send. Examining the documentation, we can achieve this with a simple POST call to /letters. Let's now create a function to consume some contact IDs and a template ID and create a letter.

/**
 * @param {string} toContactID
 * @param {string} fromContactID
 * @param {string} templateID
 */
async function createLetter(toContactID, fromContactID, templateID) {
    const requestOptions = {
        method: 'POST',
        headers: {
            'x-api-key': API_KEY,
            'Content-Type': 'application/json',
        },
        body: JSON.stringify({
            to: toContactID,
            from: fromContactID,
            template: templateID,
        }),
    };

    const resp = await fetch(POSTGRID_URL + '/letters', requestOptions);

    return await resp.json();
}

Using this function, let's now send a letter with some given contacts and the ID for the template associated to the image below, which was made in the template editor.

Previewing this letter (more info on this below), we find the following.

Unfortunately, it seems as though the address placement has blocked some of our design and our colors have disappeared. We can fix this with some additional fields we can add to the body of our request. Specifically, we need to tell the API the values of addressPlacement, color, and doubleSide. If we also take a look at the dashboard to find this preview, we are missing a letter description, which can be added with the description field. Let's extend our function to accommodate for these options.

/**
 * @param {string} toContactID
 * @param {string} fromContactID
 * @param {string} templateID
 * @param {boolean} color
 * @param {boolean} doubleSided
 * @param {boolean} blankPageForAddress
 * @param {string} description
 */
async function createLetterWithOptions(
    toContactID,
    fromContactID,
    templateID,
    color,
    doubleSided,
    blankPageForAddress,
    description
) {
    const requestOptions = {
        method: 'POST',
        headers: {
            'x-api-key': API_KEY,
            'Content-Type': 'application/json',
        },
        body: JSON.stringify({
            to: toContactID,
            from: fromContactID,
            template: templateID,
            addressPlacement: blankPageForAddress
                ? 'insert_blank_page'
                : 'top_first_page',
            color: color,
            doubleSided: doubleSided,
            description: description,
        }),
    };

    const resp = await fetch(POSTGRID_URL + '/letters', requestOptions);

    return await resp.json();
}

Using this function and setting blankPageForAddress to true and color to true, our preview will now include two pages with our second page being the template shown in color and listed with its description.

Sending Without a Template or Contacts

In case you wished to skip the previous two sections and wanted to jump to sending letters straightaway, or already have your template HTML and contact info on hand and don't want to create these ahead of time, PostGrid's API allows you to send letters in one step. Using the same API endpoint, all we must do is switch from setting to and from to contact IDs to setting these values to contacts themselves and setting field html instead of setting the field template. For more information as to what you can fill out the fields to and from with, see either the official documents for what a Contact type is or see the creation of contacts above.

/**
 * @typedef {{
 *  addressLine1:string,
 *  addressLine2:string?,
 *  city:string?,
 *  provinceOrState:string?,
 *  postalOrZip:string?,
 *  country:string?,
 *  firstName:string?,
 *  lastName:string?,
 *  email:string?,
 *  phoneNumber:string?
 *  companyName:string?,
 *  jobTitle:string?
 *  metadata: any
 * }} Contact
 *
 * @param {Contact} to
 * @param {Contact} from
 * @param {string} html
 */
async function createLetterFromScratch(to, from, html) {
    const requestOptions = {
        method: 'POST',
        headers: {
            'x-api-key': API_KEY,
            'Content-Type': 'application/json',
        },
        body: JSON.stringify({
            to: to,
            from: from,
            html: html,
        }),
    };

    const resp = await fetch(POSTGRID_URL + '/letters', requestOptions);

    return await resp.json();
}

In the return of such an API call, you can view the response to see the contact information and HTML provided. If we were to create a contact with information

const exampleContact = {
    addressLine1: '47 Dietz Ave S',
    city: 'Waterloo',
    provinceOrState: 'Ontario',
    firstName: 'John',
    lastName: 'Adams',
    jobTitle: 'Software Engineer',
    companyName: 'Solutions Technology Inc.',
};

and send a simple HTML text as 'Hello friend', we receive the following.

{
    "id":"letter_kkifTGq3haagbBnfy4yPKg",
    "object":"letter",
    "live":false,
    "addressPlacement":"top_first_page",
    "color":false,
    "doubleSided":false,
    "from":{
        "id":"contact_aPLaHaphgkPdUqVQyFWAHg",
        "object":"contact",
        "addressLine1":"47 DIETZ AVE S",
        "addressLine2":null,
        "addressStatus":"verified",
        "city":"WATERLOO",
        "companyName":"Solutions Technology Inc.",
        "country":"CANADA",
        "countryCode":"CA",
        "firstName":"John",
        "jobTitle":"Software Engineer",
        "lastName":"Adams",
        "postalOrZip":null,
        "provinceOrState":"ONTARIO"
    },
    "html":"Hello friend!",
    "sendDate":"2021-09-07T20:56:12.226Z",
    "status":"ready",
    "to":{
        "id":"contact_aPLaHaphgkPdUqVQyFWAHg",
        "object":"contact",
        "addressLine1":"47 DIETZ AVE S",
        "addressLine2":null,
        "addressStatus":"verified",
        "city":"WATERLOO",
        "companyName":"Solutions Technology Inc.",
        "country":"CANADA",
        "countryCode":"CA",
        "firstName":"John",
        "jobTitle":"Software Engineer",
        "lastName":"Adams",
        "postalOrZip":null,
        "provinceOrState":"ONTARIO"
    },
    "createdAt":"2021-09-07T20:56:12.229Z",
    "updatedAt":"2021-09-07T20:56:12.229Z"
}

Notice that a contact is created for each the to and from contacts with the address status checked and corrected where applicable. Since we are in test mode, our letter will inform us that the addresses are verified but they aren't actually verified unless we are in live mode. We can also mix-and-match sending with or without templates/contacts suiting your needs.

Sending PDF Letters

If you would like to avoid using templates altogether and already have some PDF files handy, PostGrid offers you two ways of achieving this: using a PDF file directly, or using a public URL to a PDF file. For either method, we will again be sending a POST request to /letters. Starting with the first of the two, we will need to add the line

const fs = require('fs');

to the top of our file app.js in order for us to be able to access the local filesystem. Next, let's add in our function which consumes some contact IDs, the path to our file, and whichever name we'd like to give our file.

/**
 * @param {string} toContactID
 * @param {string} fromContactID
 * @param {string} filePath
 * @param {string} fileName
 */
async function createLetterWithPDF(
    toContactID,
    fromContactID,
    filePath,
    fileName
) {
    const formData = new FormData();
    formData.append('to', toContactID);
    formData.append('from', fromContactID);
    formData.append('pdf', fs.createReadStream(filePath), fileName);

    const requestOptions = {
        method: 'POST',
        headers: {
            'x-api-key': API_KEY,
            'Content-Type': 'multipart/form-data',
        },
        body: formData,
    };

    const resp = await fetch(POSTGRID_URL + '/letters', requestOptions);

    return await resp.json();
}

Notice that in this case we will have to use the FormData object to upload our file. Similarly, if we were to send our request as we had for the other functions, we can easily send a letter with a publicly accessible link to our PDF.

/**
 * @param {string} toContactID
 * @param {string} fromContactID
 * @param {string} pdfLink
 */
async function createLetterWithPDFLink(toContactID, fromContactID, pdfLink) {
    const requestOptions = {
        method: 'POST',
        headers: {
            'x-api-key': API_KEY,
            'Content-Type': 'application/json',
        },
        body: JSON.stringify({
            to: toContactID,
            from: fromContactID,
            pdf: pdfLink,
        }),
    };

    const resp = await fetch(POSTGRID_URL + '/letters', requestOptions);

    return await resp.json();
}

Adding Values to Template Variables

Now that we've sent some basic test letters, let's try sending some letters containing variable data. What gives you the ability to add values for the variables set in the HTML editor will be the field mergeVariables. For our example, we will be using the template below.

From the sending process alone, we will not have to worry about informing the API of the values of to.firstName or from.firstName as these will be found from the respective contact information. That just leaves us with providing the value for amount. Let's create a function to consume an amount owing as well as the contact information of the sender and receiver.

/**
 * @param {string} toContactID
 * @param {string} fromContactID
 * @param {number} amount
 * @param {string} amountOwingTemplateID
 */
async function sendAmountOwingLetter(
    toContactID,
    fromContactID,
    amount,
    amountOwingTemplateID
) {
    const requestOptions = {
        method: 'POST',
        headers: {
            'x-api-key': API_KEY,
            'Content-Type': 'application/json',
        },
        body: JSON.stringify({
            to: toContactID,
            from: fromContactID,
            template: amountOwingTemplateID,
            description: `Informing of outstanding amount $${amount}`,
            mergeVariables: {
                amount: `$${amount}`,
            },
        }),
    };

    const resp = await fetch(POSTGRID_URL + '/letters', requestOptions);

    return await resp.json();
}

After sending the letter, we find that everything fills in nicely into our PDF preview.

If you wanted to associate the variable amount to individual contacts without having to store a table of contacts in your own database, you can add the variable amount to the metadata of each contact and revise the variable in the template to {{to.metadata.amount}} instead of {{amount}}.

Tracking Letters

With your mail sent, it is now to time to track and view your letters to make sure that everything is in order and that everything sent successfully.

Tracking an Individual Letter

Our first task will be to retrieve a singular letter. For this section, it will be important that you have kept the ID of your letter which was returned by the API when it was created in the previous section. If you did not record it, or would not like to save this information, we will go over the search functionality in the next section to see how you may still receive the information on your letters without saving the ID. In order to obtain the information on our individual letter, we will need to send a GET request to /letters/:id. Let's design a very basic function to perform exactly this.

/**
 * @param {string} letterID
 */
async function retrieveLetter(letterID) {
    const requestOptions = {
        method: 'GET',
        headers: {
            'x-api-key': API_KEY,
        },
    };

    const resp = await fetch(
        POSTGRID_URL + `/letters/${letterID}`,
        requestOptions
    );

    return await resp.json();
}

Let's pass the output of this function through logAwait to see what we get.

{
    "id":"letter_cLW8gqY6iibzoHfjCAFAck",
    "object":"letter",
    "live":false,
    "addressPlacement":"top_first_page",
    "color":false,
    "description":"Informing of outstanding amount $250",
    "doubleSided":false,
    "from":{
        "id":"contact_uYbXrhkRMbBwSJ77Ca2EYL",
        "object":"contact",
        "addressLine1":"200 UNIVERSITY AVE W",
        "addressLine2":"",
        "addressStatus":"verified",
        "city":"WATERLOO",
        "companyName":"PostGrid",
        "country":"CANADA",
        "countryCode":"CA",
        "description":"This contact will be used to send letters.",
        "email":"[email protected]",
        "firstName":"Nolan",
        "jobTitle":"Backend Customer Success Engineer",
        "phoneNumber":"",
        "postalOrZip":"N2L 3G1",
        "provinceOrState":"ON"
    },
    "mergeVariables":{
        "amount":"$250"
    },
    "pageCount":1,
    "sendDate":"2021-09-08T14:28:47.551Z",
    "status":"ready",
    "template":"template_j9ZEDbtx8JHqhxURZ8CcoG",
    "to":{
        "id":"contact_bReKn6B2S3b5d7bpTYLcE8",
        "object":"contact",
        "addressLine1":"47 DIETZ AVE S",
        "addressLine2":null,
        "addressStatus":"verified",
        "city":"WATERLOO",
        "companyName":"Solutions Technology Inc.",
        "country":"CANADA",
        "countryCode":"CA",
        "firstName":"John",
        "jobTitle":"Software Engineer",
        "lastName":"Adams",
        "postalOrZip":null,
        "provinceOrState":"ONTARIO"
    },
    "url":"https://eksandbox.s3.amazonaws.com/test/letter_cLW8gqY6iibzoHfjCAFAck?AWSAccessKeyId=AKIA5GFUILSUDYW4YKAG&Expires=1631121471&Signature=OsDvIuDblLDIxxNLpTIzfTErXk0%3D",
    "createdAt":"2021-09-08T14:28:47.554Z",
    "updatedAt":"2021-09-08T14:28:50.288Z"
}

In the above, we see that we have access to all of the information of the letter including information regarding the contacts, the template, variables included, and many more. Discussed further in the 'Letter Progress' section will be the field status, and in the 'Previewing Letters' section, we will be interested in the field url.

Tracking Multiple Letters

In addition to an endpoint for retrieving exactly one letter to view, PostGrid also offers the ability to list letters to analyze multiple at once. When combined with PostGrid's search functionality, you will have a very powerful tool at your hands to find letters with specific contacts, status, or even arbitrary information stored in the metainfo field of the letter.

Retrieving multiple letters will require a GET request to be sent to /letters, where we will default our query parameters to skip=0 and limit=10. By incrementing the value of skip by multiples of limit, you can paginate your results to iterate over all possible responses. The query parameter of interest for us will be search, which can either be a simple string or an object containing exact matches for certain fields.

/**
 * @param {string} search
 */
async function searchLetters(search) {
    const requestOptions = {
        method: 'GET',
        headers: {
            'x-api-key': API_KEY,
        },
    };

    const queryParams = `?skip=0&limit=10&search=${search}`;

    const resp = await fetch(
        POSTGRID_URL + '/letters' + queryParams,
        requestOptions
    );

    return await resp.json();
}

Using the above, let's take a search with the string '0' to see what the response object will look like.

{
    "object": "list",
    "limit": 10,
    "skip": 0,
    "totalCount": 0,
    "data": []
}

In this case, we found no letters, but we can see the general response to a list request. In the totalCount field, the number there will be the total amount of results in your search regardless of what limit is set to, which is actually the maximum number of results you will be sent in the response. This way, you can determine what maximum value of skip to use in order to view all results. Contained in the data field will actually be a list of all the letters matching your search in the range determined by skip and limit.

As an example of how to get a hold of this, let's suppose we want to send letters on behalf of customers who have a specific ID. To allow creation of letters, here is a basic function which will associate a customer ID to the metadata of the letter.

/**
 * @param {string} toContactID
 * @param {string} fromContactID
 * @param {string} templateID
 * @param {string} customerID
 */
async function createLetterWithCustomerID(
    toContactID,
    fromContactID,
    templateID,
    customerID
) {
    const requestOptions = {
        method: 'POST',
        headers: {
            'x-api-key': API_KEY,
            'Content-Type': 'application/json',
        },
        body: JSON.stringify({
            to: toContactID,
            from: fromContactID,
            template: templateID,
            metadata: {
                customerID: customerID,
            },
        }),
    };

    const resp = await fetch(POSTGRID_URL + '/letters', requestOptions);

    return await resp.json();
}

Now if we would like to return some letters sent by this customer, it is as simple as using the following function.

/**
 * @param {string} customerID
 */
async function findLettersByCustomerID(customerID) {
    const searchString = encodeURIComponent(
        JSON.stringify({
            metadata: {
                customerID: customerID,
            },
        })
    );

    return (await searchLetters(searchString)).data;
}

Letter Progress

In the above return values to our API calls, notice that we have a field status. This value can be one of:

  • ready
    • Upon successful creation, our letter is ready and will be sent off to printers in the next business day.
  • printing
    • The next stage is printing. If you look where the 'Progress' button is, we now find that the 'Cancel' button has been disabled. At this stage, PostGrid cannot guarantee that printers have not already sent out your mail for delivery and the letter cannot be cancelled. Fortunately, PostGrid has recognized that we are in test mode and has left us a dialogue below the stage number telling us that no actual mail was sent.
  • processed_for_delivery
    • Next, printers will take your mail and have it processed. When the local postal service has entered your mail into their systems, PostGrid will be able to relay you that your mai has been processed and is now awaiting delivery.
  • completed
    • Finally, your letter will have reached the status of completed. This is based on regional estimates of time to completion and not an actual verification of delivery. If you would like to get a tracking number for this mail to get an exact notification of delivery, you can send it as certified mail. For more information on how to send certified mail, see our API guide.
  • cancelled
    • Before letters are sent out to printers, PostGrid offers the ability to cancel your mail. Letters sent from PostGrid will not be sent to printers until after midnight in Toronto. During this period, simply press the "Cancel" button and confirm your choice in order to prevent the letter from being sent.

When working with test letters, you can actually force a letter to go through these steps. To perform this, simply make a POST request to /letters/:id/progressions.

/**
 * @param {string} letterID
 */
async function progressTestLetter(letterID) {
    const requestOptions = {
        method: 'POST',
        headers: {
            'x-api-key': API_KEY,
        },
    };

    const resp = await fetch(
        POSTGRID_URL + `/letters/${letterID}/progressions`,
        requestOptions
    );

    return await resp.json();
}

Using the function above, progressing a letter will update the status and will return back the letter object with the updated status. Progressing a completed letter will leave you with an error message, as will progressing a non-existent letter, a live letter, or a cancelled letter.

Previewing Letters

When viewing the API response to any GET request to the /letters endpoint, you will note that each letter object will contain a field url populated with a web address. This URL will allow you to download PDFs of your letters in order to make sure that template variables have been filled out correctly, that the address region doesn't block the content of your letters, among other reasonable checks. These links can be opened in your web browser to begin a download.

Cancelling Letters

Before a letter reaches the status of printing, PostGrid offers you the ability to cancel your mail. Be aware that mail that has reached the status of printing has been sent to our printers and could already be out for delivery, so before this is the last time we can guarantee that your letters will not be sent. To cancel letters with the API, all you need to do is make a DELETE request to /letters/:id. Below is a simple function which consumes a letter ID and cancels the order.

/**
 * @param {string} letterID
 */
async function cancelLetter(letterID) {
    const requestOptions = {
        method: 'DELETE',
        headers: {
            'x-api-key': API_KEY,
        },
    };

    const resp = await fetch(
        POSTGRID_URL + `/letters/${letterID}`,
        requestOptions
    );

    return await resp.json();
}

Upon a successful cancellation, you should receive the same response as if you had made a GET request to /letters/:id. However, when a letter has progressed too far or has already been deleted, this will be the error message you receive.

{
    "error": {
        "type": "cancel_failed_error",
        "message": "Cannot cancel order ID letter_iPA8xkhL7f3wBPGxi8aegE."
    }
}

Resending Letters Safely

In rare cases, letters may not be returned when listing or you did not receive a response from your request to create a letter. In this case, you may want to resend your letters without worrying whether or not your previous requests were fulfilled. For such cases, PostGrid's API gives you the ability to make an idempotent request to create letters by supplying a unique key in the header Idempotency-Key. Although you can designate this key however you would like, it is recommended that you using V4 UUIDs, more of which can be learned about here.

/**
 * @param {string} toContactID
 * @param {string} fromContactID
 * @param {string} templateID
 * @param {string} idempotencyKey
 */
async function createLetterIdempotent(
    toContactID,
    fromContactID,
    templateID,
    idempotencyKey
) {
    const requestOptions = {
        method: 'POST',
        headers: {
            'x-api-key': API_KEY,
            'Content-Type': 'application/json',
            'Idempotency-Key': idempotencyKey,
        },
        body: JSON.stringify({
            to: toContactID,
            from: fromContactID,
            template: templateID,
        }),
    };

    const resp = await fetch(POSTGRID_URL + '/letters', requestOptions);

    return await resp.json();
}

With the above function, so long as the idempotencyKey remains the same, calling this function repeatedly will not create additional mail after it successfully reaches our servers. This will remain true even if you try to change the contacts or the template, and even would remain true if we tried to send a request with any other different option.

API Reference: https://postgrid.readme.io/reference/letters