Skip to main content

Logistics & Transport Emissions Tutorial

Learn how to calculate emissions from transport operations using Dcycle’s ISO 14083-compliant Logistics API.
Estimated time: 25 minutesWhat you’ll learn:
  • Calculate single shipment emissions
  • Understand ISO 14083 methodology
  • Upload bulk shipment data
  • Track emissions by client and route
  • Generate logistics reports

Prerequisites

Before starting, ensure you have:
  • Dcycle API credentials (get them here)
  • Basic knowledge of Python or JavaScript
  • Shipment data: origin, destination, weight, transport mode
Using the Dcycle App?You can also track logistics emissions through our web interface:

Understanding Transport Emissions

Logistics emissions are typically Scope 3 (upstream or downstream transportation in your value chain).

ISO 14083 Standard

Dcycle follows ISO 14083, the international standard for quantifying and reporting GHG emissions from transport operations. Key features:
  • Well-to-Wheel (WTW) methodology: Includes fuel production + vehicle operation
  • Distance-based calculation: Accurate route distance with geocoding
  • Load factor consideration: Accounts for vehicle capacity utilization
  • Multi-modal support: Road, rail, sea, air, and intermodal transport

Transport Calculation Flow

Shipment Data β†’ Geocoding β†’ Distance Calculation β†’ TOC Selection β†’ Emission Factors β†’ CO2e
     ↓              ↓              ↓                    ↓                ↓            ↓
  Origin/         Lat/Lng       Route km         Vehicle type      WTW factors    Result
  Destination                                     & fuel

WTW (Well-to-Wheel) Breakdown

1

WTT (Well-to-Tank)

Upstream emissions: Fuel extraction, refining, and distributionExample: 0.5 kg CO2e per liter of diesel
2

TTW (Tank-to-Wheel)

Direct emissions: Fuel combustion in vehicleExample: 2.7 kg CO2e per liter of diesel
3

Total WTW

Complete footprint: WTT + TTWExample: 3.2 kg CO2e per liter of diesel

Step 1: Calculate a Single Shipment

Start with a simple shipment calculation:
import requests
import os

headers = {
    "Authorization": f"Bearer {os.getenv('DCYCLE_API_KEY')}",
    "x-organization-id": os.getenv("DCYCLE_ORG_ID"),
    "x-user-id": os.getenv("DCYCLE_USER_ID")
}

# Calculate shipment emissions
shipment = {
    "origin": "Madrid, Spain",
    "destination": "Barcelona, Spain",
    "toc": "rigid_diesel",  # Transport Operation Category
    "load": 5000,           # Weight in kg
    "load_unit": "kg",
    "year": 2024
}

response = requests.post(
    "https://api.dcycle.io/api/v1/logistics/calculate",
    headers=headers,
    json=shipment
).json()

print(f"βœ… Shipment calculated")
print(f"   Distance: {response['distance_km']:.1f} km")
print(f"   CO2e (WTW): {response['co2e']:.2f} kg")
print(f"   - TTW (direct): {response['co2e_ttw']:.2f} kg")
print(f"   - WTT (upstream): {response['co2e_wtw']:.2f} kg")
print(f"   Intensity: {response['co2e']/response['distance_km']:.3f} kg CO2e/km")

Understanding TOCs (Transport Operation Categories)

ISO 14083 defines standard vehicle categories:
TOCDescriptionTypical Use
van_dieselLight commercial vehicleUrban deliveries, courier
rigid_dieselSingle-unit truckRegional freight
articulated_dieselTractor-trailerLong-haul freight
rail_freightFreight trainBulk goods
maritime_containerContainer shipInternational shipping
air_cargoCargo aircraftExpress shipments
Get all available TOCs:
# List all Transport Operation Categories
tocs = requests.get(
    "https://api.dcycle.io/api/v1/logistics/tocs",
    headers=headers
).json()

print("Available Transport Categories:")
for toc in tocs:
    print(f"  - {toc['code']}: {toc['description']}")
    print(f"    Fuel: {toc['fuel_type']}, Category: {toc['vehicle_category']}")

Step 2: Advanced Shipment Parameters

Specify Exact Locations

For more accurate distance calculation:
shipment = {
    "origin_address": "Calle de AlcalΓ‘, 123, 28009 Madrid",
    "origin_postal_code": "28009",
    "origin_country": "ES",
    "destination_address": "Passeig de GrΓ cia, 45, 08007 Barcelona",
    "destination_postal_code": "08007",
    "destination_country": "ES",
    "toc": "rigid_diesel",
    "load": 5000,
    "load_unit": "kg",
    "year": 2024
}

response = requests.post(url, headers=headers, json=shipment).json()

Multi-Client Logistics Operations

Track shipments by client:
# First, create clients
client = {
    "name": "Client A - Retail Co",
    "contact_email": "[email protected]",
    "client_code": "CLIENT-A-001"
}

client_response = requests.post(
    "https://api.dcycle.io/api/v1/logistics/clients",
    headers=headers,
    json=client
).json()

client_id = client_response['id']

# Then associate shipments with clients
shipment = {
    "origin": "Warehouse Madrid",
    "destination": "Client A Store - Barcelona",
    "client_id": client_id,  # Link to client
    "toc": "van_diesel",
    "load": 1200,
    "load_unit": "kg",
    "year": 2024
}

response = requests.post(url, headers=headers, json=shipment).json()

Shipment Metadata

Add custom fields for reporting:
shipment = {
    "origin": "Madrid",
    "destination": "Barcelona",
    "toc": "rigid_diesel",
    "load": 5000,
    "load_unit": "kg",
    "year": 2024,
    # Optional metadata
    "shipment_reference": "SHP-2024-00123",
    "order_number": "ORD-45678",
    "delivery_date": "2024-03-15",
    "driver_name": "John Doe",
    "notes": "Express delivery, fragile items"
}

Step 3: Bulk Upload Shipments

For high-volume operations, use CSV bulk upload:

Prepare CSV File

origin,destination,toc,load,load_unit,year,client_id,shipment_reference
"Madrid, Spain","Barcelona, Spain",rigid_diesel,5000,kg,2024,client-uuid-1,SHP-001
"Barcelona, Spain","Valencia, Spain",van_diesel,1200,kg,2024,client-uuid-1,SHP-002
"Madrid, Spain","Sevilla, Spain",articulated_diesel,12000,kg,2024,client-uuid-2,SHP-003
"Valencia, Spain","Bilbao, Spain",rigid_diesel,6500,kg,2024,client-uuid-2,SHP-004

Upload and Process

# Step 1: Get presigned upload URL
upload_response = requests.post(
    "https://api.dcycle.io/api/v1/logistics/bulk/csv",
    headers=headers
).json()

presigned_url = upload_response['upload_url']
status_url = upload_response['status_url']

# Step 2: Upload CSV to S3
with open('shipments_march_2024.csv', 'rb') as f:
    requests.put(presigned_url, data=f)

print("βœ… CSV uploaded, processing...")

# Step 3: Poll for completion
import time

while True:
    status = requests.get(status_url, headers=headers).json()

    if status['status'] == 'completed':
        print(f"βœ… Processed {status['records_processed']} shipments")
        print(f"   Total CO2e: {status['total_co2e']:.2f} kg")
        print(f"   Total distance: {status['total_distance_km']:.1f} km")
        break
    elif status['status'] == 'failed':
        print(f"❌ Upload failed: {status['error']}")
        break
    elif status['status'] == 'processing':
        progress = status.get('progress_percentage', 0)
        print(f"⏳ Processing... {progress}%")

    time.sleep(5)

CSV Format Requirements

Required fields:
  • origin: Address or city name
  • destination: Address or city name
  • toc: Transport Operation Category code
  • load: Cargo weight
  • load_unit: Unit (kg, ton, lb)
  • year: Calculation year
Optional fields:
  • client_id: Client UUID (from /api/v1/logistics/clients)
  • origin_country: ISO country code (ES, FR, etc.)
  • destination_country: ISO country code
  • shipment_reference: Your internal reference
  • order_number: Order/invoice number
  • delivery_date: YYYY-MM-DD format

Step 4: Query and Analyze

Get Client Report

Generate emissions report by client:
client_id = "client-uuid-here"

report = requests.get(
    f"https://api.dcycle.io/api/v1/logistics/clients/{client_id}/report",
    headers=headers,
    params={
        "start_date": "2024-01-01",
        "end_date": "2024-03-31",
        "page": 1,
        "size": 100
    }
).json()

print(f"πŸ“Š Client Report: {report['client_name']}")
print(f"   Period: {report['start_date']} to {report['end_date']}")
print(f"   Total shipments: {report['total_shipments']}")
print(f"   Total distance: {report['total_distance_km']:,.1f} km")
print(f"   Total CO2e: {report['total_co2e']:,.2f} kg")
print(f"   Average per shipment: {report['average_co2e_per_shipment']:.2f} kg")

List All Clients

clients = requests.get(
    "https://api.dcycle.io/api/v1/logistics/clients",
    headers=headers,
    params={"page": 1, "size": 50}
).json()

print("πŸ“‹ Logistics Clients:")
for client in clients['items']:
    print(f"   - {client['name']} ({client['client_code']})")
    print(f"     Shipments: {client['total_shipments']}")
    print(f"     Total CO2e: {client['total_co2e']:.2f} kg")

Generate Period Report

# Get organization-wide logistics report
report = requests.get(
    "https://api.dcycle.io/api/v1/logistics/report",
    headers=headers,
    params={
        "start_date": "2024-01-01",
        "end_date": "2024-12-31"
    }
).json()

print(f"πŸ“Š Annual Logistics Report 2024")
print(f"   Total shipments: {report['total_shipments']:,}")
print(f"   Total distance: {report['total_distance_km']:,.0f} km")
print(f"   Total CO2e: {report['total_co2e']:,.1f} kg ({report['total_co2e']/1000:.2f} tons)")
print(f"\nBreakdown by transport mode:")

for mode in report['by_transport_mode']:
    print(f"   - {mode['toc']}: {mode['co2e']:.1f} kg CO2e ({mode['percentage']:.1f}%)")

Real-World Example: Logistics Management System

Complete workflow for managing transport emissions:
import requests
import os
from datetime import date, datetime
import csv

class LogisticsManager:
    def __init__(self):
        self.headers = {
            "Authorization": f"Bearer {os.getenv('DCYCLE_API_KEY')}",
            "x-organization-id": os.getenv("DCYCLE_ORG_ID"),
            "x-user-id": os.getenv("DCYCLE_USER_ID")
        }
        self.base_url = "https://api.dcycle.io"

    def create_client(self, name, email, code):
        """Create new logistics client"""
        response = requests.post(
            f"{self.base_url}/api/v1/logistics/clients",
            headers=self.headers,
            json={
                "name": name,
                "contact_email": email,
                "client_code": code
            }
        )
        return response.json()

    def calculate_shipment(self, **kwargs):
        """Calculate single shipment emissions"""
        response = requests.post(
            f"{self.base_url}/api/v1/logistics/calculate",
            headers=self.headers,
            json=kwargs
        )
        return response.json()

    def bulk_upload_shipments(self, csv_file_path):
        """Upload bulk shipments from CSV"""
        # Get presigned URL
        upload_response = requests.post(
            f"{self.base_url}/api/v1/logistics/bulk/csv",
            headers=self.headers
        ).json()

        # Upload CSV
        with open(csv_file_path, 'rb') as f:
            requests.put(upload_response['upload_url'], data=f)

        return upload_response['status_url']

    def wait_for_upload(self, status_url):
        """Wait for bulk upload to complete"""
        import time

        while True:
            status = requests.get(status_url, headers=self.headers).json()

            if status['status'] == 'completed':
                return status
            elif status['status'] == 'failed':
                raise Exception(f"Upload failed: {status.get('error')}")

            time.sleep(5)

    def get_client_report(self, client_id, start_date, end_date):
        """Generate client emissions report"""
        response = requests.get(
            f"{self.base_url}/api/v1/logistics/clients/{client_id}/report",
            headers=self.headers,
            params={
                "start_date": start_date.isoformat(),
                "end_date": end_date.isoformat(),
                "page": 1,
                "size": 1000
            }
        )
        return response.json()

    def export_monthly_report(self, year, month, output_file):
        """Export monthly emissions to CSV"""
        from calendar import monthrange

        start_date = date(year, month, 1)
        last_day = monthrange(year, month)[1]
        end_date = date(year, month, last_day)

        # Get all clients
        clients_response = requests.get(
            f"{self.base_url}/api/v1/logistics/clients",
            headers=self.headers,
            params={"page": 1, "size": 100}
        ).json()

        # Generate report for each client
        with open(output_file, 'w', newline='') as f:
            writer = csv.writer(f)
            writer.writerow([
                'Client', 'Client Code', 'Shipments',
                'Distance (km)', 'CO2e (kg)', 'Avg CO2e/Shipment'
            ])

            for client in clients_response['items']:
                report = self.get_client_report(
                    client['id'],
                    start_date,
                    end_date
                )

                if report['total_shipments'] > 0:
                    writer.writerow([
                        client['name'],
                        client['client_code'],
                        report['total_shipments'],
                        f"{report['total_distance_km']:.1f}",
                        f"{report['total_co2e']:.2f}",
                        f"{report['average_co2e_per_shipment']:.2f}"
                    ])

# Usage Example
manager = LogisticsManager()

# 1. Create client
print("Creating client...")
client = manager.create_client(
    name="RetailCo Spain",
    email="[email protected]",
    code="RET-ES-001"
)
print(f"βœ… Client created: {client['id']}")

# 2. Calculate single shipment
print("\nCalculating shipment...")
shipment = manager.calculate_shipment(
    origin="Madrid, Spain",
    destination="Barcelona, Spain",
    client_id=client['id'],
    toc="rigid_diesel",
    load=5000,
    load_unit="kg",
    year=2024,
    shipment_reference="SHP-2024-00001"
)
print(f"βœ… Shipment: {shipment['distance_km']:.1f} km, {shipment['co2e']:.2f} kg CO2e")

# 3. Bulk upload
print("\nUploading bulk shipments...")
status_url = manager.bulk_upload_shipments("march_shipments.csv")
result = manager.wait_for_upload(status_url)
print(f"βœ… Processed {result['records_processed']} shipments")
print(f"   Total CO2e: {result['total_co2e']:,.2f} kg")

# 4. Generate monthly report
print("\nGenerating monthly report...")
manager.export_monthly_report(2024, 3, "march_2024_report.csv")
print("βœ… Report exported to march_2024_report.csv")

Best Practices

1. Choose Appropriate TOC

Select the most accurate transport category:
def select_toc(vehicle_type, weight_kg, fuel_type="diesel"):
    """Helper to select appropriate TOC"""

    if vehicle_type == "van":
        return f"van_{fuel_type}"
    elif vehicle_type == "truck":
        if weight_kg < 7500:
            return f"rigid_{fuel_type}"
        else:
            return f"articulated_{fuel_type}"
    elif vehicle_type == "rail":
        return "rail_freight"
    elif vehicle_type == "ship":
        return "maritime_container"
    elif vehicle_type == "plane":
        return "air_cargo"

    # Default
    return f"rigid_{fuel_type}"

2. Validate Addresses

Ensure accurate geocoding:
def validate_address(address):
    """Basic address validation"""

    # Check minimum length
    if len(address) < 5:
        return False, "Address too short"

    # Should contain city and country
    if ',' not in address:
        return False, "Include city and country (e.g., 'Madrid, Spain')"

    return True, "OK"

# Use before calculation
is_valid, message = validate_address(shipment['origin'])
if not is_valid:
    print(f"❌ Invalid origin: {message}")

3. Handle Errors Gracefully

def calculate_with_fallback(shipment_data):
    """Calculate with fallback to manual distance"""

    try:
        response = requests.post(url, headers=headers, json=shipment_data)
        response.raise_for_status()
        return response.json()

    except requests.exceptions.HTTPError as e:
        if e.response.status_code == 422:  # Validation error
            # Try with manual distance
            if 'manual_distance_km' in shipment_data:
                print("⚠️  Geocoding failed, using manual distance")
                return calculate_with_manual_distance(shipment_data)
        raise

4. Track Data Quality

Monitor calculation accuracy:
def check_distance_plausibility(origin, destination, calculated_km):
    """Sanity check calculated distance"""

    # Check for very short distances
    if calculated_km < 5:
        print(f"⚠️  Very short distance: {calculated_km} km")
        return False

    # Check for impossibly long distances
    if calculated_km > 20000:  # Half Earth circumference
        print(f"⚠️  Implausibly long distance: {calculated_km} km")
        return False

    return True

Troubleshooting

Issue: Zero Emissions Calculated

# Check response for errors
if response['co2e'] == 0:
    print("❌ Zero emissions calculated")
    print(f"   TOC: {response.get('toc')}")
    print(f"   Distance: {response.get('distance_km')} km")
    print(f"   Load: {response.get('load')} {response.get('load_unit')}")

    # Common causes:
    # 1. Invalid TOC code
    # 2. Zero load
    # 3. Zero distance (same origin/destination)

Issue: Geocoding Failed

# Provide manual distance as fallback
shipment = {
    "origin": "Warehouse A",
    "destination": "Customer Site B",
    "manual_distance_km": 450,  # Fallback distance
    "toc": "rigid_diesel",
    "load": 5000,
    "load_unit": "kg",
    "year": 2024
}

# Or use more specific addresses
shipment = {
    "origin_address": "Calle Example, 123",
    "origin_postal_code": "28001",
    "origin_city": "Madrid",
    "origin_country": "ES",
    # ...
}

Issue: Bulk Upload Fails

# Check CSV format
def validate_csv(file_path):
    """Validate CSV before upload"""

    required_fields = ['origin', 'destination', 'toc', 'load', 'load_unit', 'year']

    with open(file_path, 'r') as f:
        reader = csv.DictReader(f)
        headers = reader.fieldnames

        # Check required fields
        missing = [f for f in required_fields if f not in headers]
        if missing:
            print(f"❌ Missing required fields: {missing}")
            return False

        # Check data
        for i, row in enumerate(reader, start=2):
            # Check load is numeric
            try:
                float(row['load'])
            except ValueError:
                print(f"❌ Row {i}: Invalid load value '{row['load']}'")
                return False

        return True

# Validate before upload
if validate_csv('shipments.csv'):
    status_url = manager.bulk_upload_shipments('shipments.csv')

Next Steps