Sending Postcards 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 Print & Mail 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 postcards with PostGrid:

  • Creating and designing templates
  • Creating contacts
  • Sending postcards
  • Tracking postcards 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 postcard you wish to intend, PostGrid offers both a test mode and a live mode. In test mode, actual postcards 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 postcard, it adheres to postal standards and looks exactly as it should. When you are ready to actually send the postcards, you can switch to live mode and repeat the sending process with everything set up as before. Postcards 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.

API Keys

For developers following along wishing to use the API, you will need to know where to locate and how to use your API keys. The keys can be found in your settings on the dashboard, featuring a live key prefixed by live_sk, and a test key prefixed by test_sk. The live key can be used to carry out actual mail orders while the test key will only send out postcards in test mode. More details can be found here, in the official API documentation. With each request, be sure to include the x-api-key header with your API key as the value.

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 postcards require that you either supply a PDF or templates to form the basis of the postcard. Templates allow you to dynamically create postcards 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 postcard, 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 postcards 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 postcards and for US & international postcards.

Creating Contacts

Before we can get started with sending postcards, we will need to specify who the postcards 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 Postcards

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 Postcards 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 /postcards. Let's now create a function to consume some contact IDs and a template ID and create a postcard.

/**
 * @param {string} toContactID
 * @param {string} fromContactID
 * @param {string} frontTemplateID
 * @param {string} backTemplateID
 * @param {string} postcardSize
 *
 */
async function createPostcard(toContactID, fromContactID, frontTemplateID, backTemplateID, postcardSize) {
    const requestOptions = {
        method: 'POST',
        headers: {
            'x-api-key': API_KEY,
            'Content-Type': 'application/json',
        },
        body: JSON.stringify({
            to: toContactID,
            from: fromContactID,
            frontTemplate: frontTemplateID,
            backTemplate: backTemplateID,
            size: postcardSize
        }),
    };

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

    return await resp.json();
}

This function would create a postcard but after taking a look at the dashboard, we notice that we are missing a postcard description, which can be added with the description field. We also wish to request for express delivery and thus pass true to the express field. Let's extend our function to accommodate for these options.

/**
 * @param {string} toContactID
 * @param {string} fromContactID
 * @param {string} frontTemplateID
 * @param {string} backTemplateID
 * @param {string} postcardSize
 * @param {string} description
 * @param {boolean} isExpress
 */
async function createPostcardWithOptions(toContactID, fromContactID, frontTemplateID, backTemplateID, postcardSize, description, isExpress) {
    const requestOptions = {
        method: 'POST',
        headers: {
            'x-api-key': API_KEY,
            'Content-Type': 'application/json',
        },
        body: JSON.stringify({
            to: toContactID,
            from: fromContactID,
            frontTemplate: frontTemplateID,
            backTemplate: backTemplateID,
            size: postcardSize,
            description: description,
            express: isExpress
        }),
    };

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

    return await resp.json();
}

Sending Without a Template or Contacts

In case you wished to skip the previous two sections and wanted to jump to sending postcards 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 postcards 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} frontHtml
 * @param {string} backHtml
 * @param {string} postcardSize
 */
async function createPostcardFromScratch(to, from, frontHtml, backHtml, postcardSize) {
    const requestOptions = {
        method: 'POST',
        headers: {
            'x-api-key': API_KEY,
            'Content-Type': 'application/json',
        },
        body: JSON.stringify({
            to: to,
            from: from,
            frontHTML: frontHtml,
            backHTML: backHtml,
            size: postcardSize
        }),
    };

    const resp = await fetch(POSTGRID_URL + '/postcards', 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 simple HTML text as 'Hello friend' and 'Bye friend', we receive the following.

{
    "id":"postcard_kkifTGq3haagbBnfy4yPKg",
    "object":"postcard",
    "live":false,
    "size":"6x4",
    "express":false,
    "mailingClass":"first_class",
    "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"
    },
    "frontHTML":"Hello friend!",
    "backHTML":"Bye 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 postcard 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 Postcards

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 /postcards. 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} postcardSize
 * @param {string} filePath
 * @param {string} fileName
 */
async function createPostcardWithPDF(
    toContactID,
    fromContactID,
    postcardSize,
    filePath,
    fileName
) {
    const formData = new FormData();
    formData.append('to', toContactID);
    formData.append('from', fromContactID);
    formData.append('size', postcardSize);
    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 + '/postcards', 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 postcard with a publicly accessible link to our PDF.

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

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

    return await resp.json();
}

Adding Values to Template Variables

Now that we've sent some basic test postcards, let's try sending some postcards 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 {string} postcardSize
 * @param {string} code
 * @param {string} frontTemplateID
 * @param {string} backTemplateID
 */
async function sendExamplePostcard(
    toContactID,
    fromContactID,
    postcardSize,
    code,
    frontTemplateID,
    backTemplateID
) {
    const requestOptions = {
        method: 'POST',
        headers: {
            'x-api-key': API_KEY,
            'Content-Type': 'application/json',
        },
        body: JSON.stringify({
            to: toContactID,
            from: fromContactID,
            size: postcardSize,
            frontTemplate: frontTemplateID,
            backTemplate: backTemplateID,
            description: `Activation code`,
            mergeVariables: {
                code: `$${code}`,
            },
        }),
    };

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

    return await resp.json();
}

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

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

Tracking Postcards

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

Tracking an Individual Postcard

Our first task will be to retrieve a singular postcard. For this section, it will be important that you have kept the ID of your postcard 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 postcard without saving the ID. In order to obtain the information on our individual postcard, we will need to send a GET request to /postcards/:id. Let's design a very basic function to perform exactly this.

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

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

    return await resp.json();
}

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

{
    "id":"postcard_hX2CBfGfZLix34GdCeNVdA",
    "object":"postcard",
    "live":false,
    "description":"Activation code",
    "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 postcards.",
        "email":"[email protected]",
        "firstName":"Nolan",
        "jobTitle":"Backend Customer Success Engineer",
        "phoneNumber":"",
        "postalOrZip":"N2L 3G1",
        "provinceOrState":"ON"
    },
    "mergeVariables":{
        "code":"xyz123",
        "to":{
            "companyName":"PostGrid"
            }
    },
    "size":"6x4",
    "express":true,
    "pageCount":2,
    "sendDate":"2022-05-17T14:07:04.940Z",
    "status":"ready",
    "frontTemplate":"template_8qSx8uRRPvugG8pSeaD6yW",
    "backTemplate":"template_iSoKNNNQUxxUoCmaQ1sACM",
    "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://pg-prod-bucket-1.s3.amazonaws.com/test/postcard_hX2CBfGfZLix34GdCeNVdA.pdf?AWSAccessKeyId=AKIA5GFUILSULWTWCR64&Expires=1652812987&Signature=4yC%2BHB%2FHaE7e82KxlbXa44ZREiw%3D",
    "createdAt":"2022-05-17T14:07:04.944Z",
    "updatedAt":"2022-05-17T14:07:08.945Z"
}

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

Tracking Multiple Postcards

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

Retrieving multiple postcards will require a GET request to be sent to /postcards, 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 searchPostcards(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 + '/postcards' + 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 postcards, 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 cardposts 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 postcards on behalf of customers who have a specific ID. To allow creation of postcards, here is a basic function which will associate a customer ID to the metadata of the postcard.

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

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

    return await resp.json();
}

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

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

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

Postcard 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 postcard 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 postcard 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 to you that your mail has been processed and is now awaiting delivery.
  • completed
    • Finally, your postcard will have reached the status of completed. This is based on regional estimates of time to completion and not an actual verification of delivery.
  • cancelled
    • Before postcards are sent out to printers, PostGrid offers the ability to cancel your mail. Postcards 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 postcard from being sent.

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

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

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

    return await resp.json();
}

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

Previewing Postcards

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

Cancelling Postcards

Before a postcard 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 postcards will not be sent. To cancel postcards with the API, all you need to do is make a DELETE request to /postcards/:id. Below is a simple function which consumes a postcard ID and cancels the order.

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

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

    return await resp.json();
}

Upon a successful cancellation, you should receive the same response as if you had made a GET request to /postcards/:id. However, when a postcard 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 postcard_hX2CBfGfZLix34GdCeNVdA."
    }
}

Resending Postcards Safely

In rare cases, postcards may not be returned when listing or you did not receive a response from your request to create a postcard. In this case, you may want to resend your postcards 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 postcards 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} frontTemplateID
 * @param {string} backTemplateID
 * @param {string} postcardSize
 * @param {string} idempotencyKey
 */
async function createPostcardIdempotent(
    toContactID,
    fromContactID,
    frontTemplateID,
    backTemplateID,
    postcardSize,
    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,
            frontTemplate: frontTemplateID,
            backTemplate: backTemplateID,
            size: postcardSize
        }),
    };

    const resp = await fetch(POSTGRID_URL + '/postcards', 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/postcards