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.
Shipping Aggregators — Recommended Approach¶
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¶
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¶
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¶
- PostNL Developer Portal
- PostNL API Documentation
- PostNL Changelog
- DHL API Developer Portal
- DHL eCommerce Europe — eConnect API
- DHL Parcel EU (BE-LU-NL)
- DHL Parcel NL API Guide
- DHL Unified Tracking API
- DPD Developer Integration
- DPD API Developer Guidelines (PDF)
- UPS Developer Portal
- UPS Shipping API Guide (atoship)
- UPS GitHub SDKs
- Sendcloud Shipping API
- Sendcloud Developer Portal
- Sendcloud Help Center
- MyParcel API Documentation
- MyParcel Legacy API