The Carrier Service API is one of Shopify's most powerful features for stores that need custom shipping logic. Whether you're calculating rates based on product dimensions, integrating with a 3PL, or implementing zone-based pricing, this API gives you full control.
What is the Carrier Service API?
The Carrier Service API allows your app to provide real-time shipping rates at checkout. When a customer enters their address, Shopify sends a request to your endpoint, and you return the available shipping options with prices.
This is different from Shopify's built-in shipping profiles, which only support flat rates or weight-based calculations. With the Carrier Service API, you can factor in:
- •Product dimensions and packaging logic
- •Customer location (down to postcode/zip level)
- •Real-time quotes from carriers like Royal Mail, DPD, or FedEx
- •Custom business rules (free shipping thresholds, surcharges, etc.)
Setting up the basics
First, you need to register your carrier service with the store. This is done via the Admin API:
// Register a carrier service
const response = await shopify.rest.CarrierService.create({
session,
carrier_service: {
name: "Custom Rates",
callback_url: "https://your-app.com/api/shipping-rates",
service_discovery: true,
format: "json"
}
});The callback_url is where Shopify will POST requests when rates are needed. Make sure this endpoint is publicly accessible and responds within 10 seconds—Shopify will timeout after that.
Handling the callback request
When Shopify needs rates, it sends a POST request with the cart contents and destination. Here's what the payload looks like:
{
"rate": {
"origin": {
"country": "GB",
"postal_code": "SW1A 1AA",
"city": "London"
},
"destination": {
"country": "GB",
"postal_code": "M1 1AA",
"city": "Manchester"
},
"items": [
{
"name": "Product Name",
"sku": "SKU-001",
"quantity": 2,
"grams": 500,
"price": 2999,
"vendor": "Your Store",
"requires_shipping": true
}
],
"currency": "GBP"
}
}Building the response
Your endpoint needs to return an array of available shipping rates. Here's a practical example:
// api/shipping-rates.js
export default async function handler(req, res) {
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' });
}
const { rate } = req.body;
const { destination, items } = rate;
// Calculate total weight in kg
const totalGrams = items.reduce((sum, item) => {
return sum + (item.grams * item.quantity);
}, 0);
const totalKg = totalGrams / 1000;
// Calculate cart total (prices are in cents)
const cartTotal = items.reduce((sum, item) => {
return sum + (item.price * item.quantity);
}, 0) / 100;
const rates = [];
// Standard shipping
let standardPrice = calculateStandardRate(destination, totalKg);
// Free shipping over £50
if (cartTotal >= 50) {
standardPrice = 0;
}
rates.push({
service_name: "Standard Delivery",
service_code: "standard",
total_price: Math.round(standardPrice * 100).toString(),
currency: rate.currency,
min_delivery_date: addBusinessDays(3),
max_delivery_date: addBusinessDays(5)
});
// Express option for UK only
if (destination.country === 'GB') {
rates.push({
service_name: "Express Delivery",
service_code: "express",
total_price: "799",
currency: rate.currency,
min_delivery_date: addBusinessDays(1),
max_delivery_date: addBusinessDays(2)
});
}
return res.json({ rates });
}Common pitfalls and how to avoid them
1. Price format
The total_price must be a string in the smallest currency unit (cents/pence). This catches a lot of developers out:
// Wrong - will show as £0.04
total_price: 4.99
// Wrong - number instead of string
total_price: 499
// Correct - £4.99
total_price: "499"2. Timeout handling
Shopify only waits 10 seconds for your response. If you're calling external carrier APIs, implement caching and fallbacks:
async function getShippingRates(destination, items) {
const cacheKey = generateCacheKey(destination, items);
// Check cache first
const cached = await redis.get(cacheKey);
if (cached) {
return JSON.parse(cached);
}
try {
// Try to get live rates with a 5s timeout
const rates = await Promise.race([
fetchLiveRates(destination, items),
timeout(5000)
]);
// Cache for 15 minutes
await redis.setex(cacheKey, 900, JSON.stringify(rates));
return rates;
} catch (error) {
// Fall back to calculated rates
return calculateFallbackRates(destination, items);
}
}3. Empty rates response
If you return an empty rates array, Shopify will show "No shipping options available" at checkout. Always provide at least one fallback option:
if (rates.length === 0) {
rates.push({
service_name: "Standard Shipping",
service_code: "fallback",
total_price: "999",
currency: rate.currency
});
}4. International shipping
Don't forget to handle international destinations. At minimum, check if you ship to the destination country:
const shippableCountries = ['GB', 'IE', 'FR', 'DE', 'ES'];
if (!shippableCountries.includes(destination.country)) {
return res.json({
rates: [{
service_name: "International Shipping",
service_code: "international",
total_price: "1999",
currency: rate.currency,
description: "Contact us for delivery estimate"
}]
});
}Zone-based pricing example
Here's a practical pattern for UK postcode zones:
const UK_ZONES = {
// Zone 1: London & South East
zone1: ['EC', 'WC', 'E', 'N', 'NW', 'SE', 'SW', 'W', 'BR', 'CR', 'DA'],
// Zone 2: Midlands & North
zone2: ['B', 'CV', 'DE', 'LE', 'NG', 'M', 'L', 'S', 'LS', 'BD'],
// Zone 3: Scotland & Highlands
zone3: ['EH', 'G', 'FK', 'DD', 'AB', 'IV', 'KW', 'PA', 'PH']
};
function getZonePrice(postcode, basePrice) {
const prefix = postcode.replace(/[0-9]/g, '').toUpperCase();
if (UK_ZONES.zone1.includes(prefix)) {
return basePrice;
} else if (UK_ZONES.zone2.includes(prefix)) {
return basePrice + 2.00;
} else if (UK_ZONES.zone3.includes(prefix)) {
return basePrice + 5.00;
}
// Default for unrecognised postcodes
return basePrice + 3.00;
}Testing your implementation
Shopify provides a way to test your carrier service without going through checkout. Use this GraphQL query:
mutation {
carrierServiceUpdate(
id: "gid://shopify/CarrierService/123456"
input: { callbackUrl: "https://your-ngrok-url.com/api/shipping-rates" }
) {
carrierService {
id
callbackUrl
}
}
}During development, use ngrok or similar to expose your local endpoint. Update the callback URL as needed—just remember to set it back to production when you're done.
Performance tips
- •Cache aggressively: Rates for the same postcode/weight combo rarely change
- •Use connection pooling: If calling carrier APIs, reuse HTTP connections
- •Implement circuit breakers: If a carrier API is down, fail fast to fallback rates
- •Log everything: When rates look wrong, you'll want to see exactly what was requested
Wrapping up
The Carrier Service API is powerful but has some quirks. The main things to remember:
- •Prices are strings in the smallest currency unit
- •You have 10 seconds to respond
- •Always provide fallback rates
- •Cache external API calls aggressively
Once you've got the basics working, you can build sophisticated shipping logic that would be impossible with Shopify's built-in options.