GuideMarch 13, 2026

Shipping Carriers in Odoo 19
Rate Shopping & Label Printing

INTRODUCTION

Your Warehouse Ships 200 Parcels a Day. Your Team Still Copies Tracking Numbers from Carrier Websites.

We audit Odoo warehouses every month. The pattern is always the same: the sales pipeline is automated, invoices post on confirmation, inventory adjusts in real time — and then someone opens a browser tab to FedEx Ship Manager, manually types the recipient address, prints a label, and pastes the tracking number back into Odoo. Two minutes per parcel, 200 parcels a day, one full-time employee doing nothing but copy-paste.

Odoo 19 ships with native integrations for FedEx, UPS, DHL, USPS, bpost, Easypost, and Sendcloud. These aren't third-party connectors that break on API updates — they're maintained in Odoo's core delivery modules. You can fetch live rates, compare carriers, print shipping labels, and push tracking numbers to customers — all without leaving the delivery order form.

This guide walks you through the full setup: from API credentials to your first printed label, then into advanced territory — rate shopping across carriers, automated shipping rules, multi-package shipments, and tracking webhook integration. Every configuration is tested on Odoo 19 Community and Enterprise.

01

How to Configure FedEx, UPS, and DHL Carrier Accounts in Odoo 19

Before you can fetch rates or print labels, each carrier needs API credentials configured in Odoo. The process differs per carrier, but the Odoo side follows the same pattern: install the delivery module, create a shipping method, and enter your account credentials.

Step 1 — Install the Carrier Modules

Navigate to Apps and install the modules for the carriers you use. Each carrier has its own module:

CarrierModule NameEditionAPI Required
FedExdelivery_fedexEnterpriseFedEx Web Services / REST API key
UPSdelivery_upsEnterpriseUPS Developer Kit access key
DHLdelivery_dhlEnterpriseDHL Express API credentials
USPSdelivery_uspsEnterpriseUSPS Web Tools API key
Sendclouddelivery_sendcloudEnterpriseSendcloud API key + secret
Easypostdelivery_easypostEnterpriseEasypost API key
Fixed / Based on RulesdeliveryCommunityNone (flat rate or rule-based)
Community Edition Users

The live-rate carrier integrations (FedEx, UPS, DHL, USPS) require Odoo Enterprise. Community Edition only includes the base delivery module, which supports fixed-price and rule-based shipping methods. If you're on Community and need live rates, look at Easypost via a community connector or use Sendcloud's webhook-based approach.

Step 2 — Create a Shipping Method (FedEx Example)

Go to Inventory > Configuration > Shipping Methods and click New. Here's the configuration for FedEx:

Odoo Configuration — FedEx Shipping Method
Shipping Method:     FedEx International Priority
Provider:            FedEx
Integration Level:   Get Rate and Create Shipment

── FedEx Credentials ──────────────────────────
FedEx Developer Key:     [your-api-key]
FedEx Developer Password:[your-api-password]
FedEx Account Number:    [your-9-digit-account]
FedEx Meter Number:      [your-meter-number]

── Service Options ────────────────────────────
FedEx Service Type:      INTERNATIONAL_PRIORITY
FedEx Package Type:      YOUR_PACKAGING
Weight Unit:             KG
Label Stock Type:        PAPER_4X6

── Pricing ────────────────────────────────────
Delivery Product:        [Shipping Charge] (service-type product)
Margin on Rate:          10%   (markup over carrier cost)
Free if order amount >=  150.00

The Integration Level field is critical. It has two options:

  • Get Rate — fetches live rates but does not create shipments with the carrier. You print labels elsewhere.
  • Get Rate and Create Shipment — fetches rates and creates the shipment via API when you validate the delivery order. This is what generates the label and tracking number directly in Odoo.

Step 3 — Configure Your Warehouse Address

Carrier APIs require a valid ship-from address. Go to Inventory > Configuration > Warehouses, open your warehouse, and ensure the Address field on the linked company or partner record includes a complete street address, city, state/province, postal code, and country. Missing or abbreviated state codes are the #1 cause of API errors on first setup.

Python — Verifying warehouse address via shell
# Odoo shell: verify your warehouse ship-from address
warehouse = env['stock.warehouse'].search([], limit=1)
partner = warehouse.partner_id
print(f"""
  Name:    {{partner.name}}
  Street:  {{partner.street}}
  City:    {{partner.city}}
  State:   {{partner.state_id.code}}
  Zip:     {{partner.zip}}
  Country: {{partner.country_id.code}}
""")
# All fields must be non-empty for carrier APIs to work

Step 4 — Set Product Weights and Dimensions

Carrier rate calculations depend on accurate product weights. For every product you ship, set the Weight field on the product form (under the Inventory tab). For dimensional-weight pricing (FedEx, UPS, DHL all use it), also set Volume or the individual dimension fields if available.

Python — Bulk-update missing product weights
# Find storable products with no weight set
missing = env['product.product'].search([
    ('type', '=', 'consu'),
    ('weight', '<=', 0),
])
print(f"Products missing weight: {{len(missing)}}")
for p in missing[:10]:
    print(f"  [{{p.default_code}}] {{p.name}}")
Dimensional Weight Matters

FedEx and UPS charge the greater of actual weight vs. dimensional weight. A pillow weighing 0.5 kg but occupying a 60x40x30 cm box gets billed at ~4.8 kg dimensional weight. If you only set actual weight in Odoo, the API rate quote will be lower than what the carrier actually charges — and you'll eat the difference on every shipment.

02

Rate Shopping Across FedEx, UPS, and DHL in Odoo 19 Delivery Orders

Rate shopping means querying multiple carriers for the same shipment and selecting the cheapest or fastest option. Odoo 19 supports this natively through the delivery order's carrier selection workflow.

How Rate Shopping Works

When you configure multiple shipping methods (e.g., FedEx Ground, UPS Ground, DHL Express), the sales team or warehouse operator can compare rates directly on the delivery order:

  1. Open the delivery order (or the sales order, if you want to quote shipping before confirmation).
  2. Click Add Shipping on the sales order or set the Carrier field on the delivery order.
  3. Use the Get Rate button — Odoo calls the carrier API and returns the quoted price.
  4. Change the carrier, click Get Rate again, and compare.
  5. Select the carrier with the best rate or delivery time and proceed.
Python — Automated rate comparison across carriers
# Custom method: compare rates across all active carriers
from odoo import models, fields, api

class StockPicking(models.Model):
    _inherit = 'stock.picking'

    def action_compare_carrier_rates(self):
        """Fetch rates from all active carriers and return comparison."""
        carriers = self.env['delivery.carrier'].search([
            ('delivery_type', '!=', 'fixed'),
            ('prod_environment', '=', True),
        ])
        results = []
        for carrier in carriers:
            try:
                res = carrier.rate_shipment(self.sale_id or self)
                if res.get('success'):
                    results.append({
                        'carrier': carrier.name,
                        'price': res['price'],
                        'delivery_days': res.get('transit_days', 'N/A'),
                        'carrier_id': carrier.id,
                    })
            except Exception as e:
                results.append({
                    'carrier': carrier.name,
                    'error': str(e),
                })

        # Sort by price ascending
        results.sort(key=lambda r: r.get('price', 9999))
        return results

The rate_shipment() method is the universal entry point. Every carrier module (FedEx, UPS, DHL) implements it with their specific API calls but returns a standardized dictionary with price, success, error_message, and optional transit_days.

Rate Shopping on the Sales Order (Before Confirmation)

For e-commerce and B2B portals, customers need to see shipping costs before checkout. Enable this by adding shipping methods to the sales order via the Add Shipping button. Odoo calls the carrier API with the order's delivery address and line weights, then adds the shipping charge as a separate order line.

XML — Adding carrier rate widget to e-commerce checkout
<!-- Website Sale: show all carrier options at checkout -->
<!-- Odoo 19 handles this natively via delivery_carrier settings -->

Navigate to: Website > Configuration > Settings
  Section: Shipping
    [x] Delivery Methods: enabled
    [x] Show delivery options on checkout

Then under Inventory > Configuration > Shipping Methods,
for each method, check:
    [x] Website Published
    Website name: "FedEx Ground (3-5 business days)"
    Website description: "Reliable ground shipping via FedEx"
Performance Warning: API Calls at Checkout

Each published shipping method triggers a live API call when the checkout page loads. If you have 6 carriers published, that's 6 API calls — and if FedEx's API takes 3 seconds to respond, your checkout page takes 3+ seconds to render shipping options. Keep published carriers to 3-4 maximum, or implement caching with a 15-minute TTL for repeated quotes to the same zip code.

03

Printing Shipping Labels and Managing Multi-Package Shipments in Odoo 19

Once the carrier is selected on a delivery order, validating the transfer triggers the API call that creates the shipment and returns the label. Here's the full workflow:

  1. Open the delivery order (Inventory > Operations > Transfers).
  2. Set the Carrier field to your configured shipping method.
  3. Set quantities done for each product line.
  4. Click Validate — Odoo calls the carrier API, creates the shipment, and retrieves the label.
  5. The label appears as a PDF attachment on the delivery order (check the chatter / attachments).
  6. The Tracking Reference field is populated automatically.
Python — How Odoo generates the label internally
# Simplified flow when you click Validate on a delivery order
# File: addons/delivery/models/stock_picking.py

def button_validate(self):
    # ... standard validation logic ...

    # If a carrier is set with "Get Rate and Create Shipment"
    if self.carrier_id and self.carrier_id.integration_level == 'rate_and_ship':
        res = self.carrier_id.send_shipping(self)
        # res contains:
        # {
        #     'exact_price': 15.42,
        #     'tracking_number': '794644790132',
        # }
        self.carrier_price = res[0]['exact_price']
        self.carrier_tracking_ref = res[0]['tracking_number']

        # Label is attached as binary on the picking
        # Access via: self.message_ids or ir.attachment
        # Label format: PDF (4x6 thermal) or PNG

    return super().button_validate()

Multi-Package Shipments

When an order requires multiple boxes, use Odoo's Packages feature. Before validating the delivery order:

  1. Click Put in Pack for the first group of items — Odoo creates Package 1.
  2. Set quantities for the next group, click Put in Pack again — Package 2.
  3. Repeat until all items are packed.
  4. On Validate, Odoo sends each package to the carrier API separately and retrieves a label per package.
Odoo Configuration — Package Types
Navigate to: Inventory > Configuration > Package Types

Name:              Small Box
Package Carrier:   FedEx
Width:             30 cm
Height:            20 cm
Length:            25 cm
Max Weight:        5.0 kg
Carrier Code:      FEDEX_SMALL_BOX_1

Name:              Medium Box
Package Carrier:   FedEx
Width:             45 cm
Height:            30 cm
Length:            35 cm
Max Weight:        15.0 kg
Carrier Code:      FEDEX_MEDIUM_BOX_1

Name:              UPS Standard Box
Package Carrier:   UPS
Width:             40 cm
Height:            30 cm
Length:            30 cm
Max Weight:        20.0 kg
Carrier Code:      02  (UPS Customer Supplied Package)

Thermal Printer Setup (ZPL Labels)

Warehouse teams need labels printed on thermal printers (Zebra, DYMO), not office laser printers. Configure this at the carrier level:

Configuration — Thermal label settings
Shipping Method > FedEx Ground
  Label Stock Type:    STOCK_4X6    (thermal 4x6)
  Label File Type:     ZPL          (Zebra Programming Language)

Shipping Method > UPS Ground
  Label Format:        ZPL
  Label Size:          4x6

── IoT Box / Direct Print Setup ───────────────
Inventory > Configuration > Settings
  [x] IoT Box: enabled (for direct thermal printing)
  OR
  Use browser print dialog with ZPL-to-PDF conversion

── Alternative: CUPS direct printing ──────────
# On the Odoo server, configure CUPS for the Zebra printer
sudo lpadmin -p zebra-4x6 -E \
  -v usb://Zebra/ZD421 \
  -m raw
sudo cupsaccept zebra-4x6
sudo cupsenable zebra-4x6
Label Reprint Without Re-Creating the Shipment

If a label prints incorrectly (paper jam, wrong orientation), do not void the shipment and re-validate. The label PDF is stored as an attachment on the delivery order. Open the chatter, find the attachment, and print it again. Voiding and re-creating generates a new tracking number, which means any tracking link already sent to the customer becomes invalid.

04

Automating Carrier Selection with Shipping Rules and Delivery Methods

Manual carrier selection doesn't scale. When you ship 200+ parcels daily, the warehouse team shouldn't decide between FedEx and UPS on every order. Odoo 19 lets you automate this decision with rule-based shipping methods and delivery carrier assignment on sales orders.

Rule-Based Shipping Methods

The base delivery module includes a "Based on Rules" provider type. This lets you set shipping prices (and assign carriers) based on order weight, price, quantity, or destination:

Configuration — Rule-based shipping example
Shipping Method: Domestic Standard
Provider:        Based on Rules

── Pricing Rules ──────────────────────────────
Rule 1: Weight <= 2 kg      → $7.99
Rule 2: Weight <= 10 kg     → $12.99
Rule 3: Weight <= 30 kg     → $24.99
Rule 4: Weight > 30 kg      → $24.99 + $0.80/kg over 30

── Free Shipping Threshold ────────────────────
Free if order amount >= $99.00

Automated Carrier Assignment via Server Actions

For automatic carrier assignment based on business logic (destination country, product type, customer tier), create a server action that runs on delivery order creation:

Python — Auto-assign carrier based on destination
# Server action or automated action on stock.picking creation
# Trigger: On Creation of Delivery Order (outgoing)

picking = env['stock.picking'].browse(record.id)

if not picking.carrier_id and picking.partner_id:
    dest_country = picking.partner_id.country_id.code
    order_weight = sum(
        m.product_id.weight * m.product_uom_qty
        for m in picking.move_ids
    )

    # Domestic: use cheapest ground carrier
    if dest_country == 'US':
        if order_weight <= 2.0:
            # USPS First Class — cheapest for light packages
            carrier = env.ref('delivery_usps.usps_first_class')
        elif order_weight <= 30.0:
            # UPS Ground — best domestic ground rate
            carrier = env.ref('delivery_ups.ups_ground')
        else:
            # FedEx Freight for heavy shipments
            carrier = env.ref('delivery_fedex.fedex_freight')

    # Canada: DHL or FedEx International
    elif dest_country == 'CA':
        carrier = env.ref('delivery_fedex.fedex_intl_economy')

    # Europe: DHL Express
    elif dest_country in ('DE','FR','GB','ES','IT','NL','BE'):
        carrier = env.ref('delivery_dhl.dhl_express_worldwide')

    # Rest of world: FedEx International Priority
    else:
        carrier = env.ref('delivery_fedex.fedex_intl_priority')

    picking.carrier_id = carrier

Shipping Rules on the E-Commerce Website

For online stores, control which shipping options appear at checkout based on the customer's address. Each shipping method has a Destination Availability section:

Configuration — Restrict carriers by destination
Shipping Method > FedEx Ground
  Countries:     United States, Canada
  States:        (leave empty for all)
  Zip Prefixes:  (leave empty for all)

Shipping Method > DHL Express Worldwide
  Countries:     (all European countries)
  States:        (leave empty)
  Zip Prefixes:  (leave empty)

Shipping Method > USPS First Class
  Countries:     United States
  Max Weight:    1.8 kg (USPS limit)

── Result at checkout ─────────────────────────
Customer in New York sees:  FedEx Ground, USPS First Class
Customer in Berlin sees:    DHL Express Worldwide
Customer in Tokyo sees:     FedEx International Priority
Saturday Delivery and Service Level Overrides

Some B2B customers have contracted shipping accounts or require Saturday delivery. Use partner-level carrier overrides: on the customer's contact form, set a default Delivery Method. When a sales order is created for that customer, Odoo pre-fills the carrier. This is especially useful for wholesale accounts where the customer's own FedEx/UPS account number should be used for billing.

05

Tracking Numbers, Customer Notifications, and Delivery Status Updates

When Odoo creates a shipment via the carrier API, the tracking number is stored in the carrier_tracking_ref field on the delivery order. But a tracking number sitting in a database field is useless if the customer never sees it. Here's how to close the loop:

Automatic Tracking Email to Customers

Odoo 19 can send a tracking notification email automatically when the delivery order is validated. Enable this in the shipping method configuration:

Configuration — Tracking email setup
Inventory > Configuration > Settings
  Section: Delivery
    [x] Send tracking email to customer on delivery validation

── Email Template Customization ───────────────
Settings > Technical > Email Templates
  Search: "Delivery Tracking"

  Subject: Your order {{object.sale_id.name}} has shipped!
  Body:
    Hi {{object.partner_id.name}},

    Great news — your order has been shipped.

    Carrier: {{object.carrier_id.name}}
    Tracking: {{object.carrier_tracking_ref}}
    Track your package: {{object.carrier_tracking_url}}

    Expected delivery: 3-5 business days

Tracking URL Generation

Each carrier module generates a clickable tracking URL. The carrier_tracking_url computed field builds the correct URL per carrier:

Python — How tracking URLs are built per carrier
# Each carrier module implements get_tracking_link()
# These are the URL patterns used internally:

# FedEx
f"https://www.fedex.com/fedextrack/?trknbr={{tracking_number}}"

# UPS
f"https://www.ups.com/track?tracknum={{tracking_number}}"

# DHL
f"https://www.dhl.com/en/express/tracking.html?AWB={{tracking_number}}"

# USPS
f"https://tools.usps.com/go/TrackConfirmAction?tLabels={{tracking_number}}"

# Multi-package: tracking refs are comma-separated
# e.g., "794644790132,794644790133"
# The tracking URL links to the first package;
# all packages are visible in the customer portal.

Customer Portal Tracking

When customers log into their Odoo portal and view their order, the delivery status and tracking link appear automatically. No additional configuration is needed — the website_sale module reads carrier_tracking_ref and carrier_tracking_url from the linked delivery order and renders them in the order detail page.

Python — Batch retrieve tracking status for open shipments
# Cron job: update delivery status from carrier APIs
# Useful for displaying "In Transit" / "Delivered" in portal

open_pickings = env['stock.picking'].search([
    ('state', '=', 'done'),
    ('carrier_tracking_ref', '!=', False),
    ('delivery_status', '!=', 'delivered'),
    ('date_done', '>=', fields.Date.subtract(fields.Date.today(), days=30)),
])

for picking in open_pickings:
    try:
        status = picking.carrier_id.get_tracking_status(
            picking.carrier_tracking_ref
        )
        if status == 'DELIVERED':
            picking.delivery_status = 'delivered'
            picking.message_post(
                body=f"Package delivered per {{picking.carrier_id.name}} tracking."
            )
    except Exception:
        continue  # API timeout — retry next cron run
Multi-Package Tracking References

When a delivery order has multiple packages, Odoo stores all tracking numbers in a single field, comma-separated (e.g., 1Z999AA10123456784,1Z999AA10123456785). The customer email template and portal display handle this correctly, showing each package with its own tracking link. However, if you're building custom integrations that read carrier_tracking_ref, always split(',') the value — don't assume it's a single tracking number.

06

5 Shipping Integration Mistakes That Cost You Money on Every Parcel

1

Testing with Production API Credentials

Every carrier provides a sandbox/test environment. FedEx has a test URL, UPS has an integration environment, DHL has a staging endpoint. If you configure production credentials during setup and click "Get Rate" 50 times while testing, some carriers count those as billable API calls. Worse, if you accidentally click "Validate" on a test delivery order with production credentials, you've just created a real shipment and will be billed for it.

Our Fix

Always start with the Test Environment checkbox enabled on the shipping method. Only switch to production after your label format, package types, and rate calculations are confirmed correct. Use a separate Odoo database or a staging environment for carrier integration testing.

2

Missing or Wrong Product Weights Causing Rate Discrepancies

The #1 support ticket we get after shipping go-live: "The rate Odoo quoted was $12, but FedEx charged us $28." The root cause is almost always product weight set to 0 or set in the wrong unit. When weight is 0, Odoo sends a minimum weight (often 0.1 kg) to the API. The actual package weighs 8 kg. The carrier invoices for the actual weight scanned at pickup, not the API estimate.

Our Fix

Run a weight audit before go-live. Query all storable products where weight <= 0 and block shipping method activation until the count is zero. Add a server action that prevents delivery order validation if any move line has a product with zero weight.

3

Voiding Shipments Without Notifying the Customer

A warehouse operator voids a shipment in Odoo (wrong address, package damage). Odoo cancels the shipment with the carrier API and clears the tracking number. But the customer already received the tracking email and is refreshing the FedEx tracking page. The tracking number now shows "Shipment Cancelled" with no explanation. The customer calls support.

Our Fix

Create an automated action on the stock.picking model that triggers when carrier_tracking_ref changes from a non-empty value to empty (void event). The action sends a notification email: "Your shipment is being re-processed. You'll receive a new tracking number shortly." This prevents support calls.

4

Not Handling Carrier API Downtime Gracefully

FedEx's API goes down for maintenance. Your warehouse operator clicks "Validate" on a delivery order and gets a cryptic Python traceback instead of a helpful error message. The entire picking queue stalls because every validation attempt fails. The operator starts processing orders manually outside Odoo to keep packages moving, creating data discrepancies that take days to reconcile.

Our Fix

Implement a fallback workflow: when the carrier API returns a connection error, catch the exception and allow the operator to validate the delivery without creating the shipment. Flag these orders with a "Pending Label" tag. A scheduled action retries label creation every 15 minutes for flagged orders. This keeps the warehouse moving during carrier outages.

5

International Shipments Without Customs Documentation

An international FedEx or DHL shipment requires a Commercial Invoice with HS codes, declared values, and country of origin for each product. If these fields are empty in Odoo, the carrier API either rejects the shipment outright or generates a label without customs data — which means the package gets held at customs, incurring storage fees and delivery delays of 2-4 weeks.

Our Fix

Set HS Code and Country of Origin on every product (Product > Inventory tab). Add a validation rule that blocks international delivery order creation if any product in the order is missing these fields. For DHL, also configure the Export Reason (Sale, Gift, Sample) on the shipping method.

BUSINESS ROI

What Integrated Shipping Saves Your Warehouse Operation

Shipping automation isn't about technology — it's about eliminating the most expensive manual process in your fulfillment workflow. Here's what changes:

90 secPer-Parcel Processing (was 4+ min)

No more tab-switching to carrier websites. Label prints from the same screen where the operator confirms quantities. At 200 parcels/day, that's 8+ hours saved daily.

12-18%Shipping Cost Reduction

Rate shopping automatically selects the cheapest carrier per shipment. Clients who shipped everything via FedEx Ground discover that USPS First Class is 40% cheaper for packages under 1 kg.

ZeroManual Tracking Copy-Paste

Tracking numbers flow from carrier API to delivery order to customer email to portal — automatically. No human touches the tracking number at any point.

The compounding benefit is data accuracy. When every shipment is created through the API, you get a perfect audit trail: exact shipping costs per order (not estimates), actual vs. quoted rate variance reports, carrier performance analytics, and on-time delivery metrics. This data drives your next carrier contract negotiation — and with 12 months of per-shipment cost data, you negotiate from a position of strength.

SEO NOTES

Optimization Metadata

Meta Desc

Configure FedEx, UPS, and DHL carrier integrations in Odoo 19. Rate shopping, label printing, tracking numbers, and automated shipping rules for warehouse operations.

H2 Keywords

1. "How to Configure FedEx, UPS, and DHL Carrier Accounts in Odoo 19"
2. "Rate Shopping Across FedEx, UPS, and DHL in Odoo 19 Delivery Orders"
3. "Printing Shipping Labels and Managing Multi-Package Shipments in Odoo 19"
4. "5 Shipping Integration Mistakes That Cost You Money on Every Parcel"

Stop Printing Labels from Carrier Websites

Every label printed from FedEx.com or UPS.com is a label that didn't generate a tracking number in Odoo, didn't trigger a customer notification, and didn't feed your shipping cost analytics. The carrier integrations are built into Odoo 19 — the gap isn't technology, it's configuration.

If you're shipping more than 50 parcels a day and still managing carriers outside Odoo, let's fix that. We configure carrier integrations, set up rate shopping rules, build custom shipping automation, and train warehouse teams on the new workflow. Most clients are printing their first Odoo-generated label within a week.

Book a Free Shipping Integration Assessment