Australia Post zones and rate data for importing to Shopify.
See "New rate update process" at the end of the document for update instructions.
This project contains PowerShell scripts that can be used to upload shipping rates to Shopify via the API.
You will need to have PowerShell Core (crossplatform) installed to run the scripts.
In the Shopfiy admin, go to Apps > Manage private apps.
You will need to enable private apps, and then create a new app (call it something like "AusPost Rates" or "Data Manager", or whatever name makes sense to you). It will need Read and write permission to Shipping, as well as other functions you want to use, e.g. to use scripts to add products to shipping, you need Read access to Products.
Record the API parameters in variables, which will be used in other scripts. For graph queries you will need the 'Password' value (you don't need the API key for Shared Secret).
$password = '<Password>'
$shopName = '<shop name>'First set up the base URL and headers, using the variables above.
$uri = "https://$shopName.myshopify.com/admin/api/2021-01/graphql.json"
$headers = @{
'Content-Type' = 'application/graphql';
'X-Shopify-Access-Token' = $password
}A simple query can be used to check the existing shipping profiles.
$body = '{
deliveryProfiles(first:10) {
edges {
node {
id
name
default
}
}
}
}'
Invoke-RestMethod -Method Post -Uri $uri -Headers $headers -Body $body | ConvertTo-Json -Depth 5You can also interactively test out queries in the Shopify API developer documentation site: https://shopify.dev/docs/admin-api/graphql/reference/shipping-and-fulfillment/deliveryprofile#samples
You will need to get the delivery profile ID's and location group ID's to use in other queries.
$body = '{
deliveryProfiles(first:10) {
edges {
node {
id
name
default
profileLocationGroups {
locationGroup {
id
}
}
}
}
}
}'
$deliveryProfiles = Invoke-RestMethod -Method Post -Uri $uri -Headers $headers -Body $body
$defaultProfileId = ($deliveryProfiles.data.deliveryProfiles.edges | Where-Object { $_.node.default }).node.id
$defaultProfileIdParametised queries use application/json instead of raw application/graphql, with the query passed as a string
parameter.
You can view the content of a profile, for the zone definitions with the countries in that zone and the list of delivery methods and prices (for different methods or conditions).
Use this to examine your current profiles, or to check the contents after you have created a new profile for Australia Post.
$jsonHeaders = @{
'Content-Type' = 'application/json';
'X-Shopify-Access-Token' = $password
}
$getDeliveryProfileQuery = 'query($id: ID!)
{
deliveryProfile (id: $id) {
profileLocationGroups {
locationGroupZones (first: 20) {
edges {
node {
zone {
id
name
countries {
id
name
code {
countryCode
restOfWorld
}
provinces {
id
code
name
}
}
}
methodDefinitions (first:40) {
edges {
node {
id
name
active
methodConditions {
conditionCriteria {
... on Weight {
unit
value
}
}
field
operator
}
rateProvider {
... on DeliveryRateDefinition {
id
price {
currencyCode
amount
}
}
}
}
}
}
}
}
}
}
}
}'
$getDeliveryProfileData = @{
query = $getDeliveryProfileQuery;
variables = @{
id = $defaultProfileId;
}
}
$defaultProfileDetails = Invoke-RestMethod -Method Post -Uri $uri -Headers $jsonHeaders -Body (ConvertTo-Json -Depth 3 $getDeliveryProfileData)
$defaultProfileDetails | ConvertTo-Json -Depth 15This outputs a summary of the zone name, and countries and provinces allocated to that zone.
$zoneData = $defaultProfileDetails.data.deliveryProfile.profileLocationGroups[0].locationGroupZones.edges | ForEach-Object {
$zone = $_.node.zone
$zone.countries | ForEach-Object {
$country = $_
if ($country.code.restOfWorld) {
[PSCustomObject]@{ zone = $zone.name; country = $null; countryName = $null; province = $null; provinceName = $null }
} else {
if (-not $country.provinces) {
[PSCustomObject]@{ zone = $zone.name; country = $country.code.countryCode; countryName = $country.name; province = $null; provinceName = $null }
} else {;
$country.provinces | ForEach-Object {
$province = $_
[PSCustomObject]@{ zone = $zone.name; country = $country.code.countryCode; countryName = $country.name; province = $province.code; provinceName = $province.name }
}
}
}
}
}
$zoneData | Format-TableThis can then be saved to a comma separated value (CSV) file, e.g. for manipulation in a spreadsheet program.
$zoneData | Export-Csv 'data/zone-country-province.csv'Create a new profile named 'Australia Post' to load the data into.
Use the data files to create zones and assign countries to them, then load the shipping rates for the zones.
The Shopify API reference for delivery profile updates is: https://shopify.dev/docs/admin-api/graphql/reference/shipping-and-fulfillment/deliveryprofileupdate
Get the profile you want to update based on the name:
$deliveryProfile = $deliveryProfiles.data.deliveryProfiles.edges | Where-Object { $_.node.name -eq 'General Profile' }
$deliveryProfile.node.profileLocationGroups | Measure-Object
$locationGroupId = $deliveryProfile.node.profileLocationGroups[0].locationGroup.idA zone-country CSV data file can be used to create zone information for input.
$zoneCountryData = Import-Csv 'data/auspost-zone-country-province.csv'
$zonesToCreate = [System.Collections.ArrayList]@()
$zoneInput = $null
$zoneCountryData | ForEach-Object {
$line = $_
if ($line.zone -ne $zoneInput.name) {
$zoneInput = @{ name = $line.zone; countries = [System.Collections.ArrayList]@() }
$countryInput = $null
$i = $zonesToCreate.Add($zoneInput)
}
if (-not $line.country) {
$i = $zoneInput.countries.Add(@{ restOfWorld = $true })
} else {
if ($line.country -ne $countryInput.code) {
if ($line.province) {
$countryInput = @{ code = $line.country; provinces = [System.Collections.ArrayList]@() }
} else {
$countryInput = @{ code = $line.country; includeAllProvinces = $true }
}
$i = $zoneInput.countries.Add($countryInput)
}
if ($line.province) {
$i = $countryInput.provinces.Add(@{ code = $line.province })
}
}
}
$zonesToCreate | ConvertTo-Json -Depth 5Within a profile, each location group that you ship from has different rates. In the example below there is only one location group to update.
Use the profile selected above, and add the zones to create to the profile location group ID.
$profileLocationGroupInput = @{ id = $locationGroupId; zonesToCreate = $zonesToCreate }Send this as an update.
$updateProfileQuery = 'mutation($id: ID!, $profile: DeliveryProfileInput!) {
deliveryProfileUpdate (id: $id, profile: $profile)
{
profile {
id
name
profileLocationGroups {
locationGroupZones (first: 15) {
edges {
node {
zone {
id
name
}
methodDefinitions (first:20) {
edges {
node {
id
name
rateProvider {
... on DeliveryRateDefinition {
id
price {
currencyCode
amount
}
}
}
}
}
}
}
}
}
}
}
userErrors {
field
message
}
}
}'
$addZones = @{
query = $updateProfileQuery;
variables = @{
id = $deliveryProfile.node.id;
profile = @{
locationGroupsToUpdate = @( $profileLocationGroupInput )
}
}
}
$addZonesResult = Invoke-RestMethod -Method Post -Uri $uri -Headers $jsonHeaders -Body (ConvertTo-Json -Depth 10 $addZones)
$addZonesResultTo update the zones we have created with the rates, first we need to get the created zone IDs.
$getDeliveryProfileZonesQuery = 'query($id: ID!)
{
deliveryProfile (id: $id) {
profileLocationGroups {
locationGroupZones (first: 15) {
edges {
node {
zone {
id
name
}
}
}
}
}
}
}'
$getDeliveryProfileZonesData = @{
query = $getDeliveryProfileZonesQuery;
variables = @{
id = $deliveryProfile.node.id;
}
}
$profileZones = Invoke-RestMethod -Method Post -Uri $uri -Headers $jsonHeaders -Body (ConvertTo-Json -Depth 3 $getDeliveryProfileZonesData)
$zoneIdsAndNames = $profileZones.data.deliveryProfile.profileLocationGroups[0].locationGroupZones.edges.node.zone
$zoneIdsAndNames | Measure-ObjectRead the data file and use it to build the zone updates adding the method definitions.
$zoneRateData = Import-Csv 'data/auspost-rates-to-5kg.csv'
$zonesToUpdate = [System.Collections.ArrayList]@()
$currentZone = $null
$zoneRateData | ForEach-Object {
$line = $_
if ($line.zone -ne $currentZone) {
$currentZone = $line.zone
$zone = $zoneIdsAndNames | Where-Object { $_.name -eq $currentZone}
if (-not $zone) { throw "Zone $($_.name) not found" }
$zoneInput = @{ id = $zone.id; methodDefinitionsToCreate = [System.Collections.ArrayList]@() }
$i = $zonesToUpdate.Add($zoneInput)
}
$weightConditionsInput = [System.Collections.ArrayList]@()
if ([decimal]$line.lessThanKg) {
$i = $weightConditionsInput.Add(@{ criteria = @{ unit = 'GRAMS'; value = [decimal]$line.lessThanKg * 1000; }; operator = 'LESS_THAN_OR_EQUAL_TO' })
}
if ([decimal]$line.greaterThanKg) {
$i = $weightConditionsInput.Add(@{ criteria = @{ unit = 'GRAMS'; value = [decimal]$line.greaterThanKg * 1000; }; operator = 'GREATER_THAN_OR_EQUAL_TO' })
}
$methodInput = @{
active = $true;
name = $line.method;
rateDefinition = @{ price = @{ amount = [decimal]$line.rateAud; currencyCode = 'AUD' } };
weightConditionsToCreate = $weightConditionsInput;
}
$i = $zoneInput.methodDefinitionsToCreate.Add($methodInput)
}
$zonesToUpdate | ConvertTo-Json -Depth 6$updateProfileQuery = 'mutation($id: ID!, $profile: DeliveryProfileInput!) {
deliveryProfileUpdate (id: $id, profile: $profile)
{
profile {
id
name
profileLocationGroups {
locationGroupZones (first: 15) {
edges {
node {
zone {
id
name
}
methodDefinitions (first:40) {
edges {
node {
id
name
}
}
}
}
}
}
}
}
userErrors {
field
message
}
}
}'
$profileLocationGroupUpdateInput = @{ id = $locationGroupId; zonesToUpdate = $zonesToUpdate }If you are replacing rates continue back in the replacing instructions (see below); otherwise continue to add the new rates.
$addRates = @{
query = $updateProfileQuery;
variables = @{
id = $deliveryProfile.node.id;
profile = @{
locationGroupsToUpdate = @( $profileLocationGroupUpdateInput )
}
}
}
$addRatesResult = Invoke-RestMethod -Method Post -Uri $uri -Headers $jsonHeaders -Body (ConvertTo-Json -Depth 11 $addRates)
$addRatesResultTo replace existing rates, you remove all the old method definitions for that profile, and create new ones.
Get the existing profile ID, based on the name (change the name to update different profiles)
$deliveryProfile = $deliveryProfiles.data.deliveryProfiles.edges | Where-Object { $_.node.name -eq 'General Profile' }First get the existing rates. This uses the query from 'Get existing shipping profile information', with the Australia Post delivery profile.
$getDeliveryProfileData = @{
query = $getDeliveryProfileQuery;
variables = @{
id = $deliveryProfile.node.id;
}
}
$profileDetails = Invoke-RestMethod -Method Post -Uri $uri -Headers $jsonHeaders -Body (ConvertTo-Json -Depth 3 $getDeliveryProfileData)The list of existing delivery method definitions can be easily shown:
$methodsToDelete = $profileDetails.data.deliveryProfile.profileLocationGroups.locationGroupZones.edges.node.methodDefinitions.edges.node.id
$methodsToDeleteThen follow the instructions in 'Read shipping rate data' and 'Uploading rates' up to the point where
$profileLocationGroupUpdateInput is created.
Then use both $profileLocationGroupUpdateInput and $methodsToDelete to update the rates.
$updateRates = @{
query = $updateProfileQuery;
variables = @{
id = $deliveryProfile.node.id;
profile = @{
methodDefinitionsToDelete = $methodsToDelete
locationGroupsToUpdate = @( $profileLocationGroupUpdateInput )
}
}
}
$updateRatesResult = Invoke-RestMethod -Method Post -Uri $uri -Headers $jsonHeaders -Body (ConvertTo-Json -Depth 15 $updateRates)
$updateRatesResultIf the data doesn't update in one go cleanly, e.g. you get multiple rate definitions, try deleting separately. Run this, then query the rates again (above, at the start of 'Replacing existing rates') and see if there are any more to delete.
You may need to delete and check multiple times, as it may only do a few (e.g. 40) rows at a time. Check the IDs change each time you query a batch to be deleted:
$updateRates = @{
query = $updateProfileQuery;
variables = @{
id = $deliveryProfile.node.id;
profile = @{
methodDefinitionsToDelete = $methodsToDelete
}
}
}
$updateRatesResult = Invoke-RestMethod -Method Post -Uri $uri -Headers $jsonHeaders -Body (ConvertTo-Json -Depth 15 $updateRates)
$updateRatesResultNew rates:
$updateRates = @{
query = $updateProfileQuery;
variables = @{
id = $deliveryProfile.node.id;
profile = @{
locationGroupsToUpdate = @( $profileLocationGroupUpdateInput )
}
}
}
$updateRatesResult = Invoke-RestMethod -Method Post -Uri $uri -Headers $jsonHeaders -Body (ConvertTo-Json -Depth 15 $updateRates)
$updateRatesResultGet details of the existing profile you want to replace:
$getDeliveryProfileData = @{
query = $getDeliveryProfileQuery;
variables = @{
id = $deliveryProfile.node.id;
}
}
$profileDetails = Invoke-RestMethod -Method Post -Uri $uri -Headers $jsonHeaders -Body (ConvertTo-Json -Depth 3 $getDeliveryProfileData)Get all existing location group zones:
$zonesToDelete = $profileDetails.data.deliveryProfile.profileLocationGroups.locationGroupZones.edges.node.zone.id
$zonesToDeleteThen delete them:
$deleteZones = @{
query = $updateProfileQuery;
variables = @{
id = $deliveryProfile.node.id
profile = @{
zonesToDelete = $zonesToDelete
}
}
}
$deleteZonesResult = Invoke-RestMethod -Method Post -Uri $uri -Headers $jsonHeaders -Body (ConvertTo-Json -Depth 11 $deleteZones)
$deleteZonesResultGet a list of all products you want to assign, e.g. from one vendor.
$getProductsQuery = 'query($first: Int, $filter: String)
{
products(first: $first, query: $filter) {
edges {
node {
id
handle
vendor
status
variants (first: 2) {
edges {
node {
id
title
}
}
}
}
}
pageInfo {
hasNextPage
}
}
}
'
$getProductsData = @{
query = $getProductsQuery;
variables = @{
first = 150;
filter = 'vendor:"Wholesale (AU)"'
}
}
$wholesaleProducts = Invoke-RestMethod -Method Post -Uri $uri -Headers $jsonHeaders -Body (ConvertTo-Json -Depth 3 $getProductsData)
$wholesaleProducts.data.products.edges.node | Measure-ObjectYou can further filter the objects based on properties.
Then use an update query to add them to a delivery profile.
$activeProducts = $wholesaleProducts.data.products.edges.node | ? { $_.status -eq 'ACTIVE' }
$activeProducts | Measure-Object
$activeDeliveryProfileId = ($deliveryProfiles.data.deliveryProfiles.edges | Where-Object { $_.node.name -eq 'Australia Post' }).node.id
$updateProfileQuery = 'mutation($id: ID!, $profile: DeliveryProfileInput!) {
deliveryProfileUpdate (id: $id, profile: $profile)
{
profile {
id
name
profileItems (first: 150) {
edges {
node {
product {
id
handle
vendor
}
variants (first: 2) {
edges {
node {
id
title
}
}
}
}
}
}
}
userErrors {
field
message
}
}
}'
$addActiveProducts = @{
query = $updateProfileQuery;
variables = @{
id = $activeDeliveryProfileId;
profile = @{
variantsToAssociate = $activeProducts.variants.edges.node.id
}
}
}
$addResult1 = Invoke-RestMethod -Method Post -Uri $uri -Headers $jsonHeaders -Body (ConvertTo-Json -Depth 4 $addActiveProducts)
$addResult1You can reuse the same query with different variables:
$otherProducts = $wholesaleProducts.data.products.edges.node | ? { $_.status -ne 'ACTIVE' }
$otherProducts | Measure-Object
$otherDeliveryProfileId = ($deliveryProfiles.data.deliveryProfiles.edges | Where-Object { $_.node.name -eq 'Australia Post 2' }).node.id
$addOtherProducts = @{
query = $updateProfileQuery;
variables = @{
id = $otherDeliveryProfileId;
profile = @{
variantsToAssociate = $otherProducts.variants.edges.node.id
}
}
}
$addResult2 = Invoke-RestMethod -Method Post -Uri $uri -Headers $jsonHeaders -Body (ConvertTo-Json -Depth 4 $addOtherProducts)
$addResult2.data.deliveryProfileUpdate.profileTo update the rates in a second profile.
Follow the process up to Get existing shipping profile information, where you have retrieved $deliveryProfiles. To see all the profile names you can query $deliveryProfiles.data.deliveryProfiles.edges.
Note: Use the name of the rate you want to change, e.g. 'Wholesale Shipping':
$deliveryProfile2 = $deliveryProfiles.data.deliveryProfiles.edges | Where-Object { $_.node.name -eq 'Wholesale Shipping' }
$deliveryProfile2.node.profileLocationGroups | Measure-Object
$locationGroupId2 = $deliveryProfile2.node.profileLocationGroups[0].locationGroup.idGet the existing profile details:
$getDeliveryProfileData2 = @{
query = $getDeliveryProfileQuery;
variables = @{
id = $deliveryProfile2.node.id;
}
}
$profileDetails2 = Invoke-RestMethod -Method Post -Uri $uri -Headers $jsonHeaders -Body (ConvertTo-Json -Depth 3 $getDeliveryProfileData2)Then convert that to the rates to be deleted:
$methodsToDelete2 = $profileDetails2.data.deliveryProfile.profileLocationGroups.locationGroupZones.edges.node.methodDefinitions.edges.node.id
$methodsToDelete2Then follow the instructions in 'Read shipping rate data' and 'Uploading rates' up to the point where
$profileLocationGroupUpdateInput is created.
Get the created zone IDs.
$getDeliveryProfileZonesQuery = 'query($id: ID!)
{
deliveryProfile (id: $id) {
profileLocationGroups {
locationGroupZones (first: 15) {
edges {
node {
zone {
id
name
}
}
}
}
}
}
}'
$getDeliveryProfileZonesData2 = @{
query = $getDeliveryProfileZonesQuery;
variables = @{
id = $deliveryProfile2.node.id;
}
}
$profileZones2 = Invoke-RestMethod -Method Post -Uri $uri -Headers $jsonHeaders -Body (ConvertTo-Json -Depth 3 $getDeliveryProfileZonesData2)
$zoneIdsAndNames2 = $profileZones2.data.deliveryProfile.profileLocationGroups[0].locationGroupZones.edges.node.zone
$zoneIdsAndNames2 | Measure-ObjectRead the data file and use it to build the zone updates adding the method definitions.
Note: Use the file name of the rates being changed, e.g. 'data/auspost-rates-discounted-insured-express-to-4kg.csv'
$zoneRateData2 = Import-Csv 'data/auspost-rates-discounted-insured-express-to-5kg.csv'
$zonesToUpdate2 = [System.Collections.ArrayList]@()
$currentZone = $null
$zoneRateData2 | ForEach-Object {
$line = $_
if ($line.zone -ne $currentZone) {
$currentZone = $line.zone
$zone = $zoneIdsAndNames2 | Where-Object { $_.name -eq $currentZone}
if (-not $zone) { throw "Zone $($_.name) not found" }
$zoneInput = @{ id = $zone.id; methodDefinitionsToCreate = [System.Collections.ArrayList]@() }
$i = $zonesToUpdate2.Add($zoneInput)
}
$weightConditionsInput = [System.Collections.ArrayList]@()
if ([decimal]$line.lessThanKg) {
$i = $weightConditionsInput.Add(@{ criteria = @{ unit = 'GRAMS'; value = [decimal]$line.lessThanKg * 1000; }; operator = 'LESS_THAN_OR_EQUAL_TO' })
}
if ([decimal]$line.greaterThanKg) {
$i = $weightConditionsInput.Add(@{ criteria = @{ unit = 'GRAMS'; value = [decimal]$line.greaterThanKg * 1000; }; operator = 'GREATER_THAN_OR_EQUAL_TO' })
}
$methodInput = @{
active = $true;
name = $line.method;
rateDefinition = @{ price = @{ amount = [decimal]$line.rateAud; currencyCode = 'AUD' } };
weightConditionsToCreate = $weightConditionsInput;
}
$i = $zoneInput.methodDefinitionsToCreate.Add($methodInput)
}
$zonesToUpdate2 | ConvertTo-Json -Depth 6We can then build the query to update the zones:
$updateProfileQuery = 'mutation($id: ID!, $profile: DeliveryProfileInput!) {
deliveryProfileUpdate (id: $id, profile: $profile)
{
profile {
id
name
profileLocationGroups {
locationGroupZones (first: 20) {
edges {
node {
zone {
id
name
}
methodDefinitions (first:40) {
edges {
node {
id
name
}
}
}
}
}
}
}
}
userErrors {
field
message
}
}
}'
$profileLocationGroupUpdateInput2 = @{ id = $locationGroupId2; zonesToUpdate = $zonesToUpdate2 }Then use both $profileLocationGroupUpdateInput2 and $methodsToDelete2 to update the rates.
Build the delete query:
$updateRates = @{
query = $updateProfileQuery;
variables = @{
id = $deliveryProfile2.node.id;
profile = @{
methodDefinitionsToDelete = $methodsToDelete2
}
}
}
$updateRatesResult = Invoke-RestMethod -Method Post -Uri $uri -Headers $jsonHeaders -Body (ConvertTo-Json -Depth 11 $updateRates)
$updateRatesResultAnd then the update:
$updateRates2 = @{
query = $updateProfileQuery;
variables = @{
id = $deliveryProfile2.node.id;
profile = @{
locationGroupsToUpdate = @( $profileLocationGroupUpdateInput2 )
}
}
}
$updateRatesResult = Invoke-RestMethod -Method Post -Uri $uri -Headers $jsonHeaders -Body (ConvertTo-Json -Depth 11 $updateRates2)
$updateRatesResultCheck the rates have updated in the UI.