Skip to content

E-Commerce — Shipping Provider Integrations

OWNER: aimee (scoping), urszula, maxim (backend implementation) ALSO_USED_BY: eric (invoicing link), floris, floor (checkout UI), ALL dev agents LAST_VERIFIED: 2026-03-26 SCOPE: carrier APIs, shipping aggregators, label generation, tracking, returns


Overview

Shipping is the last mile that determines customer satisfaction. This page covers the carrier APIs relevant to GE's EU SME clients, the shipping aggregators that simplify multi-carrier integration, and the implementation patterns for building provider-agnostic shipping into every e-commerce project.

GE_RECOMMENDATION: use Sendcloud as the primary shipping aggregator for all new projects. Direct carrier integration is only justified when a client has specific contractual requirements or volume-based pricing that aggregators cannot match.

Cross-reference with Address Standards for address formatting requirements and Product Standards for HS codes needed on customs declarations.


PostNL — Netherlands Primary Carrier

Company Overview

PostNL is the national postal service of the Netherlands and the default carrier for domestic Dutch shipments. Every Dutch e-commerce project will use PostNL either directly or via an aggregator.

API Overview

DEVELOPER_PORTAL: developer.postnl.nl API_DOCS: docs.api.postnl.nl AUTH: API key (provided with business account) FORMAT: REST / JSON SANDBOX: available via developer portal

LATEST_API: Shipment API v4 (released May 2025) — combines labelling, confirming, barcode, and easy return APIs into a single unified endpoint.

NOTE (Feb 2026): PostNL migrated API key management to a new portal. Existing API keys are no longer visible in the portal — store them securely. New keys can be requested through the updated portal.

Key APIs

API Purpose Endpoint Base
Shipping API v4 Create shipment + generate label in one call /v4/shipment
Labelling API Generate shipping labels (PDF or ZPL) /v2/label
Confirming API Confirm shipments for collection /v2/confirm
Barcode API Generate barcodes for shipments /v1/barcode
Track & Trace API Shipment tracking status and events /v3/track
Delivery Date API Calculate expected delivery date /v2/deliverydate
Locations API Find PostNL pickup points (Pakketpunten) /v2/locations
Checkout API Delivery options for checkout /v1/checkout

Label Generation Workflow

1. (Optional) Reserve barcode via Barcode API
2. Create shipment via Shipping API v4 → returns label PDF + barcode
3. Print label (A6 format, 105x148mm)
4. Confirm shipment (or auto-confirm on creation)
5. Hand over to PostNL (drop-off or pickup)

NOTE: as of 2025, you can create a label without pre-reserving a barcode. PostNL generates the barcode automatically and returns it in the API response.

Product Codes

PostNL uses numeric product codes to define the shipment type and service level.

COMMON_CODES: - 3085 — Standard domestic parcel - 3385 — Delivery to PostNL pickup point (Pakketpunt) - 3090 — Delivery to neighbour allowed - 3189 — Signature on delivery - 2928 — Letterbox package (brievenbuspakje) - 4945 — EU parcel (Globalpack) - 4947 — International parcel (non-EU)

NOTE (Feb 2026): PostNL simplified international product codes. Many contract-specific codes were phased out. Check the changelog for current valid codes.

Letterbox Packages (Brievenbuspakje)

DIMENSIONS: max 38 x 26.5 x 3.2 cm, max 2 kg PRODUCT_CODE: 2928 USE_CASE: small items (books, phone cases, cosmetics) — cheaper than standard parcel, no missed delivery (fits through letterbox)

Return Shipments

PostNL supports return labels via the Easy Return service. Returns can be generated at shipment creation time or on-demand when the customer requests a return.

RETURN_FLOW: 1. Customer requests return via shop's returns portal 2. System generates return label via PostNL API 3. Customer prints label and drops package at PostNL point 4. Track & Trace API provides return tracking events

Webhooks

PostNL does not provide native webhook support for tracking events. Tracking must be polled via the Track & Trace API.

WORKAROUND: use a cron job polling every 15-30 minutes for active shipments, or use Sendcloud/MyParcel which normalise tracking into webhooks.

Rate Limiting

PostNL API rate limits are not publicly documented in detail. The Shipping API handles bulk label generation — up to 200 shipments per request.

Common Gotchas

GOTCHA: product codes differ between sandbox and production — always verify against current documentation after switching environments. GOTCHA: international shipments to the US require 10-digit HS codes and complete customs data since February 2026. Missing data = rejected shipment. GOTCHA: PO box addresses are no longer supported for Delivery Code shipments (Feb 2026).


DHL — Pan-European Carrier

Company Overview

DHL operates multiple divisions relevant to e-commerce: DHL Parcel (domestic/EU), DHL Express (international/time-sensitive), and DHL eCommerce (cross-border EU). For GE projects, DHL Parcel and DHL eCommerce Europe are the most relevant.

API Overview

DEVELOPER_PORTAL: developer.dhl.com API_CATALOG: developer.dhl.com/api-catalog AUTH: OAuth2 (new, recommended) or Basic Auth (legacy, being phased out) FORMAT: REST / JSON SANDBOX: available for all APIs

MIGRATION_DEADLINE: DHL is phasing out Basic Auth. All new integrations must use OAuth2. Existing integrations using legacy software (JTL, Gambio, etc.) must migrate by end of May 2026.

Key APIs

DHL eCommerce Europe — eConnect API

PURPOSE: cross-border EU parcel shipping via DHL Parcel Connect BASE_URL: varies by contract entity AUTH: API credentials from DHL business account PRODUCTS: DHL Parcel Connect, DHL Parcel Return Connect, DHL Parcel Connect PLUS, DHL Parcel International, DHL Parcel Return International

SERVICES: - Create shipment + generate label - Track & Trace (end-to-end) - Parcel shop finder - Return label generation

REQUIREMENT: shipping contract with a DHL eCommerce legal entity

DHL Parcel EU (BE/LU/NL)

PURPOSE: domestic and Benelux parcel shipping API_DOCS: api-gw.dhlparcel.nl/docs/guide AUTH: API credentials + token-based REQUIREMENT: shipping contract with DHL Parcel Benelux

SERVICES: - Capabilities check (available services for a shipment) - Label creation (PDF, ZPL) - Pickup request scheduling - Time window delivery - Track & Trace

DHL Parcel DE Shipping (Germany) v2

PURPOSE: domestic German parcel shipping BASE_URL: via developer.dhl.com AUTH: OAuth2 (recommended) or Basic Auth (deprecated)

KEY_OPERATIONS: - validateShipment — validate shipment data before creating - createShipmentOrder — create shipment with documents - updateShipmentOrder — modify shipping documents - deleteShipmentOrder — cancel shipments - getLabel — retrieve shipping label - getExportDoc — retrieve export/customs documents

Shipment Tracking — Unified API

PURPOSE: single API for tracking across all DHL divisions BASE_URL: https://api-eu.dhl.com/track/shipments AUTH: API key in request header (DHL-API-Key) COVERAGE: DHL Group, DHL Freight, DHL eCommerce, Post & Parcel Germany

// DHL Unified Tracking example
const response = await fetch(
  `https://api-eu.dhl.com/track/shipments?trackingNumber=${trackingNumber}`,
  { headers: { 'DHL-API-Key': process.env.DHL_API_KEY } }
);
const { shipments } = await response.json();

Location Finder API

PURPOSE: find DHL ServicePoints, Packstations, Paketshops USE_IN: checkout flow — let customers choose pickup location

EU Customs Declarations

For non-EU shipments, DHL requires customs declarations (CN23 form). The API generates these automatically when customs data is provided:

REQUIRED_FIELDS: - HS code (6-10 digits) — see Product Standards - Item description - Quantity and weight per item - Value and currency - Country of origin

Common Gotchas

GOTCHA: DHL has separate APIs per division (Parcel NL, Parcel DE, eCommerce, Express) — each requires its own contract and credentials. There is no single unified shipping API. GOTCHA: OAuth2 tokens expire — implement token refresh logic, not per-request login. GOTCHA: the Parcel EU (NL) API requires tracker code + receiver postcode for tracking, not just the tracking number alone.


DPD — Pan-European Carrier

Company Overview

DPD (Dynamic Parcel Distribution) operates across 30+ European countries with a strong presence in DE, FR, NL, BE, and the UK. Known for the Predict service (delivery time window notifications) and extensive ParcelShop network.

API Overview

DEVELOPER_PORTAL: dpd.com/developers AUTH: JWT token (Bearer scheme) FORMAT: REST / JSON RATE_LIMIT: 60 shipment calls per minute

Authentication

1. Obtain depot number, customer number, and API credentials from DPD
2. Request JWT token via login endpoint
3. Include JWT in Authorization header for all subsequent calls
4. IMPORTANT: do NOT call login before every API request — reuse token until expiry

Key APIs

API Purpose
Shipping API v1.1 Create shipments, generate labels
ParcelShop Finder Find DPD Pickup points
Tracking Shipment status and events
Returns Return label generation

DPD Predict Service

Predict sends the recipient an email/SMS with a 1-hour delivery time window on the morning of delivery. This significantly reduces missed deliveries.

REQUIREMENT: email address and/or mobile number must be provided with the shipment. NOTE: DPD charges a surcharge for failed delivery attempts if Predict is not used.

SETUP: email template for Predict must be configured with DPD in advance.

ParcelShop Network

DPD operates ParcelShops across Europe for pickup and drop-off.

INTEGRATION: - Download ParcelShop list (XML or CSV) — update daily - Or use ParcelShop Finder API for real-time lookup - Filter by: location, cash-on-delivery support, opening hours

Label Generation

Average label generation time: ~1 second per label. Format: PDF (A6) or ZPL for thermal printers.

Webhooks

DPD supports webhook notifications for tracking events via their Cloud platform. Configure webhook URLs in the DPD portal.

Common Gotchas

GOTCHA: DPD API credentials are depot-specific — a client with multiple depots needs separate credentials per depot. GOTCHA: the login endpoint must NOT be called per-request. DPD will throttle or block accounts that call login excessively. GOTCHA: ParcelShop availability changes frequently — cache with daily refresh, not weekly.


UPS — International Carrier

Company Overview

UPS operates globally with strong EU presence. Relevant for international shipments, B2B logistics, and clients needing premium delivery services. UPS operates from EU legal entities for GDPR compliance.

API Overview

DEVELOPER_PORTAL: developer.ups.com GITHUB: github.com/UPS-API (SDKs and MCP server) AUTH: OAuth 2.0 client credentials flow (mandatory since 2025) FORMAT: REST / JSON SANDBOX: available via developer portal

NOTE: UPS completed migration from legacy XML APIs to OAuth2 REST APIs in 2024-2025. All new integrations must use the REST APIs. Legacy access keys are no longer supported.

Key APIs

API Purpose
Shipping API Create shipments, generate labels, void shipments
Tracking API Real-time tracking with webhook support
Rating API Rate calculation and service comparison
Address Validation Validate and normalise addresses
Locations API Find UPS Access Points (pickup/dropoff)
Time in Transit Estimated delivery dates

OAuth2 Authentication

// UPS OAuth2 token acquisition
const tokenResponse = await fetch('https://onlinetools.ups.com/security/v1/oauth/token', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/x-www-form-urlencoded',
    'Authorization': `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString('base64')}`,
  },
  body: 'grant_type=client_credentials',
});
const { access_token, expires_in } = await tokenResponse.json();

Access Point Network

UPS Access Points are pickup/drop-off locations (shops, lockers) similar to DHL ServicePoints and DPD ParcelShops. Use the Locations API to integrate into checkout.

Trade Documentation

For non-EU shipments, UPS handles customs documentation including commercial invoices, certificates of origin, and SED (Shipper's Export Declaration).

REQUIRED: HS codes, declared value, country of origin. See Product Standards.

Common Gotchas

GOTCHA: UPS API response times can be slow (2-5 seconds for rating) — cache rates where possible and show loading states in checkout. GOTCHA: OAuth2 tokens have limited lifetime — implement automatic refresh. GOTCHA: sandbox and production use different base URLs and credentials.


Why Aggregators Over Direct Integration

REASON_1: single API for all carriers — one integration instead of 4-6 separate ones REASON_2: rate comparison across carriers at checkout REASON_3: normalised tracking events regardless of carrier REASON_4: built-in returns portal and branded tracking pages REASON_5: carrier updates handled by the aggregator, not your codebase REASON_6: pre-negotiated shipping rates (often cheaper than direct for SME volumes)

COST_OF_DIRECT: each carrier API has different auth, different data models, different label formats, different tracking event schemas, and different error codes. Maintaining 4 carrier integrations is 4x the development and 4x the maintenance burden.

GE_POLICY: always start with an aggregator. Only add direct carrier integration when a client outgrows aggregator pricing or has carrier-specific contractual requirements.


Sendcloud (NL) — PRIMARY RECOMMENDATION

Company Overview

Sendcloud is a Dutch shipping platform (Eindhoven) serving 25,000+ online retailers across Europe. It is GE's recommended shipping aggregator for all new e-commerce projects.

WEBSITE: sendcloud.com DEVELOPER_PORTAL: sendcloud.dev HELP_CENTER: support.sendcloud.com

Why Sendcloud for GE Projects

  • NL-headquartered — EU data processing, Dutch support, GDPR-compliant
  • 160+ carriers including PostNL, DHL, DPD, UPS, GLS, Budbee, Fietskoeriers
  • Full shipping from 8 EU countries: NL, BE, DE, FR, AT, ES, IT, UK
  • Pre-built checkout widget for delivery options
  • Returns portal with branded tracking
  • Shipping rules engine (auto-select carrier based on weight/destination/value)

API Version

USE: API v3 (strongly recommended over legacy API) V3_FEATURES: multicollo with dimensions, paperless trade, native ZPL labels, smarter delivery handling

Key APIs

API Purpose Docs
Ship an Order One-call shipment creation + label sendcloud.dev/docs/shipping
Shipments Full shipment lifecycle management sendcloud.dev/docs/shipping
Service Points Pickup point finder across all carriers sendcloud.dev/docs/shipping
Dynamic Checkout Delivery options (same-day, next-day, nominated) sendcloud.dev/docs/shipping
Returns Standalone return label creation sendcloud.dev/docs/shipping
Pick-ups Schedule carrier pickup at warehouse sendcloud.dev/docs/shipping
Shipping Methods Available shipping methods per zone sendcloud.dev/docs/shipping

Authentication

AUTH: HTTP Basic (API key + API secret)
HEADER: Authorization: Basic base64({api_key}:{api_secret})

API credentials are generated in the Sendcloud panel under Settings > Integrations.

Create Shipment + Label

// Sendcloud v3: create parcel and generate label in one call
const response = await fetch('https://panel.sendcloud.sc/api/v3/parcels', {
  method: 'POST',
  headers: {
    'Authorization': `Basic ${Buffer.from(`${apiKey}:${apiSecret}`).toString('base64')}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    parcel: {
      name: 'Jan de Vries',
      address: 'Kalverstraat',
      house_number: '123',
      city: 'Amsterdam',
      postal_code: '1012PA',
      country: 'NL',
      weight: 1.5,                    // kg
      order_number: 'ORD-2026-001',
      shipment: { id: 8 },           // shipping method ID
      request_label: true,            // generate label immediately
      apply_shipping_rules: true,     // use configured shipping rules
    },
  }),
});
const { parcel } = await response.json();
// parcel.label.normal_printer[0] → PDF label URL
// parcel.tracking_number → carrier tracking number
// parcel.tracking_url → branded tracking page URL

Webhooks

Sendcloud provides webhooks for tracking events, parcel status changes, and return updates.

EVENTS: - parcel-status-changed — tracking status update - parcel-created — new parcel registered - parcel-returned — return received

Configure webhook URL in Sendcloud panel under Settings > Integrations > Webhooks.

Checkout Widget

Sendcloud offers a pre-built JavaScript checkout widget that shows available delivery options (standard, express, same-day, pickup point) based on the destination address.

INTEGRATION: configure shipping methods per delivery zone in Sendcloud panel, then use the Dynamic Checkout API to retrieve and display options in your checkout.

Testing

USE: "Unstamped letter" shipping method for testing. This creates parcels and labels without incurring charges.

NO sandbox environment — testing is done on the production API with test shipping methods.

Pricing

Sendcloud charges per label. Exact pricing depends on the plan (Essential, Small Shop, Large Shop, Business, Enterprise). Pre-negotiated carrier rates are included.

ADVISE_CLIENTS: Sendcloud pricing is typically competitive with or cheaper than direct PostNL/DHL rates for volumes under 10,000 parcels/month.

Common Gotchas

GOTCHA: Sendcloud has no separate sandbox environment. Use test shipping methods to avoid charges during development. GOTCHA: shipping method IDs are account-specific. Do not hardcode them — fetch available methods via the API and store in configuration. GOTCHA: apply_shipping_rules: true requires rules to be configured in the panel first. Without rules, set to false and specify the shipping method explicitly.


MyParcel (NL) — Alternative Aggregator

Company Overview

MyParcel is a Dutch shipping platform focused primarily on PostNL and DHL integration. Simpler than Sendcloud, with a focus on NL-domestic shipping.

WEBSITE: myparcel.nl API_DOCS: developer.myparcel.nl LEGACY_DOCS: myparcelnl.github.io/api

When to Use MyParcel Instead of Sendcloud

USE_MYPARCEL_WHEN: - Client ships primarily via PostNL with occasional DHL - Simpler integration is preferred (fewer features, less complexity) - Client already has a MyParcel account and workflow

USE_SENDCLOUD_WHEN: - Multi-carrier requirements (3+ carriers) - Cross-border EU shipping is significant - Client needs branded tracking and returns portal - Checkout delivery options widget is desired

Supported Carriers

  • PostNL (primary)
  • DHL (domestic NL and parcel)
  • DPD
  • Bpost (Belgium, via SendMyParcel.be)
  • GLS

Key Features

Feature Support
Label generation PDF and ZPL
Letterbox packages Yes (PostNL, NL only)
Digital stamps Yes (PostNL, NL only)
Returns PostNL only
Pickup points PostNL Pakketpunten
Same-day delivery DHL Today (16:30-21:30)
Large format PostNL (100x70x58 to 175x78x58 cm or >23 kg)

Authentication

AUTH: API key in header
HEADER: Authorization: bearer {api_key}

Important Limitations

LIMITATION: MyParcel does not have a test environment. All API calls are against production. LIMITATION: return shipments only available for PostNL (NL) and Bpost (BE). LIMITATION: international mailbox packages require a PostNL contract. LIMITATION: large format shipments must be flagged — PostNL will retroactively charge if scanned dimensions exceed standard limits.


Implementation Patterns

Abstract Shipping Provider Interface

Design a provider-agnostic shipping interface so the carrier or aggregator can be swapped without rewriting the checkout and fulfilment flows.

// Provider-agnostic shipping interface
interface ShippingProvider {
  // Rate calculation
  getRates(params: RateRequest): Promise<ShippingRate[]>;

  // Shipment lifecycle
  createShipment(params: ShipmentRequest): Promise<Shipment>;
  getLabel(shipmentId: string): Promise<LabelResponse>;
  cancelShipment(shipmentId: string): Promise<void>;

  // Tracking
  getTracking(trackingNumber: string): Promise<TrackingEvent[]>;

  // Locations
  getPickupPoints(params: LocationQuery): Promise<PickupPoint[]>;

  // Returns
  createReturnLabel(params: ReturnRequest): Promise<ReturnLabel>;
}

interface ShippingRate {
  carrierId: string;
  carrierName: string;           // 'PostNL', 'DHL', etc.
  serviceName: string;           // 'Standard', 'Express', 'Pickup Point'
  price: number;                 // in cents (EUR)
  currency: string;
  estimatedDeliveryDays: { min: number; max: number };
  pickupPointRequired: boolean;
}

interface Shipment {
  id: string;
  trackingNumber: string;
  trackingUrl: string;
  labelUrl: string;
  carrier: string;
  status: ShipmentStatus;
}

type ShipmentStatus =
  | 'created'
  | 'label_printed'
  | 'handed_to_carrier'
  | 'in_transit'
  | 'out_for_delivery'
  | 'delivered'
  | 'delivery_failed'
  | 'returned_to_sender';

Rate Calculation at Checkout

// Checkout shipping step
async function getShippingOptions(
  address: Address,
  cartItems: CartItem[],
  provider: ShippingProvider,
): Promise<ShippingRate[]> {
  const totalWeight = cartItems.reduce((sum, item) => sum + item.weight * item.quantity, 0);
  const dimensions = calculatePackageDimensions(cartItems);

  const rates = await provider.getRates({
    destination: {
      countryCode: address.countryCode,
      postcode: address.postcode,
      city: address.city,
    },
    weight: totalWeight,
    dimensions,
    value: cartItems.reduce((sum, item) => sum + item.price * item.quantity, 0),
  });

  // Sort by price, cheapest first
  return rates.sort((a, b) => a.price - b.price);
}

Label Generation Workflow

┌──────────────┐     ┌───────────────┐     ┌──────────────┐
│ Order Placed │ ──→ │ Pick & Pack   │ ──→ │ Generate     │
│              │     │ (warehouse)   │     │ Label        │
└──────────────┘     └───────────────┘     └──────┬───────┘
                                           ┌───────▼───────┐
                                           │ Print Label   │
                                           │ (A6 / ZPL)    │
                                           └───────┬───────┘
                                           ┌───────▼───────┐
                                           │ Confirm +     │
                                           │ Hand to       │
                                           │ Carrier       │
                                           └───────┬───────┘
                                           ┌───────▼───────┐
                                           │ Track via     │
                                           │ Webhook/Poll  │
                                           └───────────────┘

TIMING: generate labels during pick-and-pack, not at order placement. Carrier pickup schedules and daily cutoff times determine when labels should be generated.

BATCH_LABELS: for high-volume shops, generate labels in batch (end of day) and combine into a single PDF for printing.

Tracking Webhook Handler

// Generic tracking webhook handler (works with Sendcloud, can adapt for others)
async function handleTrackingWebhook(payload: WebhookPayload) {
  const { tracking_number, status, timestamp, message } = parseWebhookPayload(payload);

  // Update order status in database
  await db.update(orders)
    .set({
      shippingStatus: mapCarrierStatus(status),
      lastTrackingUpdate: new Date(timestamp),
      trackingHistory: sql`tracking_history || ${JSON.stringify({
        status, timestamp, message,
      })}::jsonb`,
    })
    .where(eq(orders.trackingNumber, tracking_number));

  // Notify customer (email/push) on key events
  if (['out_for_delivery', 'delivered', 'delivery_failed'].includes(status)) {
    await notifyCustomer(tracking_number, status, message);
  }
}

Returns Flow

┌──────────────┐     ┌───────────────┐     ┌──────────────┐
│ Customer     │ ──→ │ Returns       │ ──→ │ Generate     │
│ Requests     │     │ Portal        │     │ Return Label │
│ Return       │     │ (reason,      │     │ (via API)    │
│              │     │  photos)      │     │              │
└──────────────┘     └───────────────┘     └──────┬───────┘
                                           ┌───────▼───────┐
                                           │ Customer      │
                                           │ Prints Label  │
                                           │ + Drops Off   │
                                           └───────┬───────┘
                                           ┌───────▼───────┐
                                           │ Return        │
                                           │ Received +    │
                                           │ Inspected     │
                                           └───────┬───────┘
                                           ┌───────▼───────┐
                                           │ Refund        │
                                           │ Processed     │
                                           └───────────────┘

EU_LAW: consumers have 14-day right of withdrawal for online purchases (EU Consumer Rights Directive). The return shipping cost can be charged to the consumer if clearly stated before purchase. See Consumer Protection.

Shipping Rules Engine

A shipping rules engine auto-selects the carrier based on order characteristics:

// Shipping rules configuration (store in database, not code)
interface ShippingRule {
  id: string;
  name: string;
  priority: number;                    // lower = higher priority
  conditions: {
    destinationCountry?: string[];     // ['NL', 'BE', 'DE']
    weightMin?: number;                // kg
    weightMax?: number;                // kg
    orderValueMin?: number;            // EUR cents
    orderValueMax?: number;            // EUR cents
    dimensionFits?: 'letterbox' | 'standard' | 'large';
  };
  action: {
    carrierId: string;                 // 'postnl', 'dhl', 'dpd'
    serviceType: string;               // 'standard', 'express', 'pickup'
    freeShippingThreshold?: number;    // EUR cents — free shipping above this
  };
}

// Example rules
const defaultRules: ShippingRule[] = [
  {
    id: 'nl-letterbox',
    name: 'NL letterbox packages via PostNL',
    priority: 1,
    conditions: { destinationCountry: ['NL'], dimensionFits: 'letterbox' },
    action: { carrierId: 'postnl', serviceType: 'letterbox' },
  },
  {
    id: 'nl-standard',
    name: 'NL standard via PostNL',
    priority: 2,
    conditions: { destinationCountry: ['NL'] },
    action: { carrierId: 'postnl', serviceType: 'standard', freeShippingThreshold: 5000 },
  },
  {
    id: 'eu-standard',
    name: 'EU via DHL Parcel Connect',
    priority: 3,
    conditions: { destinationCountry: ['BE', 'DE', 'FR', 'AT', 'ES', 'IT'] },
    action: { carrierId: 'dhl', serviceType: 'parcel_connect' },
  },
  {
    id: 'international',
    name: 'International via DHL Express',
    priority: 10,
    conditions: {},                     // catch-all
    action: { carrierId: 'dhl', serviceType: 'express' },
  },
];

SENDCLOUD_NOTE: Sendcloud has a built-in shipping rules engine in their panel. When using Sendcloud, configure rules there instead of building a custom engine. Set apply_shipping_rules: true on parcel creation.


Database Schema for Shipments

CREATE TABLE shipments (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  order_id UUID NOT NULL REFERENCES orders(id),
  provider TEXT NOT NULL,              -- 'sendcloud', 'postnl', 'dhl'
  carrier TEXT NOT NULL,               -- 'postnl', 'dhl', 'dpd', 'ups'
  service_type TEXT,                   -- 'standard', 'express', 'letterbox'
  tracking_number TEXT,
  tracking_url TEXT,
  label_url TEXT,
  status TEXT NOT NULL DEFAULT 'pending',
  weight_kg NUMERIC(6,3),
  dimensions_cm JSONB,                -- { length, width, height }
  shipping_cost_cents INTEGER,
  customs_data JSONB,                 -- HS codes, values, origins (for international)
  tracking_history JSONB DEFAULT '[]',
  provider_reference TEXT,            -- provider's internal ID
  created_at TIMESTAMPTZ DEFAULT now(),
  shipped_at TIMESTAMPTZ,
  delivered_at TIMESTAMPTZ,
  updated_at TIMESTAMPTZ DEFAULT now()
);

CREATE TABLE returns (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  order_id UUID NOT NULL REFERENCES orders(id),
  shipment_id UUID REFERENCES shipments(id),
  return_tracking_number TEXT,
  return_tracking_url TEXT,
  return_label_url TEXT,
  reason TEXT,
  status TEXT NOT NULL DEFAULT 'requested',
  refund_amount_cents INTEGER,
  created_at TIMESTAMPTZ DEFAULT now(),
  received_at TIMESTAMPTZ,
  processed_at TIMESTAMPTZ
);

CREATE INDEX idx_shipments_order ON shipments(order_id);
CREATE INDEX idx_shipments_tracking ON shipments(tracking_number);
CREATE INDEX idx_shipments_status ON shipments(status);
CREATE INDEX idx_returns_order ON returns(order_id);

Carrier API Quick Reference

Carrier API Docs Auth Sandbox Webhook
PostNL developer.postnl.nl API key Yes No (poll)
DHL EU developer.dhl.com OAuth2 Yes Yes
DHL NL api-gw.dhlparcel.nl Token-based Yes Yes
DPD dpd.com/developers JWT Yes Yes
UPS developer.ups.com OAuth2 Yes Yes
Sendcloud sendcloud.dev Basic Auth No* Yes
MyParcel developer.myparcel.nl Bearer token No Yes

*Sendcloud uses test shipping methods instead of a separate sandbox.


Sources