Multi-carrier shipping for Italian ecommerce: 3PL, GLS and unified tracking in one console
- shipping
- ecommerce
- automation
Italian ecommerce lives with three types of intermediary for logistics: a 3PL handling warehouse and fulfillment, one or more national couriers for delivery, and a tracking aggregation service to keep customer care sane. Three vendors, three very different APIs, three vocabularies. Each has its own exceptions (held packages, failed attempts, wrong addresses), its own webhooks, its own formats. As volume grows, the problem isn’t “shipping”: it’s knowing where the packages are and why some are stuck.
This post tells how we unified, into a single operational console, the integration with an Italian 3PL, the courier portal of one of the largest national networks (with WebLabeling and even SOAP API), and an aggregated tracking platform. Industry: footwear ecommerce, medium-high volume, Italian market. The patterns described apply to any Italian ecommerce doing external fulfillment + courier + tracking.
The three actors and why each is needed
- 3PL (e.g. Elogy): receives orders from the store, handles picking/packing/shipping. Exposes REST APIs for order push and multi-event webhooks for updates (
new_lead,shipped,delivered,exception). - National courier (e.g. GLS Italia): physical package delivery. Exposes a portal (WebLabeling) for label generation and shipment management, plus APIs for status polling and — when needed — a SOAP API for real-time operations like hold/release of held packages.
- Tracking aggregator (e.g. Qapla’): normalises events from N carriers into a single format, provides customer widgets, automated emails, and a webhook API to push events to the merchant.
Each is excellent at one thing. None is the complete source of truth. The merchant needs a unified view.
Pattern 1: 3PL integration with multi-event webhook
Pushing the order to the 3PL is the easy part: a function that maps the Shopify order into the format the 3PL expects (order header, item lines, address, notes, requested service). The delicate part is state management.
The 3PL exposes a single webhook with an event_type field. The handler:
- Verifies webhook signature (HMAC or shared secret token).
- Persists the event in a
shipping_eventstable withprovider,event_type,payload,received_at. - Develops the event into a per-shipment state machine (
pending → labeled → in_transit → delivered → exception → resolved). - Notifies downstream systems (customer care UI, customer email via WhatsApp/email, Shopify fulfillment update).
Status sync complements the webhook: every 30 minutes a script calls the status endpoint for “in flight” orders and catches events the webhook may have missed (a classic: badly signed webhook = rejected by our handler = status not updated until poll reconciles).
Pattern 2: GLS Italia between WebLabeling, webhook and SOAP
GLS is a case study in itself. It exposes three very different surfaces:
- WebLabeling: modern REST API for label generation, pickup booking, status reading.
- Webhook push (GLS Italy): shipment event notifications, structured format but not always complete on held-package detail.
- SOAP API: for advanced operations like
ReleaseShipmentStock(held package release), which REST doesn’t cover.
Held packages are the most expensive event category for customer care. A held package not resolved within 48 hours becomes a return. Our solution:
- Dedicated polling for held packages: every 60 minutes, the script reads shipments in “exception” state from webhook and poll status, and queues them in
held_open. - Resolution UI: the operator sees the queue with all the detail and can act: contact customer, request pickup point retrieval, unblock from GLS portal.
- Manual resolution logger: when the operator acts, the action gets logged. This lets us see, after 3-6 months, which held-package causes are most frequent (wrong address, recipient absent, document issue) and fix upstream.
The SOAP part is the most “old school”. GLS exposes a WSDL endpoint for ReleaseShipmentStock: the integration requires parsing XML, handling SOAP envelopes, typical enterprise authentication (username + password + customer code in headers). The pattern that worked:
- A dedicated Edge Function (Deno) that manually builds the SOAP body and parses the XML response with a robust parser.
- Retry with backoff on timeout (the endpoint is notoriously slow).
- Mapping SOAP error codes to our internal states, because the GLS text message is in Italian but not parser-friendly.
Integrating a SOAP API in 2026 may sound anachronistic, but for certain legacy services it’s the only option. It’s worth isolating it in a single function, with a REST-like interface toward the rest of the system.
Pattern 3: aggregated tracking and stale reconciliation
The tracking aggregator handles normalising cross-carrier events. We connect it via webhook: every tracking event arrives, we persist it, and we update the customer-visible state.
The real problem is when tracking “gets stuck”. A package that’s been in_transit for 5 days with no new events is almost always a problem: forgotten at a hub, wrong address not yet flagged, local strike. Customer care can’t look at every single tracking.
The qapla_stale_check reconciliation script runs every night:
- Selects all trackings in non-terminal state (
in_transit,out_for_delivery) with no events in the last 24 hours. - For each, forces a refresh by calling the tracking aggregator API (it doesn’t just wait for the webhook).
- If after refresh the state doesn’t change, it raises an alert on the customer care console with priority based on days_since_last_event.
In a typical week the script identifies 20-40 stale trackings that without this check would have reached customer care only after the customer’s email.
The unified console: held packages + logistics in one view
All these flows land in an internal UI that shows, per shipment:
- 3PL status (order received, packed, shipped).
- Courier status (in transit, in delivery, delivered, held, problem).
- Tracking status (cross-carrier normalised events).
- Any pending actions (held package to resolve, address to confirm, scheduled next attempt).
The UI has quick filters: “shipments held > 24h”, “stale shipments > 48h”, “exist in 3PL but no tracking”. It’s the single pane of glass that lets the logistics team work by priority instead of by chronological order.
Concrete example: on a volume of 200-300 shipments per day, the “held open” queue typically shows 15-25 records. The team resolves 80% within the day. Without the aggregated view, the same held packages used to emerge messily from the 3 separate systems and would close in 2-3 days. The operational gain is the customer’s time receiving the package, not the team’s time at constant volume.
Replicable pattern
If you run an Italian ecommerce with 3PL + courier + tracking:
- Persist every event webhook in a generic
shipping_eventstable, before building the logic. You’ll need the history. - Unified shipment state machine, not per-vendor. You map the 3 actors’ states into your model, not the other way round.
- Webhook + poll always together. Webhook for latency, poll for reliability.
- Dedicated queue for exceptions (held packages, stale tracking). Without one, exceptions hide in the volume.
- Isolate legacy APIs (SOAP, XML) in single functions with a REST-like interface to the rest of the system.
- Log manual resolutions: after 3 months you’ll have data to optimise upstream.
Setup for an Italian merchant with 3 logistics vendors is typically 3-5 weeks. The return is in customer care (fewer “where’s my order?” tickets), in avoided returns (held packages resolved in time), and in the logistics team’s capacity.