How to Build Multi-Carrier Shipping (Rates, Labels, Tracking) using a Unified Shipping API
April 10, 2026
If you need to support shipping across providers like Shippo, EasyPost, FedEx, UPS, DHL, and USPS, you need a consistent way to create shipments, retrieve rates, generate labels, and track deliveries—without building and maintaining separate integrations for each API.
This guide shows how to implement a full shipping workflow using Unified's Shipping API.
Use case: multi-carrier shipping workflow
Build a shipping flow that:
- creates shipments from order data
- retrieves carrier rates
- selects a service
- generates a shipping label
- tracks delivery status
All using a single normalized API.
Core objects
| Object | Purpose |
|---|---|
| Shipment | source of truth for addresses, packages, and fulfillment state |
| Rate | available pricing and delivery options |
| Label | purchased label, tracking number, label URL |
| Tracking | delivery status and event timeline |
| Carrier | optional carrier discovery |
Step 1: (Optional) List carriers
If your product allows carrier selection upfront:
const carriers = await sdk.shipping.listShippingCarriers({
connectionId,
limit: 100,
offset: 0,
sort: 'name',
order: 'asc',
fields: ['id', 'name', 'code', 'is_active'],
});
If you don't need explicit carrier selection, skip this step.
Step 2: Create a shipment
The shipment object anchors the workflow. It contains:
- origin and destination addresses
- package details
- optional fulfillment configuration (signature, insurance, etc.)
const shipment = await sdk.shipping.createShippingShipment({
connectionId,
shippingShipment: {
order_id: 'order_123',
from_address: {
name: 'Warehouse',
address1: '100 Industrial Rd',
city: 'Toronto',
region_code: 'ON',
postal_code: 'M5V 2T6',
country_code: 'CA',
telephone: '+1-416-555-0100',
},
to_address: {
name: 'Jane Doe',
address1: '200 Main St',
city: 'Chicago',
region_code: 'IL',
postal_code: '60601',
country_code: 'US',
telephone: '+1-312-555-0188',
is_residential: true,
},
packages: [
{
weight: 2.5,
weight_unit: 'lb',
length: 10,
width: 8,
height: 4,
size_unit: 'inch',
value: 85,
currency: 'USD',
},
],
reference_number: 'SO-10001',
},
fields: [
'id',
'status',
'carrier_id',
'service_code',
'rate_id',
'label_id',
'tracking_id',
],
});
The shipment can later be updated with:
- selected rate
- carrier/service
- label and tracking references
Step 3: Retrieve shipping rates
Rates can be requested using the shipment_id.
const rateResponse = await sdk.shipping.createShippingRate({
connectionId,
shippingRate: {
shipment_id: shipment.id,
currency: 'USD',
},
fields: ['shipment_id', 'id', 'rates'],
});
Each rate may include:
amount,currencyestimated_days,delivery_daysestimated_delivery_end_atis_guaranteed- surcharge breakdowns
Select a rate based on your product logic:
- lowest cost
- fastest delivery
- guaranteed service
Step 4: Apply selected rate to the shipment
Update the shipment with the selected rate.
const selectedRate = rateResponse.rates?.[0];
const updatedShipment = await sdk.shipping.updateShippingShipment({
connectionId,
id: shipment.id,
shippingShipment: {
carrier_id: selectedRate?.carrier_id,
rate_id: rateResponse.id,
service_code: selectedRate?.code,
},
fields: ['id', 'carrier_id', 'rate_id', 'service_code'],
});
This keeps the shipment aligned with the chosen fulfillment option.
Step 5: Create the shipping label
Label creation produces:
- tracking number
- label URL
- label cost
- service metadata
const label = await sdk.shipping.createShippingLabel({
connectionId,
shippingLabel: {
shipment_id: updatedShipment.id,
rate_id: updatedShipment.rate_id,
service_code: updatedShipment.service_code,
label_format: 'PDF_4X6',
},
fields: [
'id',
'tracking_number',
'label_url',
'label_format',
'status',
'label_cost',
'label_cost_currency',
],
});
At this point, you can:
- render or download the label
- attach tracking to the order
- mark the shipment ready
Step 6: Retrieve label state (if needed)
If label generation is asynchronous or needs verification:
const latestLabel = await sdk.shipping.getShippingLabel({
connectionId,
id: label.id,
fields: [
'id',
'tracking_number',
'label_url',
'status',
'is_voided',
],
});
You can also retrieve labels incrementally:
const labels = await sdk.shipping.listShippingLabels({
connectionId,
shipment_id: shipment.id,
updated_gte: lastSyncAt,
limit: 100,
offset: 0,
sort: 'updated_at',
order: 'asc',
fields: ['id', 'tracking_number', 'status', 'updated_at'],
});
Step 7: Retrieve tracking data
Tracking provides:
- current shipment status
- delivery estimate
- full event timeline
const tracking = await sdk.shipping.getShippingTracking({
connectionId,
id: updatedShipment.tracking_id,
fields: [
'id',
'tracking_number',
'status',
'events',
'estimated_delivery',
'actual_delivery_at',
'status_description',
],
});
Tracking events include:
- timestamped status updates
- carrier-specific event codes
- location data
- delivery confirmation
Step 8: Maintain shipment state incrementally
Use list endpoints with pagination and timestamps:
const shipments = await sdk.shipping.listShippingShipments({
connectionId,
updated_gte: lastSyncAt,
limit: 100,
offset: 0,
sort: 'updated_at',
order: 'asc',
fields: [
'id',
'order_id',
'status',
'carrier_id',
'service_code',
'label_id',
'tracking_id',
'updated_at',
],
});
This supports:
- syncing fulfillment state into your system
- updating order status
- detecting delivery changes
Step 9: Handle updates with webhooks + polling
Shipping integrations do not provide consistent webhook coverage.
In practice:
- some integrations support tracking events
- some support shipment updates
- others require polling
Recommended approach:
Use webhooks where supported
- receive real-time updates
- reduce API calls
Use incremental polling everywhere else
- use
updated_gte - paginate with
limit+offset - sort deterministically
This ensures consistent state across all integrations.
Extended scenarios
The same workflow supports:
International shipping
customsobject- duties and taxes configuration
- importer/exporter data
Returns
is_returnoriginal_shipment_idreturn_reason- return labels
Insurance and delivery controls
insuranceis_signature_requiredis_adult_signature_required
End-to-end flow
const shipment = await sdk.shipping.createShippingShipment({...});
const rateResponse = await sdk.shipping.createShippingRate({
shipment_id: shipment.id,
});
const selectedRate = rateResponse.rates?.[0];
await sdk.shipping.updateShippingShipment({
id: shipment.id,
shippingShipment: {
carrier_id: selectedRate?.carrier_id,
rate_id: rateResponse.id,
service_code: selectedRate?.code,
},
});
const label = await sdk.shipping.createShippingLabel({
shipment_id: shipment.id,
rate_id: rateResponse.id,
});
const tracking = await sdk.shipping.getShippingTracking({
id: shipment.tracking_id,
});
Key takeaways
- Shipping is a multi-step workflow: shipment → rate → label → tracking
- Rates should be tied to a shipment for traceability
- Labels provide the bridge to fulfillment and tracking
- Tracking must be handled as a stateful, event-driven object
- Pagination and
updated_gteenable incremental sync across all integrations - Webhooks are partial—polling is required for full coverage
This structure lets you support multiple shipping providers through one implementation, without maintaining separate integration logic for each.