Setting Up Shopify Shipping Rates With Claude Code and the Shopify AI Toolkit

Glowing shipping routes converging into a Shopify cart node on a dark world map

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 the userErrors array, 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

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.