How to use Shipping Rules

In this example we take you through how to use Shipping Rules while within the Operator API

What you’ll learn

In this article you’ll learn about the Shipping Rules Engine, specifically:

  • What Shipping Rules are & who can use them
  • The primary objects that make up Shipping Rules
  • The queries and mutations that can be used to manage Shipping Rules

What are Shipping Rules?

Shipping Rules allow you to determine the rate you charge for a purchased item based on where you are going to send it (Shipping Zone) and an applicable Shipping Profile (e.g. Weight, size, delivery time slot etc.). The table below summarizes the entities that make up a Shipping Rule, along with some other information:


EntityPurposeCreated ByGraphQL Object
Shipping RateThe cost (rate) that can be applied by the rule. You can define multiple rates.Operators (Admins) or SellersShippingRate
Shipping ZoneA zone is made up of a list of Zip or Post codes that defines a geographic area.Operators (Admins) or SellersShippingZone
Shipping ProfileDetermined by the Operator only, a Shipping Profile will typically relate to the size, weight or delivery time slot for the purchased item, but can be “anything”. Shipping Profiles are defined on the Advert VariantsOperators (Admins) onlyShippingProfile
Shipping RuleComprised of: Shipping Rates, Shipping Zones and Shipping ProfilesOperators (Admins) or SellersShippingOption

Object Relationships

To further detail how Shipping Rules (referred to as Shipping Options in the GraphQL API) are constructed, it can be useful to look at the relationships between the primary objects:

Shipping Rule Relationships

While all the relationships are important, the key call out from this diagram is the relationship between: variants <-> shipping profiles <-> shipping rules. In essence:

  • The marketplace operator defines the Shipping Profiles E.g.:
    • Small (<= 5kg)
    • Medium (> 5kg <= 10Kg)
    • Large (> 10Kg)
  • Operators and Sellers can then create Shipping Rules, that encompass:
    • The Shipping Profile
    • The Shipping Rates
    • The Shipping Zones
  • When sellers create products, they can assign a Shipping Profile to the product variants
  • Then when variants are selected for purchase you can determine the relevant shipping rules for that variant (noting that there may be multiple shipping rules for each variant)

Creating Shipping Rules

Step 1: Creating Shipping Profiles

As detailed above, it is the shipping profile that acts as the linchpin between the variant and the relevant shipping rules (or “options”). Determining your shipping profiles is one of the key decisions you will need to make as an operator leveraging shipping rules.

IMPORTANT: Only operators can create shipping profiles, and they can only be created via the Operator Portal.

In this example we are going to create 3 Shipping Profiles:

  • Small (<= 5kg)
  • Medium (> 5kg <= 10Kg)
  • Large (> 10Kg)

To do so:

  • Login to the Operator Portal
  • In the main menu, select: “Shipping Settings”
  • Select “Shipping Profiles” and “Create new shipping profile”
  • Populate the Name for this Shipping Profile along with the Description

Create Shipping Profile


  • Click Save

Repeat this step to add the additional 2 Shipping Profiles.

Step 2: Creating Shipping Zones

Next up we’ll create some shipping zones, that are one or more zip /postal codes. Using this data and cross-referencing with the customer’s delivery address can help you to determine the appropriate shipping rate.

In this example we are going to use the shippingZoneCreateOrUpdate mutation to create the following shipping zones:

  • Zone 1: Metro Gotham City
  • Zone 2: Rural Gotham
  • Zone 3: Metro Metropolis
  • Zone 4: Rural Metropolis

Zip / Post Codes

For each zone we need to provide a list of zip / post codes that we will provide as a Base64 encoded csv.

An example csv file for Zone 1 - Metro Gotham City in its non-encoded format is shown below:

Postcode
10001
10002
10003
10004

Base64 encoding this file will result in the following string: UG9zdGNvZGUKMTAwMDEKMTAwMDIKMTAwMDMKMTAwMDQ=


With this data we can now call shippingZoneCreateOrUpdate

mutation createShippingZone{
  shippingZoneCreateOrUpdate(
    input: {
      attributes: {
        name: "Zone 1 - Metro Gotham City"
        description: "Areas of Gotham served by the metro rail network"
        spreadsheet: {
          dataBase64: "UG9zdGNvZGUKMTAwMDEKMTAwMDIKMTAwMDMKMTAwMDQ="
          filename: "metro_gotham.csv"
        }
      }
    }
  ) {
    shippingZone {
      id
      name
      postcodes
    }
    errors {
      field
      messages
    }
  }
}

For brevity, the above example is not employing variables to pass data to the mutation which is the recommended approach. The API collections follow this method.


The resulting return payload for this mutation call is shown below:

{
  "data": {
    "shippingZoneCreateOrUpdate": {
      "shippingZone": {
        "id": "U2hpcHBpbmdab25lLTg1",
        "name": "Zone 1 - Metro Gotham City",
        "postcodes": ["10001", "10002", "10003", "10004"]
      },
      "errors": null
    }
  }
}

We’ll repeat this mutation call for the remaining 3 zones.

Using the shippingZones query, you can check to see that we have created all zones correctly:

query getShippingZOnes {
  shippingZones(first: 10, after: null) {
    nodes {
      id
      name
      description
      postcodes
      seller {
        id
        businessName
      }
    }
  }
}

This should return something similar to the following:

{
  "data": {
    "shippingZones": {
      "nodes": [
        {
          "id": "U2hpcHBpbmdab25lLTU=",
          "name": "Zone 1 - Metro Gotham",
          "description": "Areas of Gotham served by the metro rail network",
          "postcodes": ["10001", "10002", "10003", "10004"],
          "seller": null
        },
        {
          "id": "U2hpcHBpbmdab25lLTQ=",
          "name": "Zone 2 - Rural Gotham",
          "description": "Areas of Gotham not served by the metro rail network",
          "postcodes": ["10005", "10006", "10007", "10008"],
          "seller": null
        },
        {
          "id": "U2hpcHBpbmdab25lLTM=",
          "name": "Zone 4 - Rural Metropolis",
          "description": "Areas of Metropolis not served by the monorail",
          "postcodes": ["20005", "20006", "20007", "20008"],
          "seller": null
        },
        {
          "id": "U2hpcHBpbmdab25lLTI=",
          "name": "Zone 3 - Metro Metropolis",
          "description": "Areas of Metropolis served by the monorail",
          "postcodes": ["20001", "20002", "20003", "20004"],
          "seller": null
        }
      ]
    }
  }
}

You’ll note that as we’ve been creating the zones as an operator, the seller detail that we have asked for in our query returns a null response.

Step 3: Creating Shipping Rates

The next step is to determine the Shipping Rates we want to have, in this example we are going to have 6:

  • Small Metro - $5
  • Medium Metro - $10
  • Large Metro - $20
  • Small Rural - $10
  • Medium Rural - $15
  • Large Rural - $25

Calling the shippingRateCreateOrUpdate mutation in the following way, we can create our first rate:

mutation createShippingRates {
  shippingRateCreateOrUpdate(
    input: { attributes: { name: "Small Metro", rate: "5" } }
  ) {
    errors {
      field
      messages
    }
    shippingRate {
      id
      name
      rateCents
    }
  }
}

This will return the following payload:

{
  "data": {
    "shippingRateCreateOrUpdate": {
      "errors": null,
      "shippingRate": {
        "id": "U2hpcHBpbmdSYXRlLTM=",
        "name": "Small Metro",
        "rateCents": 500
      }
    }
  }
}

We’ll repeat this mutation call for the remaining 5 rates.

Using the shippingRates query, you can check to see that we have created all rates correctly:

query getShippingRates {
  shippingRates(first: 10, after: null) {
    nodes {
      name
      id
      rateCents
      seller {
        businessName
        id
      }
    }
  }
}

This should return something similar to the following:

{
  "data": {
    "shippingRates": {
      "nodes": [
        {
          "name": "Large Metro",
          "id": "U2hpcHBpbmdSYXRlLTE0",
          "rateCents": 2000,
          "seller": null
        },
        {
          "name": "Medium Metro",
          "id": "U2hpcHBpbmdSYXRlLTEz",
          "rateCents": 1000,
          "seller": null
        },
        {
          "name": "Small Metro",
          "id": "U2hpcHBpbmdSYXRlLTEy",
          "rateCents": 500,
          "seller": null
        },
        {
          "name": "Large Rural",
          "id": "U2hpcHBpbmdSYXRlLTEx",
          "rateCents": 2500,
          "seller": null
        },
        {
          "name": "Medium Rural",
          "id": "U2hpcHBpbmdSYXRlLTEw",
          "rateCents": 1500,
          "seller": null
        },
        {
          "name": "Small Rural",
          "id": "U2hpcHBpbmdSYXRlLTk=",
          "rateCents": 1000,
          "seller": null
        }
      ]
    }
  }
}

Step 4: Define your Shipping Rules

This is the step where we bring it altogether and create some Shipping Rules using:

  • Shipping Profiles
  • Shipping Zones
  • Shipping Rates

We are going to create the following rules

RuleProfileZonesRates
Small MetroSmall (<= 5Kg)- Zone 1 - Metro Gotham
- Zone 3 - Metro Metropolis
Small Metro
Small RuralSmall (<= 5Kg)- Zone 2 - Rural Gotham
- Zone 4 - Rural Metropolis
Small Rural
Medium MetroMedium (> 5kg <= 10Kg)- Zone 1 - Metro Gotham
- Zone 3 - Metro Metropolis
Medium Metro
Medium RuralMedium (> 5kg <= 10Kg)- Zone 2 - Rural Gotham
- Zone 4 - Rural Metropolis
Medium Rural
Large MetroLarge (> 10Kg)- Zone 1 - Metro Gotham
- Zone 3 - Metro Metropolis
Large Metro
Large RuralLarge (> 10Kg)- Zone 2 - Rural Gotham
- Zone 4 - Rural Metropolis
Large Rural

To do so we’ll use the Operator Portal, by navigating to:

  • Configuration
    • Shipping Settings
      • Shipping Rules
        • Create New Shipping Rule

Create Shipping Rule


Repeat for the remaining 5 shipping options / rules.

Using the shippingOptions query, you can check to see that we have created all rates correctly:

query {
  shippingOptions(first: 1, after: null) {
    totalCount
    pageInfo {
      endCursor
      hasNextPage
    }
    nodes {
      id
      name
      type
      shippingRates(first: 10) {
        nodes {
          id
          name
          rateCents
        }
      }
      shippingProfiles(first: 10) {
        nodes {
          id
          name
          description
        }
      }
      shippingZones(first: 10) {
        nodes {
          id
          name
          postcodes
        }
      }
      id
      seller {
        id
        businessName
      }
    }
  }
}

This should return something similar to the following (note we’ve only show the first page of shipping option data given the size of the payload):

{
	"data": {
		"shippingOptions": {
			"totalCount": 6,
			"pageInfo": {
				"endCursor": "MQ",
				"hasNextPage": true
			},
			"nodes": [
				{
					"id": "U2hpcHBpbmdPcHRpb24tMTI=",
					"name": "Large Rural",
					"type": "marketplace",
					"shippingRates": {
						"nodes": [
							{
								"id": "U2hpcHBpbmdSYXRlLTEx",
								"name": "Large Rural",
								"rateCents": 2500
							}
						]
					},
					"shippingProfiles": {
						"nodes": [
							{
								"id": "U2hpcHBpbmdQcm9maWxlLTc=",
								"name": "Large (> 10Kg)",
								"description": ""
							}
						]
					},
					"shippingZones": {
						"nodes": [
							{
								"id": "U2hpcHBpbmdab25lLTM=",
								"name": "Zone 4 - Rural Metropolis",
								"postcodes": [
									"20005",
									"20006",
									"20007",
									"20008"
								]
							},
							{
								"id": "U2hpcHBpbmdab25lLTQ=",
								"name": "Zone 2 - Rural Gotham",
								"postcodes": [
									"10005",
									"10006",
									"10007",
									"10008"
								]
							}
						]
					},
					"seller": null
				}
			]
		}
	}
}

Step 5: Applying Shipping Profiles to Products

In terms of now applying shipping profiles to products, (and by extension shipping profiles / rules) , that would typically be a Seller responsibility, however for completeness we’ll show how you can update an existing variant to have a shipping profile using the variantUpdate mutation.

Operator Vs Seller Defined Rules

So far in this article we have been working as an operator to define shipping rules, however Sellers can also create shipping rules, using their own rates and zones (shipping profiles can only be created by the operator). Therefore Sellers can use shipping rules:

  • Created by the operator
  • Created by themselves (the seller)

In the example that follows we are going to update a sellers variant with a profile that links to shipping rules defined by us (the operator), to that end we need to ensure that for this seller, we have selected that we want to use operator defined shipping rules. This can be set in the Seller Portal for the given seller under the Shipping menu, then toggling the “Use marketplace name defined shipping rules”:


Use Operator Shipping Rules


In this example we are applying profile Small (<= 5Kg) (id: U2hpcHBpbmdQcm9maWxlLTU=) to an existing variant with id: VmFyaWFudC0yNg==:

mutation addShippingProfileToVariant {
  variantUpdate(
    input: {
      variantId: "VmFyaWFudC0yNg=="
      attributes: { shippingProfileId: "U2hpcHBpbmdQcm9maWxlLTU=" }
    }
  ) {
    variant {
      id
      shippingProfile {
        id
        name
      }
      shippingOptions(first: 10) {
        totalCount
        nodes {
          name
        }
      }
    }
  }
}

This should return the following,

{
	"data": {
		"variantUpdate": {
			"variant": {
				"id": "VmFyaWFudC0yNg==",
				"shippingProfile": {
					"id": "U2hpcHBpbmdQcm9maWxlLTU=",
					"name": "Small (<= 5Kg)"
				},
				"shippingOptions": {
					"totalCount": 2,
					"nodes": [
						{
							"name": "Small Metro"
						},
						{
							"name": "Small Rural"
						}
					]
				}
			}
		}
	}
}

NOTE: If we had not toggled on the use of the operator defined shipping rules in the previous step, while the shipping profile would be returned in the above mutation response, we would have a null set for the shipping options.

Consuming Product Shipping Rules

Having defined shipping rules and associating shipping profiles to variants, as an Operator you will now want to consume those rules so that you can determine shipping pricing at checkout. There are 2 possible approaches, with the 2nd one being preferred.

Option 1: Retrieving Shipping Rates & Zones with the Variant

The following advertsWhere query is returning all advert variants, along with the applicable:

  • Shipping Profile
  • Shipping Options (Rules)
  • Shipping Zones
  • SHipping Rates
query GetAllShippingOptionData {
  advertsWhere(first: 10, after: null, variantSkus: ["test_sku_1"]) {
    totalCount
    pageInfo {
      hasNextPage
      endCursor
    }
    nodes {
      id
      title
      legacyId
      variants {
        nodes {
          id
          countOnHand
          shippingProfile {
            id
            name
          }
          shippingOptions(first: 10) {
            totalCount
            nodes {
              id
              name
              shippingZones {
                totalCount
                edges {
                  node {
                    name
                    postcodes
                  }
                }
              }
              shippingRates {
                totalCount
                edges {
                  node {
                    name
                    rateCents
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}

For 1 Advert, with 1 Variant this would return the following data:

{
  "data": {
    "advertsWhere": {
      "totalCount": 1,
      "pageInfo": {"hasNextPage": false, "endCursor": "MQ"},
      "nodes": [
        {
          "id": "QWR2ZXJ0LTEwMDAwMDAyNA==",
          "title": "Handcrafted Plastic Shoes",
          "legacyId": 100000024,
          "variants": {
            "nodes": [
              {
                "id": "VmFyaWFudC0yNg==",
                "countOnHand": 66,
                "shippingProfile": {
                  "id"  : "U2hpcHBpbmdQcm9maWxlLTU=",
                  "name": "Small (<= 5Kg)"
                },
                "shippingOptions": {
                  "totalCount": 2,
                  "nodes": [
                    {
                      "id": "U2hpcHBpbmdPcHRpb24tNw==",
                      "name": "Small Metro",
                      "shippingZones": {
                        "totalCount": 2,
                        "edges": [
                          {
                            "node": {
                              "name": "Zone 3 - Metro Metropolis",
                              "postcodes": ["20001", "20002", "20003", "20004"]
                            }
                          },
                          {
                            "node": {
                              "name": "Zone 1 - Metro Gotham",
                              "postcodes": ["10001", "10002", "10003", "10004"]
                            }
                          }
                        ]
                      },
                      "shippingRates": {
                        "totalCount": 1,
                        "edges": [
                          {
                            "node": {"name": "Small Metro", "rateCents": 500}
                          }
                        ]
                      }
                    },
                    {
                      "id": "U2hpcHBpbmdPcHRpb24tOA==",
                      "name": "Small Rural",
                      "shippingZones": {
                        "totalCount": 2,
                        "edges": [
                          {
                            "node": {
                              "name": "Zone 4 - Rural Metropolis",
                              "postcodes": ["20005", "20006", "20007", "20008"]
                            }
                          },
                          {
                            "node": {
                              "name": "Zone 2 - Rural Gotham",
                              "postcodes": ["10005", "10006", "10007", "10008"]
                            }
                          }
                        ]
                      },
                      "shippingRates": {
                        "totalCount": 1,
                        "edges": [
                          {
                            "node": {"name": "Small Rural", "rateCents": 1000}
                          }
                        ]
                      }
                    }
                  ]
                }
              }
            ]
          }
        }
      ]
    }
  }
}

Imagine if:

  • We were returning 100’s of products
  • Each product had 20 variants
  • We had many more shipping zones

The data payload would get quickly out of hand. Given that once set up Shipping Rule data should be relatively “slow moving” (i.e. it wouldn’t be changing every few minutes), querying this type of data in this way is very inefficient.

Option 2: Retrieving Shipping Rule IDs only

To reduce the amount of data that you pull back for every Advert & Variant (as shown above) a 2nd approach is suggested in which you:

  • Only return the shippingOption IDs with your Adverts & Variants
  • Periodically query (e.g. daily) & cache all the shippingOptions including all shippingZones and shippingRules
  • “Join” the cached shipping options on the shipping option Ids for each Variant

Examples of both these queries are provided below.

advertsWhere Query - When consuming products

query GetShippingOptionIdsOnly {
  advertsWhere(first: 10, after: null, variantSkus: ["test_sku_1"]) {
    totalCount
    pageInfo {
      hasNextPage
      endCursor
    }
    nodes {
      id
      title
      legacyId
      variants {
        nodes {
          id
          countOnHand
          shippingOptions(first: 10) {
            totalCount
            nodes {
              id
              name
            }
          }
        }
      }
    }
  }
}

Shipping Options Query - Run at an hourly cadence

query {
  shippingOptions(first: 10, after: null) {
    totalCount
    pageInfo {
      endCursor
      hasNextPage
    }
    nodes {
      id
      name
      type
      shippingRates(first: 10) {
        nodes {
          id
          name
          rateCents
        }
      }
      shippingZones(first: 10) {
        nodes {
          id
          name
          postcodes
        }
      }
      id
      seller {
        id
        businessName
      }
    }
  }
}

For the rules we’ve defined earlier this would bring back:

{
  "data": {
    "shippingOptions": {
      "totalCount": 6,
      "pageInfo": {
        "endCursor": "Ng",
        "hasNextPage": false
      },
      "nodes": [
        {
          "id": "U2hpcHBpbmdPcHRpb24tMTI=",
          "name": "Large Rural",
          "type": "marketplace",
          "shippingRates": {
            "nodes": [
              {
                "id": "U2hpcHBpbmdSYXRlLTEx",
                "name": "Large Rural",
                "rateCents": 2500
              }
            ]
          },
          "shippingZones": {
            "nodes": [
              {
                "id": "U2hpcHBpbmdab25lLTM=",
                "name": "Zone 4 - Rural Metropolis",
                "postcodes": [
                  "20005",
                  "20006",
                  "20007",
                  "20008"
                ]
              },
              {
                "id": "U2hpcHBpbmdab25lLTQ=",
                "name": "Zone 2 - Rural Gotham",
                "postcodes": [
                  "10005",
                  "10006",
                  "10007",
                  "10008"
                ]
              }
            ]
          },
          "seller": null
        },
        {
          "id": "U2hpcHBpbmdPcHRpb24tMTE=",
          "name": "Large Metro",
          "type": "marketplace",
          "shippingRates": {
            "nodes": [
              {
                "id": "U2hpcHBpbmdSYXRlLTE0",
                "name": "Large Metro",
                "rateCents": 2000
              }
            ]
          },
          "shippingZones": {
            "nodes": [
              {
                "id": "U2hpcHBpbmdab25lLTI=",
                "name": "Zone 3 - Metro Metropolis",
                "postcodes": [
                  "20001",
                  "20002",
                  "20003",
                  "20004"
                ]
              },
              {
                "id": "U2hpcHBpbmdab25lLTU=",
                "name": "Zone 1 - Metro Gotham",
                "postcodes": [
                  "10001",
                  "10002",
                  "10003",
                  "10004"
                ]
              }
            ]
          },
          "seller": null
        },
        {
          "id": "U2hpcHBpbmdPcHRpb24tMTA=",
          "name": "Medium Rural",
          "type": "marketplace",
          "shippingRates": {
            "nodes": [
              {
                "id": "U2hpcHBpbmdSYXRlLTEw",
                "name": "Medium Rural",
                "rateCents": 1500
              }
            ]
          },
          "shippingZones": {
            "nodes": [
              {
                "id": "U2hpcHBpbmdab25lLTM=",
                "name": "Zone 4 - Rural Metropolis",
                "postcodes": [
                  "20005",
                  "20006",
                  "20007",
                  "20008"
                ]
              },
              {
                "id": "U2hpcHBpbmdab25lLTU=",
                "name": "Zone 1 - Metro Gotham",
                "postcodes": [
                  "10001",
                  "10002",
                  "10003",
                  "10004"
                ]
              }
            ]
          },
          "seller": null
        },
        {
          "id": "U2hpcHBpbmdPcHRpb24tOQ==",
          "name": "Medium Metro",
          "type": "marketplace",
          "shippingRates": {
            "nodes": [
              {
                "id": "U2hpcHBpbmdSYXRlLTEz",
                "name": "Medium Metro",
                "rateCents": 1000
              }
            ]
          },
          "shippingZones": {
            "nodes": [
              {
                "id": "U2hpcHBpbmdab25lLTI=",
                "name": "Zone 3 - Metro Metropolis",
                "postcodes": [
                  "20001",
                  "20002",
                  "20003",
                  "20004"
                ]
              },
              {
                "id": "U2hpcHBpbmdab25lLTU=",
                "name": "Zone 1 - Metro Gotham",
                "postcodes": [
                  "10001",
                  "10002",
                  "10003",
                  "10004"
                ]
              }
            ]
          },
          "seller": null
        },
        {
          "id": "U2hpcHBpbmdPcHRpb24tOA==",
          "name": "Small Rural",
          "type": "marketplace",
          "shippingRates": {
            "nodes": [
              {
                "id": "U2hpcHBpbmdSYXRlLTk=",
                "name": "Small Rural",
                "rateCents": 1000
              }
            ]
          },
          "shippingZones": {
            "nodes": [
              {
                "id": "U2hpcHBpbmdab25lLTM=",
                "name": "Zone 4 - Rural Metropolis",
                "postcodes": [
                  "20005",
                  "20006",
                  "20007",
                  "20008"
                ]
              },
              {
                "id": "U2hpcHBpbmdab25lLTQ=",
                "name": "Zone 2 - Rural Gotham",
                "postcodes": [
                  "10005",
                  "10006",
                  "10007",
                  "10008"
                ]
              }
            ]
          },
          "seller": null
        },
        {
          "id": "U2hpcHBpbmdPcHRpb24tNw==",
          "name": "Small Metro",
          "type": "marketplace",
          "shippingRates": {
            "nodes": [
              {
                "id": "U2hpcHBpbmdSYXRlLTEy",
                "name": "Small Metro",
                "rateCents": 500
              }
            ]
          },
          "shippingZones": {
            "nodes": [
              {
                "id": "U2hpcHBpbmdab25lLTI=",
                "name": "Zone 3 - Metro Metropolis",
                "postcodes": [
                  "20001",
                  "20002",
                  "20003",
                  "20004"
                ]
              },
              {
                "id": "U2hpcHBpbmdab25lLTU=",
                "name": "Zone 1 - Metro Gotham",
                "postcodes": [
                  "10001",
                  "10002",
                  "10003",
                  "10004"
                ]
              }
            ]
          },
          "seller": null
        }
      ]
    }
  }
}