Back to Journal
logisticsen

Carrier integration: Foxpost, MPL, GLS

Six carriers, six webhook shapes, six label formats. The Foxpost parcel-locker API is the one thing nobody else does well.

One adapter pattern, six carriers. That is the marketing pitch, and the marketing pitch lies, because the "one" adapter pattern is really six bespoke implementations behind a shared interface. Here is what we learned wiring up Foxpost, MPL, GLS, DPD, DHL and UPS one by one.

Label generation: PDF vs. ZPL

Two formats in the world. PDF is the small-business default (A4 sheet, home printer). ZPL is the professional one (Zebra GK420 or similar label printer, 100x150mm). The Logistics module emits both, and the partner picks which to send: smaller warehouses (1-2 pickers) print PDF, larger ones (5+ pickers) push ZPL to the label printer via JetDirect.

Foxpost is the odd one out: their labels come as PNG (yes, really), which no printer driver enjoys. Our fix: server-side convert the PNG to ZPL using the ZebraPicture library, and the partner never sees the difference.

Webhook ingestion

Every carrier sends webhooks (delivered, failed, in_transit, returned), and every one is different. Our CarrierWebhookController has one endpoint per carrier (/api/v1/carriers/foxpost/webhook, /api/v1/carriers/mpl/webhook, ...), and each adapter's own parseWebhook runs a zod schema over the payload. On success the normalised carrier_event row lands in carrier_events.

The most painful lesson: carrier X (we will not name them) returns HTTP 200 on webhook ingest even when the payload is semantically broken (missing tracking number, for example). That means we cannot tell a successful ingest from a silent failure on their end. The fix: parse errors land in webhook_parse_errors, and a low-frequency Slack alert fires when the parse-error rate goes above 1%/hour for a given carrier. That is how we caught a DPD schema change within 38 minutes in March 2026.

Idempotency keys per carrier

DHL and UPS send an idempotency key (X-Idempotency-Key header). GLS does not. Foxpost deliberately double-fires the delivered event to make sure we receive it. So the Logistics platform keeps a carrier_event_hash dedup table holding the SHA-256 of every webhook payload. If the same hash arrives within 24 hours we drop it. Since January 2026 this has stopped 1.4 million duplicate events.

For bookShipment calls we supply our own idempotency key (order_42_attempt_2); the carrier either honours it or not, and if not, our server-side dedup catches the duplicate. Our first implementation was naive exponential backoff (1s, 2s, 4s, 8s), and at GLS that produced double bookings because the reservation succeeded and only the response was lost. Today every booking call carries an idempotency key.

COD reconciliation

Carriers ship weekly cash-on-delivery settlement files (CSV or SFTP feeds). The Logistics module records them in cod_collections, and the Finance module reconciles against the list of dispatched COD orders. Mismatches (carrier remitted less than the webhook said was collected) land in a review queue, and the finance operator works through them with the carrier. GLS COD settlement averages 8.4 days of lag (worse at month end). We feed that into the carrier performance scorecard.

The one thing only Foxpost does well

The Foxpost parcel-locker availability API. Real-time, tells us which locker my parcel will fit into (based on dimensions and current occupancy). We wire it into the webshop checkout, so the buyer can only choose lockers with capacity. None of the other five carriers offer this; the rest reply with "we will see at sortation." It is an API feature that may be the single biggest reason Foxpost holds the lead in the Hungarian parcel-locker market.

The takeaway

There is no winner among the carriers: there is cheap (MPL), precise (DHL), flexible (GLS), locker-focused (Foxpost). The Logistics module's job is to abstract that completely, so the partner does not feel the difference at the webshop layer. After six years of integration refits the lesson is simple: there is no "final" adapter implementation. Carriers change something every month.