How to use Shipping Rules
15 minute read
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
IMPORTANT
Shipping Rules are only available for use in Connected environments, (i.e. if you are using theorderCreate
mutation to create orders in Marketplacer) and are not supported in either Full-Stack or Headless configurations.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:
Entity | Purpose | Created By | GraphQL Object |
---|---|---|---|
Shipping Rate | The cost (rate) that can be applied by the rule. You can define multiple rates. | Operators (Admins) or Sellers | ShippingRate |
Shipping Zone | A zone is made up of a list of Zip or Post codes that defines a geographic area. | Operators (Admins) or Sellers | ShippingZone |
Shipping Profile | Determined 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 Variants | Operators (Admins) only | ShippingProfile |
Shipping Rule | Comprised of: Shipping Rates, Shipping Zones and Shipping Profiles | Operators (Admins) or Sellers | ShippingOption |
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:
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)
Calculating the shipping cost
Shipping rules provide a reference data framework for operators and sellers to use, but it is up to the operator at the point of checkout to determine the appropriate shipping cost based on the shipping rule data, as well as other data inputs such as the customer’s delivery address.Creating Shipping Rules
Shipping Rule Design
Given the level of flexibility of the shipping rule framework, and the varied and individualist nature of operator and seller shipping requirements, attempting to detail design patterns to encompass these requirements is out of scope for this document.
For further guidance on how to design your shipping rules:
- Refer to the Marketplacer Knowledge Base articles on shipping rules
- Speak to your Marketplacer point of contact for a chat
The remainder of this article focused on how you can manage shipping rules using the operator API.
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
- Click Save
Repeat this step to add the additional 2 Shipping Profiles.
API Vs. Portal
For the remainder of this article we are only going to use the Operator API to manage our shipping rules, you can of course continue to use the Operator Portal if you wish.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
Rule | Profile | Zones | Rates |
---|---|---|---|
Small Metro | Small (<= 5Kg) | - Zone 1 - Metro Gotham - Zone 3 - Metro Metropolis | Small Metro |
Small Rural | Small (<= 5Kg) | - Zone 2 - Rural Gotham - Zone 4 - Rural Metropolis | Small Rural |
Medium Metro | Medium (> 5kg <= 10Kg) | - Zone 1 - Metro Gotham - Zone 3 - Metro Metropolis | Medium Metro |
Medium Rural | Medium (> 5kg <= 10Kg) | - Zone 2 - Rural Gotham - Zone 4 - Rural Metropolis | Medium Rural |
Large Metro | Large (> 10Kg) | - Zone 1 - Metro Gotham - Zone 3 - Metro Metropolis | Large Metro |
Large Rural | Large (> 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
- Shipping Rules
- Shipping Settings
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”:
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 allshippingZones
andshippingRules
- “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
}
]
}
}
}