DHL Shipping Middleware for Picqer

This Laravel application serves as a middleware between Picqer and DHL Parcel DE Shipping API. It receives webhook calls from Picqer with shipment data, processes that data, creates shipments via the DHL API, and returns shipping labels and tracking information back to Picqer.

Features

  • Webhook integration with Picqer
  • DHL Parcel DE Shipping API integration
  • OAuth2 Password Grant (ROPC) authentication with DHL API
  • Automatic shipping label retrieval and storage (base64-decoded PDF)
  • Webhook signature verification
  • Sandbox and Production mode support
  • Automatic country code conversion (ISO alpha-2 to alpha-3)
  • Shipment tracking and status management
  • Database storage of all shipments and labels
  • Dark mode web interface with shipments overview and documentation

Web Interface

The middleware includes a professional dark mode web interface with three main sections:

  1. Home (/) - Dashboard with system status and quick links
  2. Shipments (/shipments) - View all shipments, download labels, track packages
  3. Documentation (/docs) - Complete setup guides and API references

Features

  • Real-time shipment tracking
  • One-click label downloads
  • Status filtering and pagination
  • System health monitoring
  • Complete API documentation
  • Mobile responsive design

Quick Start

  1. Start the development server:

    php artisan serve
    

    Then visit http://localhost:8000

  2. Or continue reading for setup instructions...

Requirements

  • PHP 8.2 or higher
  • Composer
  • SQLite (default) or MySQL/PostgreSQL
  • DHL Parcel DE API credentials
  • Picqer account with custom shipping method

Installation

  1. Install dependencies:

    composer install
    
  2. Configure environment: The .env file has already been created. Generate an application key if needed:

    php artisan key:generate
    
  3. Run migrations: Migrations have already been run. If you need to run them again:

    php artisan migrate
    
  4. Configure environment variables: Edit .env and add your credentials:

    # DHL Configuration
    DHL_MODE=sandbox  # or 'production'
    
    # Sandbox credentials (you need all 4!)
    # From developer.dhl.com:
    DHL_SANDBOX_CLIENT_ID=your_sandbox_client_id
    DHL_SANDBOX_CLIENT_SECRET=your_sandbox_client_secret
    # From DHL Business Portal (GKP):
    DHL_SANDBOX_USERNAME=your_gkp_username
    DHL_SANDBOX_PASSWORD=your_gkp_password
    
    # Production credentials (when ready - all 4 again)
    DHL_PRODUCTION_CLIENT_ID=your_production_client_id
    DHL_PRODUCTION_CLIENT_SECRET=your_production_client_secret
    DHL_PRODUCTION_USERNAME=your_production_gkp_username
    DHL_PRODUCTION_PASSWORD=your_production_gkp_password
    
    # DHL Shipper details
    # Sandbox: use 33333333330101
    # Production: use your actual billing number
    DHL_BILLING_NUMBER=33333333330101
    DHL_SHIPPER_NAME="Your Company Name"
    DHL_SHIPPER_STREET="Your Street"
    DHL_SHIPPER_HOUSE_NUMBER="123"
    DHL_SHIPPER_POSTAL_CODE="12345"
    DHL_SHIPPER_CITY="Your City"
    DHL_SHIPPER_COUNTRY=DE
    DHL_SHIPPER_EMAIL="[email protected]"
    DHL_SHIPPER_PHONE="+49301234567"
    
    # Label format is configured in config/dhl.php (default: A6 format)
    # Available formats: 910-300-410 (A6), 910-300-700 (A4), 910-300-710 (A4 4-up), etc.
    
    # Picqer Configuration
    PICQER_WEBHOOK_SECRET=your_webhook_secret
    PICQER_VERIFY_SIGNATURE=true
    

    Important: DHL authentication requires 4 credentials:

    • 2 from developer.dhl.com (API Key/Client ID + Client Secret)
    • 2 from your DHL Business Customer Portal/GKP (Username + Password)
  5. Start the development server:

    php artisan serve
    

API Endpoints

Picqer Webhook Endpoints

Get Configuration Options

  • URL: GET /api/webhook/picqer/config-options
  • Description: Returns available shipping configuration options for Picqer
  • Authentication: HTTP Basic Auth (optional)
  • Response:
    {
      "version": "1.0",
      "data": {
        "items": [
          {
            "key": "shipping_method",
            "label": "Shipping Method",
            "description": "Select the DHL shipping product",
            "type": "list",
            "possible_values": [
              {"value": "V01PAK", "label": "DHL Paket (Standard)"},
              {"value": "V01PRIO", "label": "DHL Paket (Priority)"},
              {"value": "V06PAK", "label": "DHL Paket International"},
              {"value": "V53WPAK", "label": "DHL Europaket"}
            ],
            "default_value": "V01PAK",
            "required": true
          }
        ]
      }
    }
    

Create Shipment

  • URL: POST /api/webhook/picqer/shipment
  • Description: Receives webhook from Picqer to create a shipment
  • Authentication: HTTP Basic Auth (optional) + Signature Verification
  • Headers:
    • X-Picqer-Signature: HMAC signature (if verification enabled)
  • Request Body: Picqer webhook payload (see Picqer Integration section)
  • Success Response (201):
    {
      "version": "1.0",
      "data": {
        "carrier_key": "dhl",
        "carrier_name": "DHL",
        "tracking_code": "00340434161094015902",
        "tracking_url": "https://www.dhl.de/de/privatkunden/pakete-empfangen/verfolgen.html?piececode=00340434161094015902",
        "label": {
          "format": "A6",
          "documents": {
            "pdf": "BASE64_ENCODED_PDF_DATA",
            "zpl": "BASE64_ENCODED_ZPL_DATA"
          }
        }
      }
    }
    
  • Error Response (4xx):
    {
      "version": "1.0",
      "error_messages": [
        "Recipient country not allowed for this product",
        "Invalid postal code format"
      ]
    }
    

Shipment Management Endpoints

List All Shipments

  • URL: GET /api/shipments
  • Description: Retrieve paginated list of all shipments
  • Query Parameters:
    • per_page (optional): Items per page (default: 20)
    • status (optional): Filter by status (pending, processing, created, failed, cancelled)
  • Response:
    {
      "success": true,
      "data": [...],
      "pagination": {
        "total": 50,
        "per_page": 20,
        "current_page": 1,
        "last_page": 3
      }
    }
    

Get Shipment Information

  • URL: GET /api/shipments/{shipmentId}
  • Description: Retrieve shipment details
  • Response:
    {
      "success": true,
      "data": {
        "id": 1,
        "status": "created",
        "tracking_number": "00340434161094015902",
        "dhl_shipment_number": "00340434161094015902",
        "tracking_url": "https://www.dhl.de/...",
        "label_url": "http://localhost:8000/api/shipments/1/label",
        "created_at": "2025-11-07T13:45:20.000000Z",
        "shipped_at": "2025-11-07T13:45:25.000000Z"
      }
    }
    

Get Shipping Label

  • URL: GET /api/shipments/{shipmentId}/label
  • Description: Download the shipping label (PDF or ZPL)
  • Response: Binary PDF or ZPL file

Health Check

  • URL: GET /api/health
  • Description: Check API status and configuration
  • Response:
    {
      "status": "ok",
      "timestamp": "2025-11-07T13:45:20+00:00",
      "mode": "sandbox"
    }
    

Picqer Integration

Setting Up External Shipping Provider

This middleware is fully compliant with Picqer's External Shipping Provider Integration specification.

In Picqer:

  1. Go to Settings → Shipping Methods
  2. Create a new External Shipping Provider (or Custom Shipping Method)
  3. Configure the endpoints:
    • Configuration URL: https://your-domain.com/api/webhook/picqer/config-options (optional)
    • Shipment Creation URL: https://your-domain.com/api/webhook/picqer/shipment
  4. Choose Authentication Method:
    • HTTP Basic Auth (optional - username/password)
    • Or rely on webhook signature verification
  5. Add the webhook secret in your .env: PICQER_WEBHOOK_SECRET=your_secret_here

Picqer Webhook Payload Format

Picqer sends webhooks in the following structure (compliant with their API specification):

{
  "version": "1.0",
  "data": {
    "profile": [
      {
        "key": "shipping_method",
        "value": "V01PAK"
      },
      {
        "key": "notification_email",
        "value": 1
      }
    ],
    "shipment": {
      "weight": 1500,
      "total_paid": 99.99,
      "value_of_goods": 79.99,
      "recipient": {
        "name": "John Doe",
        "contactname": "Jane Doe",
        "address": "Hauptstraße",
        "address2": "123",
        "zipcode": "10115",
        "city": "Berlin",
        "region": "Berlin",
        "country": "DE",
        "emailaddress": "[email protected]",
        "telephone": "+49123456789"
      },
      "references": {
        "reference": "Order #12345",
        "idorder": 12345,
        "idreturn": null,
        "idpicklist": 67890,
        "orderid": "O2025-12345",
        "returnid": null,
        "picklistid": "P2025-67890"
      },
      "preferred_delivery_date": "2025-11-15",
      "pickup_point_data": [],
      "language": "en",
      "packaging": {
        "idpackaging": 123,
        "length": 30,
        "width": 20,
        "height": 15
      }
    }
  }
}

Customs Data Support

For shipments outside the EU, Picqer includes customs_data in the webhook:

{
  "version": "1.0",
  "data": {
    "shipment": {
      "customs_data": {
        "products": [
          {
            "description": "Winter Scarf",
            "amount": 2,
            "weight": 350,
            "value": 29.99,
            "hs_code": "288392",
            "country_of_origin": "CN"
          }
        ]
      }
    }
  }
}

The middleware automatically:

  • Detects customs data presence
  • Transforms it to DHL's customs format
  • Returns A4 format labels (including CN23/commercial invoice)
  • For EU shipments: returns A6 format labels

Configuration Options

The middleware provides these configurable options in Picqer (via /config-options endpoint):

Option Type Description Default
shipping_method List DHL product (V01PAK, V01PRIO, V06PAK, V53WPAK) V01PAK
notification_email List Send tracking email to recipient Yes
reference_note Text Optional note on label null

Response Format

The middleware returns Picqer-compliant responses:

Success Response:

{
  "version": "1.0",
  "data": {
    "carrier_key": "dhl",
    "carrier_name": "DHL",
    "tracking_code": "00340434161094015902",
    "tracking_url": "https://www.dhl.de/...",
    "label": {
      "format": "A6",
      "documents": {
        "pdf": "BASE64_ENCODED_PDF"
      }
    }
  }
}

Error Response:

{
  "version": "1.0",
  "error_messages": [
    "Recipient country not allowed for this product",
    "Invalid postal code format"
  ]
}

Important Notes

  • References: Uses Picqer's custom reference text or order ID for DHL labels. Picklist information is stored but NOT sent to DHL. Reference text is sent to refNo field which appears on the label
  • Country Codes: Accepts both 2-letter (DE) and 3-letter (DEU) ISO codes, automatically converts to DHL's required 3-letter format
  • Weight: Should be in grams
  • Dimensions: Should be in centimeters (automatically converted to millimeters for DHL)
  • Processing Time: Responses within 20 seconds (Picqer requirement)
  • Label Format: A6 for standard shipments, A4 for customs shipments
  • Carrier Key: Returns "dhl" for marketplace/webshop integrations
  • Profile Data: Profile configuration from Picqer is used internally but NOT passed to DHL API

Testing the Integration

Test the configuration endpoint:

curl http://localhost:8000/api/webhook/picqer/config-options

Test shipment creation with Picqer format:

curl -X POST http://localhost:8000/api/webhook/picqer/shipment \
  -H "Content-Type: application/json" \
  -d '{
    "version": "1.0",
    "data": {
      "profile": [
        {"key": "shipping_method", "value": "V01PAK"}
      ],
      "shipment": {
        "weight": 1500,
        "recipient": {
          "name": "Test Recipient",
          "address": "Teststraße",
          "address2": "10",
          "zipcode": "10115",
          "city": "Berlin",
          "country": "DE",
          "emailaddress": "[email protected]"
        },
        "references": {
          "reference": "TEST-001",
          "orderid": "O2025-TEST"
        },
        "packaging": {
          "length": 30,
          "width": 20,
          "height": 15
        }
      }
    }
  }'

DHL API Integration

Authentication

The middleware uses OAuth2 Password Grant (ROPC) authentication with the DHL API:

  • Requires 4 credentials: client_id, client_secret, username (GKP), password (GKP)
  • Tokens are cached for 30 minutes (1800 seconds)
  • Automatic token refresh on expiration
  • Separate credentials for sandbox and production
  • Test connection: php artisan dhl:test-connection

API URLs

Sandbox:

  • Auth: https://api-sandbox.dhl.com/parcel/de/account/auth/ropc/v1/token
  • API: https://api-sandbox.dhl.com/parcel/de/shipping/v2

Production:

  • Auth: https://api-eu.dhl.com/parcel/de/account/auth/ropc/v1/token
  • API: https://api-eu.dhl.com/parcel/de/shipping/v2

Supported DHL Products

  • V01PAK (DHL Paket) - Default
  • Additional products can be configured via the product_code field

Label Formats

The label format can be configured in config/dhl.php. Supported formats:

Format Code Description Use Case
910-300-410 A6 format (105x148mm) Default - Standard shipping labels
910-300-700 A4 format, 1 label per page Single label printing
910-300-710 A4 format, 4 labels per page Batch printing (4-up)
910-300-600 105x205mm format Extended shipping label
910-300-300 103x199mm format Alternative label size
910-300-700-oZ A4 without additional docs Clean A4 format

To change the label format, edit config/dhl.php:

'label' => [
    'format' => '910-300-700', // Change to desired format
],

API Documentation

Environment Modes

Sandbox Mode

Set DHL_MODE=sandbox in .env to use the DHL sandbox environment for testing.

Test your connection:

php artisan dhl:test-connection

Sandbox API URLs:

  • Auth: https://api-sandbox.dhl.com/parcel/de/account/auth/ropc/v1/token
  • API: https://api-sandbox.dhl.com/parcel/de/shipping/v2

Production Mode

Set DHL_MODE=production in .env to use the live DHL API.

Production API URLs:

  • Auth: https://api-eu.dhl.com/parcel/de/account/auth/ropc/v1/token
  • API: https://api-eu.dhl.com/parcel/de/shipping/v2

Database Schema

Shipments Table

Stores all shipment information:

  • id - Primary key
  • picqer_order_id - Reference to Picqer order
  • picqer_reference - Order reference
  • picqer_data - Original Picqer webhook data (JSON)
  • dhl_shipment_number - DHL shipment number
  • dhl_tracking_number - DHL tracking number
  • dhl_product - DHL product code
  • dhl_request - DHL API request payload (JSON)
  • dhl_response - DHL API response (JSON)
  • label_url - URL to label (if provided)
  • label_data - Binary label data
  • label_format - Label format (PDF, ZPL)
  • status - Shipment status (pending, processing, created, failed, cancelled)
  • error_message - Error message if failed
  • mode - Environment mode (sandbox/production)
  • shipped_at - Timestamp when shipped
  • created_at, updated_at, deleted_at

Logging

All operations are logged in storage/logs/laravel.log:

  • Webhook receipts
  • DHL API calls
  • Token generation
  • Shipment creation
  • Errors and exceptions

Security

  • Webhook signature verification (HMAC-SHA256)
  • Environment-based configuration
  • Secure credential storage
  • API request logging
  • Error masking in production

Testing

Test DHL Connection

Run the test command to verify your DHL credentials:

php artisan dhl:test-connection

Expected output:

Testing DHL API Connection...
================================

✓ Successfully obtained access token
✓ Token expires in: 1799 seconds

Connection test successful!

Test API Endpoints

Health Check:

curl http://localhost:8000/api/health

Configuration Options (Picqer):

curl http://localhost:8000/api/webhook/picqer/config-options

Create Shipment (Picqer Format):

curl -X POST http://localhost:8000/api/webhook/picqer/shipment \
  -H "Content-Type: application/json" \
  -d '{
    "version": "1.0",
    "data": {
      "profile": [
        {"key": "shipping_method", "value": "V01PAK"}
      ],
      "shipment": {
        "weight": 1500,
        "recipient": {
          "name": "Test Recipient",
          "address": "Teststraße",
          "address2": "10",
          "zipcode": "10115",
          "city": "Berlin",
          "country": "DE",
          "emailaddress": "[email protected]"
        },
        "references": {
          "reference": "TEST-001",
          "orderid": "O2025-TEST"
        },
        "packaging": {
          "length": 30,
          "width": 20,
          "height": 15
        }
      }
    }
  }'

List Shipments:

curl http://localhost:8000/api/shipments

Download Label:

# Get the shipment ID from the response, then:
curl http://localhost:8000/api/shipments/{shipment_id}/label > label.pdf

Troubleshooting

Common Issues

  1. JWT Token Errors

    • Verify all 4 credentials in .env:
      • Client ID and Secret (from developer.dhl.com)
      • Username and Password (from GKP/Business Portal)
    • Check if credentials are for correct environment (sandbox/production)
    • Run php artisan dhl:test-connection for detailed diagnostics
    • Clear cached token: php artisan cache:clear
  2. Webhook Signature Verification Fails

    • Ensure PICQER_WEBHOOK_SECRET matches Picqer configuration
    • Set PICQER_VERIFY_SIGNATURE=false for testing (not recommended for production)
  3. Billing Number Not Available

    • Sandbox: Use DHL_BILLING_NUMBER=33333333330101
    • Production: Use your actual DHL billing number
    • Verify the number matches your environment mode
  4. Label Not Available

    • Labels are automatically extracted from DHL's label.b64 field
    • Stored as base64-decoded PDF in database
    • Check dhl_response field in database for raw API response
    • Verify shipper details are complete in .env
  5. Country Code Errors

    • Middleware automatically converts 2-letter to 3-letter codes
    • Supported: All major European countries (DE→DEU, NL→NLD, etc.)
    • If you see "must be three characters", check the country mapping in DHLShippingService.php

Project Structure

├── app/
│   ├── Http/Controllers/Api/
│   │   └── ShippingWebhookController.php  # Main webhook controller
│   ├── Models/
│   │   └── Shipment.php                   # Shipment model
│   └── Services/DHL/
│       ├── DHLAuthService.php             # DHL authentication
│       └── DHLShippingService.php         # DHL shipment creation
├── config/
│   ├── dhl.php                            # DHL configuration
│   └── picqer.php                         # Picqer configuration
├── database/migrations/
│   └── *_create_shipments_table.php       # Shipments table migration
└── routes/
    └── api.php                            # API routes

License

This project is proprietary software.

Support

For issues or questions:

  • Check logs in storage/logs/laravel.log
  • Review DHL API documentation
  • Review Picqer API documentation