Apilo
API
Integracje
Node.js
E-commerce
REST

The Apilo API — 8 traps that cost you shipments

Integration notes: tokens after a redeploy, a silent 512-record limit, a carrier method that isn't in the order, tags outside the list, and a webhook whose condition lives outside the code.

Mateusz KozłowskiMateusz Kozłowski14 min read
In this article

Why this list

These are notes from building a layer on top of Apilo for a feed wholesaler — a panel that assembles order batches, splits them into parcels and prints labels in bulk. I described the business side in a separate case study; what's left here is the integration itself: where the Apilo API behaves differently from what the documentation suggests, and which of those differences cost real money.

Every example comes from a live deployment and refers to the Apilo REST API as we worked with it in the summer of 2026. If you're reading this later, check the swagger on your own account — some behaviour (carrier method identifiers, for instance) is changed by the marketplace, not by Apilo.

1. Tokens: a single-use code and an ephemeral disk

Apilo's authorisation is an OAuth variant with a single-use code: you exchange the code from the panel once for an access + refresh pair, and then live off the refresh token. Once used, the code is dead — a second exchange attempt returns 401 no matter how valid it looks.

# to działa raz — po wymianie kod jest martwy
POST /rest/auth/token/        (Basic: clientId:clientSecret)
  { "grantType": "authorization_code", "token": "<APILO_CLIENT_CODE_AUTH>" }
  → { accessToken, refreshToken, accessTokenExpireAt, refreshTokenExpireAt }

# to woła się w kółko — i to trzeba przechować
POST /rest/auth/token/
  { "grantType": "refresh_token", "token": "<refreshToken>" }

The symptom that exposed this for us: 401 Invalid credentials after every redeploy. Tokens were saved to a file, and the host (Railway) has an ephemeral filesystem — after a deploy the file is gone, the authorisation code is already spent, and the integration is down. Tokens have to live in the database, shared by the panel and the scheduled job.

A second false alarm: the access token shown in the Apilo panel is not your token. The panel shows the one generated when the application was created; the integration keeps its own copy and refreshes it independently. An expired value in the panel says nothing about production — diagnose through your own status endpoint. A keep-alive helps too: ours rolls the token as a side effect of the hourly sync, so a disconnect only threatens after a longer outage.

2. The order list stops at 512 records

GET /rest/api/orders/ has a hard limit of 512 records per request. Sending limit=9999 doesn't produce an error — Apilo quietly returns one page:

GET /rest/api/orders/?limit=9999      → 512 zamówień, HTTP 200, zero ostrzeżeń
GET /rest/api/orders/?limit=500&offset=0    → 500
GET /rest/api/orders/?limit=500&offset=500  → 500
GET /rest/api/orders/?limit=500&offset=1000 → 137  ← porcja niepełna = koniec

The effect: orders beyond the first page don't exist in the panel. To the operator it looks like broken filters („my order isn't there”), while the sync log shows an innocent Synced 512/512 orders. The fix is paging by offset until a partial page — ideally in a single function, because in our case the same query lived in two files and fixing one copy left the other broken for another day.

3. method is not originalCode

When creating a shipment (POST /rest/api/shipping/shipment/) the method field looks obvious: the order already has a shipping line item with an originalCode. That's the trap:

POST /rest/api/shipping/shipment/
  { "method": "2488f7b7-…-a1b2c3d4e5f6", … }   ← originalCode z Allegro

422 Invalid method given. Available methods are:
    inpost_locker_standard, inpost_courier_standard, inpost_locker_allegro, …

method has to come from the method list of the carrier account (GET /rest/api/shipping/carrier-account/:id/method/), not from the order. Worse, the format depends on the account:

  • InPost, DPD and personal pickup accounts → slugs (inpost_locker_standard, inpost_locker_allegro, default),
  • Allegro accounts (DHL, DPD, One, Orlen, Poczta, UPS) → UUIDs, and there originalCode usually matches. That's why the integration „worked” right up to the day Allegro changed its locker method identifiers — and an InPost account started receiving an Allegro UUID,
  • locker methods additionally require the sendingMethod and template options (size A/B/C), and the target locker sits in the order's addressDelivery.parcelIdExternal — if you don't pass it into the receiver address, the shipment is created without a destination point.
The recognised carrier category and its limits shown on the order — before the shipment exists. Screenshot from demo mode, fictional data.

4. Dimensions: a 422 instead of a shipment

Dimensions are a separate class of 422. If you send a hard-coded 60×40×40 for every carrier, a courier will accept it and a pickup point will reject the whole shipment:

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

Two things are worth doing straight away: keep weight and dimension limits in per-category configuration and clamp the defaults to them (better a smaller parcel than no parcel), and recognise the category from the human-readable method name (originalName, e.g. „ORLEN Paczka — pickup point”) rather than from originalCode — that one is often a UUID and matches no rule.

5. createdAt is not the order date

createdAt on an Apilo order is the date it was imported into Apilo, not the date the customer placed it. That one is orderedAt, and the gap can be two days — an order placed on 24 July showed up in our list as 26 July:

RestOrderListDTO     → createdAt            (data importu do Apilo)
RestOrderDetailDTO   → createdAt, orderedAt (orderedAt = data złożenia u sprzedawcy)

filtry: createdAfter/Before · orderedAfter/Before · updatedAfter/Before

So the date column in the panel has to show orderedAt (falling back to createdAt, which is all the list returns). But the fetch filter deliberately stayed on createdAfter: with orderedAfter, orders placed earlier but imported into Apilo only now would drop off the print list. Those are exactly the „late” ones you cannot afford to lose.

6. Tags aren't on the order list

The „Label created” tag is the most reliable proof that a shipment already exists. The problem is that the API treats tags as an afterthought:

GET /rest/api/orders/                 → brak pola z tagami
GET /rest/api/orders/{orderId}/tag/   → tagi JEDNEGO zamówienia
GET /rest/api/orders/tag/map/         → słownik tagów (tu bierzesz id)

The consequence is architectural: for a list of 400 orders you cannot check tags — that would be 400 requests. We check them selectively: during the cart pre-flight and behind a separate button for the current page of the list, with a cap and limited concurrency.

Cart pre-flight: eight orders ready to print, one undetermined — and that one stays out of the batch.

7. Check which DTO sits behind the endpoint

Apilo has several structures sharing a base name (…DTO, …DTO2, …DTO3) with different fields. The name guarantees nothing, and the difference can switch off an entire feature:

GET /rest/api/orders/{id}/shipment/    → RestOrderShipmentDTO   { id, idExternal }
GET /rest/api/shipping/shipment/{id}/  → RestShipmentDetailsDTO  { …, media, status }

Our „print all labels from the cart” feature read the label file identifier from the order's shipment list. It worked locally because the mock was more generous than production and returned full records. In production, with 19 orders holding ready labels, the message read „no order has a ready label”. The moral: a mock that returns more than the real API isn't a convenient mock, it's a test that certifies code which doesn't work.

8. The cancellation webhook and a condition living outside the code

An hourly sync isn't enough for cancellations: an order cancelled at 10:05 was getting a paid label at 10:20. Apilo can call our URL immediately, but the „Cancelled” condition lives in a rule inside the Apilo panel, not in the code. The call itself means „cancelled” — so changing the rule changes the meaning of the endpoint, and nothing in the repository shows it:

Apilo → Automatyzacja → „Zmiana statusu zamówienia"
        → warunek: Status zamówienia = Anulowane
        → akcja:   Wywołaj adres URL

GET /api/webhooks/apilo/order-cancelled/<SEKRET>?orderId={orderId}
We keep the configuration instructions in the panel next to the URL you paste into Apilo — otherwise that knowledge disappears into an email thread.
  • Register the webhook route before the auth middleware. Apilo has no session cookie — behind a login requirement it gets a redirect to the login page and the webhook „works” while doing nothing. The rule in Apilo will not report that mistake.
  • Don't mirror the cancellation into the order status. The webhook also arrives for orders the sync hasn't imported yet — the UPDATE has nothing to update, and the later import sets the status back to „ready to ship”. The single source of truth should be a separate cancellations table (ours deliberately has no foreign key, precisely because of that window).
  • Store the whole request payload (a JSONB column in our case) and guard the secret in the URL: no secret means 503, fail closed. Never log the value that was supplied.

Rules for working against a live API

Apilo is the client's production system, and POST /rest/api/shipping/shipment/ creates a real, paid shipment at the carrier that the integration cannot undo. That changes how you work more than any API detail:

  • 1

    Zero tests against the production account

    Tests run against a local HTTP mock (swap the API base URL) and a throwaway database. No test may touch the client's account or the database holding learned patterns.

  • 2

    The mock reproduces constraints, not convenience

    The 512 limit, the shapes of the structures, the missing fields — the mock has to repeat them. Otherwise the tests go green on code that doesn't work in production.

  • 3

    Guards where the money is spent

    „Does this shipment already exist” and „has this order been cancelled” have to be checked in the endpoint that creates the shipment, not only in the preview. The preview doesn't pay the invoices.

  • 4

    Read-only diagnostics

    A handful of endpoints that only read (list fields, the tag dictionary, carrier account methods) cut every subsequent incident from hours to minutes.

For the same story from the warehouse floor — 10 kg sacks, locker sizes and batches of eighty labels — see the case study of this integration.


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