Problem
You have a small Shopify store and want to offer your customers a special shipping rate when they buy multiple quantities of a single product. There are Shopify apps in the app store that provide this functionality but you don’t want to pay for an app for such a simple requirement. You’re not a developer and don’t know how to host your own web service but you are somewhat technical and can follow instructions.
Solution
Thankfully, Google has your back. Using Google Apps Script, you can stitch together a solution to meet some simple shipping rate needs. Keep reading to find out how.
Prerequisites
- You have a Shopify store with admin access - you’ll need to create a private app
- You have a Google account
Steps in this tutorial
There are a few steps we’ll need to take. Don’t be intimidated by this list - I’ll show you exactly what you need to do.
- Create a private app in your Shopify store
- Create a Google Apps Script that will handle our logic and respond when Shopify makes a request during checkout
- Let Shopify know we want to provide shipping rates by creating a Carrier Service using the Shopify API
- Test our logic to ensure shipping rates are returned and are accurate
Caveats
First of all, I’d recommend having at least one of the included shipping rate providers enabled (USPS, UPS, etc.). What you don’t want to have happen is have an issue with the custom shipping rates we are building in this post and not respond with shipping rates to Shopify. In order for a checkout to continue, shipping rates must be provided for items that require shipping - if they are not, your customer will not be able to complete the checkout and you’ll lose a sale!
Keep in mind that this is somewhat of a naive implementation - the goal is for you to understand how to use Google Apps Script to respond to a POST request. Depending on your requirements - the ways you can implement this are virtually endless.
Lastly, it would be worth checking the documentation on how to verify that the request is actually being sent from Shopify. Please see the Shopify documentation on webhook verification to learn more about this. Again, this post is simply to show you what is possible.
Now that we have a better understanding of the pitfalls, let’s get started!
Step 1: Create a private app in your Shopify store
- Log in to your store and choose Apps in the Admin menu
- On the Apps page, choose the Manage private apps link near the bottom of the page (this is just next to “Working with a developer on your shop?”)
- On the Private apps page, click the Create a new private app button in the upper-right portion of the screen
- Provide a Private app name - I’m using “My Special Shipping Rates”
- Provide an Emergency developer email - this is the email address that will be notified if Shopify detects a problem
- In the Admin API section, click the link at the bottom of the section - Review disabled Admin API permissions - this will display more permissions
- Scroll down and you should see one that reads “Shipping rates, countries and provinces” - here, be sure and select Read and write - this tells Shopify that we can create can use the credentials associated with this private app to create a Carrier Service (this is the name Shopify uses to refer to apps that can provide shipping rates).
- Accept the default settings for all other options and choose Save at the top of the page to save your changes
- After saving the changes, you’ll have access to an API key and Password - we’ll need these later
Congratulations! You now have access to communicate with your store programmatically.
Step 2: Create a Google Apps Script
- Once you have a Google account, go to Google Drive and click the New+ button.
- On the New menu, you’ll probably see Google Docs and Google Sheets but, to get to Google Apps Script, you’ll need to mouse over More, then choose Google Apps Script.
- After clicking Google Apps Script, a new script will be opened with a myFunction placeholder function created for you.
function myFunction() {
}
- Referring to the documentation, you’ll see that an apps script deployed as a web app can respond to an HTTP GET or POST request. Shopify sends a POST request with the details of the checkout so we’ll need to implement a
doPost()
method to respond to the POST request. Change the function name frommyFunction()
todoPost(e)
(Thee
in the parenthesis is important because the details of the request from Shopify will be nested in thee
parameter). It should look like this:
function doPost(e) {
}
- Next, let’s take a look at what the request from Shopify will look like - a request from Shopify for shipping rates will look like this:
{
"rate": {
"origin": {
"country": "CA",
"postal_code": "K2P1L4",
"province": "ON",
"city": "Ottawa",
"name": null,
"address1": "150 Elgin St.",
"address2": "",
"address3": null,
"phone": "16135551212",
"fax": null,
"email": null,
"address_type": null,
"company_name": "Jamie D's Emporium"
},
"destination": {
"country": "CA",
"postal_code": "K1M1M4",
"province": "ON",
"city": "Ottawa",
"name": "Bob Norman",
"address1": "24 Sussex Dr.",
"address2": "",
"address3": null,
"phone": null,
"fax": null,
"email": null,
"address_type": null,
"company_name": null
},
"items": [{
"name": "Short Sleeve T-Shirt",
"sku": "",
"quantity": 1,
"grams": 1000,
"price": 1999,
"vendor": "Jamie D's Emporium",
"requires_shipping": true,
"taxable": true,
"fulfillment_service": "manual",
"properties": null,
"product_id": 48447225880,
"variant_id": 258644705304
}],
"currency": "USD",
"locale": "en"
}
}
There’s a lot here but let’s break it down - the entire request is wrapped in a rate
object so let’s ignore the rate at the top. Nested in the rate is five data properties: origin, destination, items, currency, and locale. Based on these property keys, we can see that the origin
is where the order is shipping from (most likely your store’s address), the destination
is the customer’s address, items
is an array of the cart line items, currency
is the transaction currency, and, finally, the locale
.
For providing simple shipping rates such as a “first + additional” rate where we’ll charge a price for the first item and, for each additional quantity, we’ll charge a less expensive price to reward the customer for buying multiple quantities. We’ll need to loop over each item in the items
array and calculate based on each item’s quantity.
- Next, let’s take a look at what Shopify is expecting back - the response will need to be in the following form:
{
"rates": [
{
"service_name": "canadapost-overnight",
"service_code": "ON",
"total_price": "1295",
"description": "This is the fastest option by far",
"currency": "CAD",
"min_delivery_date": "2013-04-12 14:48:45 -0400",
"max_delivery_date": "2013-04-12 14:48:45 -0400"
}
]
}
Similar to the request Shopify sends, the response needs to be wrapped in a rates
array. Because this is an array, we could send back multiple shipping rates but for our use case, we’ll only be sending back one.
Note: min_delivery_date and max_delivery_date are optional so I won’t include them in our response 7. Now that we know what to expect from Shopify and what Shopify is expecting back from us, we can proceed to implement our Google Apps Script logic. Using Postman, I sent a POST request emulating the request we’ll get from Shopify and here’s what it looks like:
{
"parameter": {},
"contextPath": "",
"contentLength": 1150,
"queryString": "",
"parameters": {},
"postData": {
"type": "application/json",
"length": 1150,
"contents": "{\n \"rate\": {\n \"origin\": {\n \"country\": \"CA\",\n \"postal_code\": \"K2P1L4\",\n \"province\": \"ON\",\n \"city\": \"Ottawa\",\n \"name\": null,\n \"address1\": \"150 Elgin St.\",\n \"address2\": \"\",\n \"address3\": null,\n \"phone\": \"16135551212\",\n \"fax\": null,\n \"email\": null,\n \"address_type\": null,\n \"company_name\": \"Jamie D's Emporium\"\n },\n \"destination\": {\n \"country\": \"CA\",\n \"postal_code\": \"K1M1M4\",\n \"province\": \"ON\",\n \"city\": \"Ottawa\",\n \"name\": \"Bob Norman\",\n \"address1\": \"24 Sussex Dr.\",\n \"address2\": \"\",\n \"address3\": null,\n \"phone\": null,\n \"fax\": null,\n \"email\": null,\n \"address_type\": null,\n \"company_name\": null\n },\n \"items\": [{\n \"name\": \"Short Sleeve T-Shirt\",\n \"sku\": \"\",\n \"quantity\": 1,\n \"grams\": 1000,\n \"price\": 1999,\n \"vendor\": \"Jamie D's Emporium\",\n \"requires_shipping\": true,\n \"taxable\": true,\n \"fulfillment_service\": \"manual\",\n \"properties\": null,\n \"product_id\": 48447225880,\n \"variant_id\": 258644705304\n }],\n \"currency\": \"USD\",\n \"locale\": \"en\"\n }\n}",
"name": "postData"
}
}
Pretty ugly, huh? We’ll it’s not as bad as it seems - you can see that the data we care about is nested in the postData
object with the contents
key. There’s a function we’ll use in Google Apps Script to get these contents back into a form we can more-easily work with.
Here’s the final Google Apps Script implementation - I’ll discuss below:
function doPost(e) {
var contents = JSON.parse(e.postData.contents);
var items = contents.rate.items;
var firstPrice = 500;
var additionalPrice = 250;
var price = 0;
for (i=0;i<items.length;i++)
{
price += firstPrice;
for (q=2;q<=items[i].quantity;q++)
{
price += additionalPrice;
}
}
var response = {
rates: [
{
service_name: "My Special Shipping Rates",
service_code: "MSSR",
total_price: price,
description: "Special rates for multiple item purchase",
currency: "USD"
}
]
};
return ContentService.createTextOutput(JSON.stringify(response)).setMimeType(ContentService.MimeType.JSON);
}
Note: We are assuming here that all items in your store require shipping. If that is not the case, you’ll want to add a condition to check the item’s requires_shipping
property.
var contents = JSON.parse(e.postData.contents);
Get the content sent by Shopify - the JSON.parse()
function takes the ugly string we see above and converts it to a JavaScript object we can work with in our code.
var items = contents.rate.items;
We specifically want to work with the items
data and, by assigning it to a variable, we can simply type items
rather than contents.rate.items
.
var firstPrice = 500;
var additionalPrice = 250;
var price = 0;
Here we’re setting our price for the first and additional item and initializing a variable to hold our price. Note: Shopify expects the value we send back to be in the lowest denomination, in our case, it is USD so we are sending the value back in cents (500 cents = 5 USD)
for (i=0;i<items.length;i++)
We are going to loop over the items array and work with each item. This for
loop is basically saying “Start at zero and, keep looping over items while the current loop is less than the total number of items in the items array.
Note: JavaScript arrays are zero-indexed so the first item is at the zero position
price += firstPrice;
Take the price value and add the firstPrice value to it. +=
is shorthand for price = price + firstPrice
.
for (q=2;q<=items[i].quantity;q++)
{
price += additionalPrice;
}
Same loop logic as before but here we’re starting with 2 because we’re only adding the additionalPrice
when the quantity is 2 or greater.
var response = {
rates: [
{
service_name: "My Special Shipping Rates",
service_code: "MSSR",
total_price: price,
description: "Special rates for multiple item quantity purchase",
currency: "USD"
}
]
};
Here we are creating a response variable that is in the format Shopify is expecting. Notice that we are assigning the value of our price
variable to the total_price
- all other values are values we typed in. Shopify is expecting the service_code
to be unique so use something unique that doesn’t overlap with other rates - I’ve simply used the initials of the value I assigned to the service_name
.
return ContentService.createTextOutput(JSON.stringify(response)).setMimeType(ContentService.MimeType.JSON);
Finally, we send back our response. Shopify is expecting a JSON response so we’re using the built in ContentService
service to create a text output - we’re using JSON.stringify()
to convert our JavaScript object into JSON and we’re setting the MIME type to JSON.
8. We can now deploy our script. First, let’s save the script by clicking the Save button on the toolbar or by choosing File -> Save.
9. Now that we have the script saved, let’s publish it by selecting the Publish menu option and choose Deploy as web app….
10. Accept the proposed current web app URL.
11. Project version should be New.
12. Execute the app as: should be you (Me in the selection).
13. Who has access to the app: should be Anyone, even anonymous.
14. Choose Deploy to publish it.
Step 3: Let Shopify know about our Google Apps Script
At this point, although we’ve created our Google Apps Script to provide shipping rates to Shopify, Shopify is not aware of it. We need to let Shopify know about this using the Shopify Carrier Service API. Unfortunately, there is no way to do this from the Shopify Admin - it must be done through he API.
We’ll use a free tool to communicate with Shopify’s API - there are a lot of ways to do this but I’ll use Postman here. Postman is a HTTP client and, although they have versions that support teams, you can download a free, individual copy. Head over to the Postman download page and download the app to your computer. Note: Postman can be used to test your Google Apps Script, too
- Open Postman
- The most prominent button on the page is the Send button - it’s located in the upper right portion of the screen. To the left of the URL field is where we indicate the type of request (GET, POST, etc.) - select the dropdown and choose POST.
- In the URL field, enter this:
https://<YOUR_PRIVATE_APP_API_KEY_GOES_HERE>:<YOUR_PRIVATE_APP_PASSWORD_GOES_HERE>@<YOUR_SHOPIFY_STORE_DOMAIN_GOES_HERE>.myshopify.com/admin/carrier_services.json
The api key and password is from Step 1: Create a private app in your Shopify store. The Shopify domain is the portion before “myshopify.com” when you created your store - this is not the custom domain that you may have pointing to your store - this is specifically the one that has the myshopify.com suffix.
Note: Make sure you don’t miss the colon (:) between your api key and password and make sure you include the @ between your password and myshopify domain 4. In the Body tab below the URL field, choose format raw and to the right of the format, select JSON(application/json) from the dropdown list. 5. In the Body section, include this:
{
"carrier_service": {
"name": "Special Shipping Rate",
"callback_url": "<YOUR_GOOGLE_APPS_SCRIPT_URL_GOES_HERE>",
"service_discovery": true
}
}
Include a name
that you’ll be able to recognize - your customers won’t see this, they’ll see the service name and description we send back in the response. For the callback_url
, paste in your Google Apps Script URL enclosed in double quotes. Mine looks like this:
6. Double check that you’ve got everything typed/pasted correctly - after verifying, click the Send button to send it to Shopify.
7. If it was successful, you should see a response like this in the response body section:
{
"carrier_service": {
"id": REDACTED,
"name": "Google Apps Script Provided",
"active": true,
"service_discovery": true,
"carrier_service_type": "api",
"format": "json",
"callback_url": "https://script.google.com/macros/s/REDACTED/exec"
}
}
Note: If your request was not successful, first go back over these instructions and ensure you followed them correctly. If that doesn’t work and you have already been using Postman, consider opening Postman’s Cookies section and delete any existing cookies. Existing cookies, especially related to other Shopify stores, can cause issues so just delete them.
Congratulations, you’ve successfully made Shopify aware of your carrier service (custom shipping rates)!
Step 4: Test our shipping rates
Anytime changes are made like this it is very important to test. In order for us to test, we’ll make a slight change to our App Scripts logic.
Referring back to the data that Shopify sends, you’ll notice that the destination
includes a name
value:
{
...
"destination": {
"country": "CA",
"postal_code": "K1M1M4",
"province": "ON",
"city": "Ottawa",
"name": "Bob Norman",
...
}
}
During testing, we’ll use a special name when we checkout so our logic will only get triggered for our special name. If a real purchase is taking place during our testing, we’ll send back an empty rates
array so it won’t affect checkout for your customers.
Here’s what we’ll change in our logic. Let’s add the following just after the var price = 0;
line:
if (contents.rate.destination.name.indexOf("*test*") === -1) {
return ContentService.createTextOutput(JSON.stringify({rates: []})).setMimeType(ContentService.MimeType.JSON);
}
Our doPost()
function should now look like this:
function doPost(e) {
var contents = JSON.parse(e.postData.contents);
var items = contents.rate.items;
var firstPrice = 500;
var additionalPrice = 250;
var price = 0;
if (contents.rate.destination.name.indexOf("*test*") === -1) {
return ContentService.createTextOutput(JSON.stringify({rates: []})).setMimeType(ContentService.MimeType.JSON);
}
for (i=0;i<items.length;i++)
{
price += firstPrice;
for (q=2;q<=items[i].quantity;q++)
{
price += additionalPrice;
}
}
var response = {
rates: [
{
service_name: "My Special Shipping Rates",
service_code: "MSSR",
total_price: price,
description: "Special rates for multiple item purchase",
currency: "USD"
}
]
};
return ContentService.createTextOutput(JSON.stringify(response)).setMimeType(ContentService.MimeType.JSON);
}
Here, we’re checking if the name includes *test*
and, if it does, we’re just sending back an empty rates
array.
Note: Be sure to save these changes and go back through the publish process. To publish, on the dropdown list for version, be sure and choose New, then choose Update.
Now, we’ll initiate a checkout to see if our Apps Script is working.
- Go to your store and select a single item and place it in the cart.
- Choose to checkout and when prompted for the customer name and address on the Shipping tab, be sure to set the last name to “*test*”
- Choose Continue to shipping method and you should see the rate you setup in Google Apps Script.
- Continue to test with different items and quantities to ensure it is calculating properly.
Now that it has been tested and you are ready for customers to get your custom shipping rate during checkout, go back and remove the conditional check we added to our script - simply remove this and be sure and republish the script:
if (contents.rate.destination.name.indexOf("*test*") === -1) {
return ContentService.createTextOutput(JSON.stringify({rates: []})).setMimeType(ContentService.MimeType.JSON);
}
Conclusion
I hope you’ve enjoyed this post. If you have specific questions, please reach out to me on Twitter @ballenkidd. Thanks!