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.
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:
| Carrier | Module Name | Edition | API Required |
|---|---|---|---|
| FedEx | delivery_fedex | Enterprise | FedEx Web Services / REST API key |
| UPS | delivery_ups | Enterprise | UPS Developer Kit access key |
| DHL | delivery_dhl | Enterprise | DHL Express API credentials |
| USPS | delivery_usps | Enterprise | USPS Web Tools API key |
| Sendcloud | delivery_sendcloud | Enterprise | Sendcloud API key + secret |
| Easypost | delivery_easypost | Enterprise | Easypost API key |
| Fixed / Based on Rules | delivery | Community | None (flat rate or rule-based) |
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:
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.00The 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.
# 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 workStep 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.
# 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}}")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.
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:
- Open the delivery order (or the sales order, if you want to quote shipping before confirmation).
- Click Add Shipping on the sales order or set the Carrier field on the delivery order.
- Use the Get Rate button — Odoo calls the carrier API and returns the quoted price.
- Change the carrier, click Get Rate again, and compare.
- Select the carrier with the best rate or delivery time and proceed.
# 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.
<!-- 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"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.
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:
- Open the delivery order (
Inventory > Operations > Transfers). - Set the Carrier field to your configured shipping method.
- Set quantities done for each product line.
- Click Validate — Odoo calls the carrier API, creates the shipment, and retrieves the label.
- The label appears as a PDF attachment on the delivery order (check the chatter / attachments).
- The Tracking Reference field is populated automatically.
# 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:
- Click Put in Pack for the first group of items — Odoo creates Package 1.
- Set quantities for the next group, click Put in Pack again — Package 2.
- Repeat until all items are packed.
- On Validate, Odoo sends each package to the carrier API separately and retrieves a label per package.
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:
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-4x6If 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.
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:
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.00Automated 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:
# 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 = carrierShipping 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:
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 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.
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:
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 daysTracking URL Generation
Each carrier module generates a clickable tracking URL. The carrier_tracking_url computed field builds the correct URL 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.
# 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 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.
5 Shipping Integration Mistakes That Cost You Money on Every Parcel
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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:
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.
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.
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.
Optimization Metadata
Configure FedEx, UPS, and DHL carrier integrations in Odoo 19. Rate shopping, label printing, tracking numbers, and automated shipping rules for warehouse operations.
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"