Apilo
Case Study
E-commerce
Logistyka
InPost
Automatyzacja

Apilo and oversized parcels — a case study

How an animal feed wholesaler stopped splitting sacks into parcels by hand and clicking labels one by one — and why weight is the smallest part of the problem.

Mateusz KozłowskiMateusz Kozłowski12 min read
Every screenshot in this post comes from the system's demo mode — the orders and customer data are fictional.
In this article

What came out of it

An animal feed and pet supplies wholesaler sells on Allegro and its own store, and collects the orders in Apilo. The goods are sacks weighing 2 to 25 kg, so nearly every parcel brushes against a carrier limit. We built a layer on top of Apilo that splits an order into parcels on its own, picks the locker size, prints labels for a whole batch — and learns from the operator every time she corrects its proposal.

  • 1

    One algorithm decides the parcel split

    The single-order preview and the batch print call the same function. Previously the batch print used its own rule — one item, one parcel — and bought more shipments than needed.

  • 2

    Carrier limits in configuration, not in code

    Weight and dimensions per category (locker, pickup point, courier) live in one JSON file. A new carrier or a changed limit is an entry, not a logic change.

  • 3

    The system learns from the operator

    A manual correction is stored as a pattern for that exact set of line items. The next identical order packs itself — in the batch print too.

  • 4

    Nothing gets printed without proof

    Before a batch, the panel checks which orders already have a label in Apilo. Every shipment created is paid for at the carrier and cannot be undone from the integration.

Monday: 80 labels and sacks of feed

Orders from Allegro and the store flow into Apilo. Apilo collects them well in one place and can create a shipment at the carrier, but it is still one order at a time: open it, check what's inside, decide how many parcels, type the weight, pick the size, create the shipment, print. With twenty orders that's survivable. On a Monday morning there can be eighty.

The second problem is the product range. A feed store doesn't sell 20×15×10 cm boxes. It sells 10, 20 and 25 kg sacks — goods that brush against a weight or dimension limit on every single order. The same line item fits into a parcel locker one day and has to go by courier the next.

The order list pulled live from Apilo. Filtering by SKU, carrier and status is the starting point for assembling a print batch.

The third problem is the expensive one: every shipment created in Apilo is paid for at the carrier and cannot be reversed from the integration. A mistake doesn't end with an error message, it ends with an invoice. That's why this whole automation is built around the question „are we really allowed to create this”, not around „how do we do it faster”.

Why „split it into parcels” isn't a sum of weights

The first instinct — add up the item weights and divide by the carrier limit — breaks on three things at once:

  • A parcel locker has sizes, not just a weight limit. Compartment A is 8 cm high, B is 19 cm, C is 41 cm. A 10 kg sack is light against the 25 kg limit, but it is bulky and will not fit into size B.
  • Same contents, different carrier, different split. Two 10 kg sacks fit into one locker shipment (20 ≤ 25 kg), sit exactly on the line at a pickup point with a 20 kg limit, and leave room for a third with a courier (31.5 kg).
  • The split translates straight into cost. Every extra parcel is another paid shipment. The rule „one item, one parcel” is safe and systematically overpays.

So the split is computed by a packing algorithm (first-fit decreasing) against the weight limit of the carrier category, and the locker size is driven by the most demanding item in the parcel, not by the total weight. A sack of 10 kg or more forces size C even when the weight would fit a smaller compartment.

Three 10 kg sacks going to a parcel locker: the system proposes two parcels (20/25 kg and 10/25 kg) and locker size C for both. The operator can move an item to another parcel — and that correction is remembered.

How the system knows what a sack weighs at all

Apilo returns order line items with a name, SKU, quantity and price. It does not return a weight — usually there simply isn't one in the sales catalogue. The industry saves us: the weight is in the product name („Horse feed 25kg”, „Poultry granulate 10kg”). So a name parser reads the weight — and that is exactly the piece of code that can quietly cost money.

„Worek 22,5 kg"   → regex z samą kropką → 5 kg     ❌ zaniżona deklaracja → dopłata od kuriera
„Mieszanka 1,25kg" → regex z samą kropką → 25 kg    ❌ fałszywa blokada limitu
„Worek 22,5 kg"   → regex z przecinkiem → 22,5 kg  ✅

A regular expression that only accepts a decimal point turns „Sack 22,5 kg” into 5 kg and „Mix 1,25kg” into 25 kg. The first mistake is an under-declared weight and a carrier surcharge after the fact; the second is a false limit block on something that weighs a kilo. Both look perfectly normal in the panel.

Carrier limits: weight is half the problem

The first version sent Apilo hard-coded dimensions of 60×40×40 cm for every shipment. With a courier it went through. Until the first pickup-point shipment:

422 DIMENSIONS_VALIDATION_ERROR
packages.dimensions  Podane wymiary wykraczają poza limit 64 x 41 x 38 cm

A height of 40 cm against a 38 cm limit killed the entire shipment, and the message reached the operator as a raw 422. The fix was to move the limits out of the code and into configuration — a single carrier-limits.json holds both weight and dimensions per carrier category:

{
  "id": "inpost_paczkomat", "label": "InPost Paczkomat",
  "match": ["paczkomat"], "maxKg": 25,
  "maxDims":     { "l": 64, "w": 38, "h": 41 },
  "defaultDims": { "l": 60, "w": 35, "h": 38 }
},
{
  "id": "locker_or_point", "label": "Automat / punkt odbioru",
  "match": ["automat", "punkt", "orlen paczka", "żabka"], "maxKg": 20,
  "maxDims":     { "l": 64, "w": 41, "h": 38 },
  "defaultDims": { "l": 60, "w": 40, "h": 30 }
}
The same 10 kg sack, but shipped to a pickup point: a different weight limit (20 kg) and different dimensions (64×41×38). The panel shows the limit before anyone clicks „Generate”.

The category is recognised from the human-readable shipping method name („ORLEN Paczka — pickup point”), not from the method code — codes are often marketplace identifiers that match no rule at all. Adding a carrier or changing a limit is now a JSON edit; default dimensions are clamped to the limit so that a smaller parcel goes out instead of a 422.

One packing brain — and learning from the batch print

The worst version of a system like this is the one where the preview shows something different from what the print does. We had it for a while: the order preview computed parcels with the algorithm, while the batch print followed „one item, one parcel”. The operator saw one parcel and the batch bought two shipments. Today both paths call the same function, and the order of decision sources is explicit:

  • 1

    A manual correction on this order

    If the operator rearranged the items in the preview, her split beats everything else.

  • 2

    A remembered pattern

    The order fingerprint is the carrier category plus the set of SKUs with quantities. A hit means somebody has already packed exactly this combination by hand.

  • 3

    The packing algorithm

    Only when there is neither a correction nor a pattern does it run first-fit decreasing against the category weight limit and the size rule.

The pattern fingerprints the whole order, not a single item: carrier_category::sku:quantity, sorted. A different quantity of the same SKU is a different pattern, because it packs differently. So is a different carrier type, because the limit changes.

The batch print learns too — a bigger change than it looks. Patterns used to be created only in the single-order preview, the path the operator takes least often. All the main traffic went through batches and taught the system nothing.

The print batch: cart, pre-flight, weight review

Orders go into a cart stored in the database, not in a browser tab. That way a batch can be assembled before noon, picked up again after lunch, and the list shows that a colleague has already put the same orders into her own cart.

Pre-flight before generating: eight orders ready, one undetermined. „Undetermined” does not mean „no label” — such an order must not enter the batch.

Before a single shipment is created, the panel answers the question „which of these orders already has a label in Apilo”. It collects the proof from three sources:

  • our own mirror of shipments, refreshed by an hourly sync,
  • the order status in Apilo („Shipped”, „Delivered”) — it comes with the list, so it costs no extra request,
  • the „Label created” tag — the most reliable one, but it needs a separate request per order, so we ask for it selectively.

The verdict has three states: has a label, doesn't, or unknown. Collapsing „unknown” into „doesn't” costs one paid shipment per undetermined order — so those orders simply drop out of the batch with a clear message. For the same reason the cart has two distinct actions: „Generate labels” (creates, costs money, skips anything already labelled) and „Print all labels” (read-only, click as often as you like).

Weight review before dispatching a batch. Products are deduplicated by SKU — the same sack appears in a dozen orders, so the operator fixes its weight once.

The last step before dispatch is the weight review. Products are deduplicated by SKU, so the operator corrects a weight once per product rather than once per order; a missing weight blocks the button, and a weight above the category limit gets a warning before Apilo rejects it. A batch is capped at twenty orders — beyond that, parallel API calls stop being polite.

An order cancelled after the label was printed

The sync with Apilo runs hourly. The operator assembles a batch in fifteen minutes. That is enough for an order cancelled at 10:05 to get a paid label at 10:20 — and for nobody to notice until the carrier invoice arrives.

So a cancellation doesn't wait for the sync; it arrives from Apilo immediately, through an automation rule that calls our URL. The record lands in a separate table, and a server-side guard rejects any attempt to create a shipment for a cancelled order. It is the one block that cannot be overridden with a „generate anyway” button: a second label is sometimes a deliberate choice, a label for a cancelled order never is.

What changed (and what it doesn't solve)

Day-to-day work now looks like this:

  • Assembling batches instead of clicking through orders. Filter by SKU and carrier, cart, one weight review, one merged PDF to the printer.
  • The parcel split is the system's decision, and when the operator corrects it, the next identical order is already packed correctly.
  • Carrier limits are visible before printing, instead of arriving as a 422 from Apilo after the fact.
  • Cancelled and already-labelled orders drop out of the batch on their own.

I'm not quoting savings percentages here, because an honest number comes from measurement, not from a slide. The system collects its own telemetry: how many splits were accepted unchanged, how many were corrected, where the suggestion came from (a stored pattern or the algorithm) and how often the weight is unknown. The AI accuracy tab shows it as a time series — and that is the only version of ROI worth showing a client.

Takeaways for your warehouse

  • 1

    Start with what costs money

    In shipping, the expensive part isn't the time spent clicking — it's the surplus shipment, the surcharge for an under-declared weight and the label for a cancelled order. Automate around those three, not around „faster”.

  • 2

    One decision path, not two

    If the preview and the execution compute the same thing in two pieces of code, sooner or later they will diverge — and the client will see it first.

  • 3

    Limits and rules belong in configuration

    A table of carrier limits changes more often than the logic around it. Kept in code, it goes stale in two copies at once.

  • 4

    Measure from day one

    Telemetry of „how much the system did on its own versus what a human corrected” costs a few lines per operation, and after a month it is the only hard answer to whether the investment paid off.

If you're after the technical side — the traps in the Apilo API itself: tokens, the silent 512-record limit, carrier methods, tags and webhooks — I wrote them up in a separate post.


Mateusz Kozłowski

Mateusz Kozłowski

Founder of flowbiz · Process automation expert

I implement automations, integrations and AI in mid-sized companies across Pomerania and Kuyavia-Pomerania.

Mateusz from flowbiz - automation expert

Free Consultation

Reclaim 40 hours weekly

I'll walk you through the automation process step by step. No technical jargon, no hidden costs - just concrete solutions tailored to your business.

Free process audit - we'll pinpoint your biggest bottlenecks

Concrete savings plan - we'll show you how much you'll save

Fast rollout - see your first results within a week