Shipping is where a lot of Shopify automation quietly stops. Products, inventory, redirects, metafields all bulk-update cleanly through the Admin API, then someone asks "can you set the shipping rates too?" and the honest answer is "it depends what you mean by shipping rates." This walks through what is actually reachable from Claude Code talking to a store through the Shopify AI toolkit, and where you cross the line from a one-off API call into building and hosting a small app.
How Shopify models shipping
Before any mutation, understand the hierarchy, because the API mirrors it exactly:
Delivery profile contains one or more location groups (which warehouses ship), each containing zones (countries and regions), each containing method definitions (a named, selectable rate). A method definition is either a fixed price, a price-conditioned rate, a weight-conditioned rate, or a pointer to a carrier service that calculates rates live.
Every store has one General profile. You add custom profiles to give specific products their own rates, for example heavy freight, made-to-order furniture, or oversized items that should never qualify for a flat parcel rate.
Two buckets, and the line between them
The key distinction that decides how much work a request is:
| What you want | Where it runs |
|---|---|
| Flat rates, price-based rates ("free over £50"), weight bands | Fully scriptable through the Admin API. Claude Code does it end to end. |
| Real-time carrier rates (live Royal Mail, UPS, DHL quotes, postcode rules engines) | The API only registers it. The rates come from an HTTPS endpoint you host. |
Most merchants only need the first bucket. The second is where the engineering lives.
Prerequisites
1. Authorise with the shipping scope
The toolkit auth context needs write_shipping for changes and read_shipping for reads. Re-auth explicitly, the same way you would add theme or product scopes:
shopify store auth --store=your-store.myshopify.com \
--scopes=read_shipping,write_shipping
2. Read the current state before you touch it
Never blow away an existing delivery profile. Pull the profile, its zones, and its method definitions first, then build mutations against the gaps. This is the introspection query to hand Claude Code:
query {
deliveryProfiles(first: 5) {
edges { node {
id name default
profileLocationGroups {
locationGroupZones(first: 20) { edges { node {
zone { id name countries { code { countryCode } } }
methodDefinitions(first: 20) { edges { node {
id name active
rateProvider { __typename }
}}}
}}}
}
}}
}
}
Part A: static rates the toolkit handles end to end
Flat, price-based, and weight-based rates are all created with the deliveryProfileUpdate mutation by passing methodDefinitionsToCreate into a zone.
A flat rate
mutation {
deliveryProfileUpdate(
id: "gid://shopify/DeliveryProfile/123456789"
profile: {
locationGroupsToUpdate: [{
id: "gid://shopify/DeliveryLocationGroup/111"
zonesToUpdate: [{
id: "gid://shopify/DeliveryZone/222"
methodDefinitionsToCreate: [{
name: "Standard"
active: true
rateDefinition: { price: { amount: 4.95, currencyCode: GBP } }
}]
}]
}]
}
) { profile { id } userErrors { field message } }
}
Price-based, free over a threshold
Add a priceConditions block. Two definitions give "£4.95 under £50, free at or over £50":
methodDefinitionsToCreate: [
{
name: "Standard"
rateDefinition: { price: { amount: 4.95, currencyCode: GBP } }
priceConditions: [{ criteria: { amount: 0, currencyCode: GBP }, operator: GREATER_THAN_OR_EQUAL_TO }]
},
{
name: "Free shipping"
rateDefinition: { price: { amount: 0, currencyCode: GBP } }
priceConditions: [{ criteria: { amount: 50, currencyCode: GBP }, operator: GREATER_THAN_OR_EQUAL_TO }]
}
]
Weight-based bands
Swap priceConditions for weightConditions with unit: KILOGRAMS. Shopify's mutation reference only shows price examples, but the weight input field is live:
{
name: "0 to 2 kg"
rateDefinition: { price: { amount: 3.50, currencyCode: GBP } }
weightConditions: [{ criteria: { value: 2, unit: KILOGRAMS }, operator: LESS_THAN_OR_EQUAL_TO }]
}
The Claude Code workflow. Give it the store and a rate table: zones, bands, thresholds. It introspects the profile, maps your bands to the zone GIDs, builds one
deliveryProfileUpdate, and reads back to confirm. Always check theuserErrorsarray, which returns empty on success.
Part B: carrier-calculated rates
This is how you show live prices computed per cart at checkout instead of pre-set rates. Shopify does not know the price. At checkout it calls an endpoint you host and renders whatever rates you return. The full behaviour is in the CarrierService API reference.
Plan gating. Creating a carrier service fails unless the store is on Advanced or higher, on a plan with yearly billing, has the carrier-calculated shipping add-on, or is a development store.
1. Register the callback URL, the toolkit does this
One API call via carrierServiceCreate. After this the service appears as a rate source inside the merchant's delivery profile.
mutation {
carrierServiceCreate(input: {
name: "Maelify Live Rates"
callbackUrl: "https://rates.yourdomain.com/shopify/rates"
supportsServiceDiscovery: true
active: true
}) {
carrierService { id name callbackUrl active }
userErrors { field message }
}
}
2. Host the endpoint, you build this
Same architecture as any Lambda plus API Gateway middleware. At checkout Shopify POSTs the cart and destination:
{ "rate": {
"destination": { "postal_code": "SW1A 1AA", "country": "GB" },
"items": [ { "name": "...", "quantity": 2, "grams": 500, "price": 1299, "sku": "..." } ],
"currency": "GBP"
}}
You compute rates and respond. Prices are in minor units, so pence or cents:
{ "rates": [{
"service_name": "Royal Mail Tracked 48",
"service_code": "RM48",
"total_price": "495",
"currency": "GBP",
"min_delivery_date": "2026-06-25 00:00:00 +0000",
"max_delivery_date": "2026-06-27 00:00:00 +0000"
}]}
The constraints that make this a build, not a one-liner
| Concern | Behaviour |
|---|---|
| Timeout | Respond fast or Shopify falls back to backup rates. No room for a slow upstream carrier call on the critical path. |
| No retries | One call per rate refresh. A 500 means the customer loses that option, full stop. |
| Caching | Successful rates are cached for 15 minutes. After an error the cache expires in 30 seconds. The cache key is the cart signature plus destination. |
| Redirects | A 30x is only followed on the same domain. A cross-domain redirect triggers backup rates. |
| Transport | The callback must be a public HTTPS URL, always on, not a script you run on demand. |
Build it defensively. Wrap any upstream carrier API in a short timeout with a circuit breaker, and fall back to a sane static rate table rather than erroring. This sits on the checkout critical path. Never let it return nothing.
Division of labour
| Task | Where it runs |
|---|---|
| Flat, price, and weight rates | Admin API, Claude Code via the toolkit, end to end |
| Register a carrier service | Admin API, Claude Code via the toolkit (carrierServiceCreate) |
| Compute live rates at checkout | An endpoint you host and Claude Code scaffolds |
| Read back and audit the config | Admin API, Claude Code via the toolkit |
Reference
- deliveryProfileUpdate: zones, method definitions, rate definitions, price and weight conditions
- deliveryProfileCreate: custom profiles
- DeliveryProfile object: full schema for reads
- carrierServiceCreate: register a live-rate callback
- DeliveryCarrierService object: carrier service schema
- CarrierService REST reference: payloads, caching, timeout, plan gating
- Access scopes: read_shipping and write_shipping
The takeaway
If a client needs flat, price-based, or weight-banded rates, that is a single idempotent mutation run against the live store, introspect-first so nothing existing gets clobbered. If they need real-time carrier quotes, that is a small, well-bounded app: register the callback once through the API, then host an endpoint that rates fast and fails safe. The trap is treating the second like the first. Live rating sits on the checkout critical path, and the difference between a 200 in 300ms and a slow upstream call is the difference between a sale and an empty shipping step.
This is the kind of work we do at Maelify: Shopify Plus architecture where the boring, proven primitive beats the clever one, and the failure modes are designed in rather than discovered in production.