W tym artykule
Po co ta lista
To są notatki z budowy nadbudowy nad Apilo dla hurtowni pasz — panelu, który kompletuje partie zamówień, dzieli je na paczki i drukuje etykiety zbiorczo. Stronę biznesową opisałem w osobnym case study; tutaj zostaje sama integracja: co w API Apilo działa inaczej, niż sugeruje dokumentacja, i które z tych różnic kosztują realne pieniądze.
Wszystkie przykłady pochodzą z działającego wdrożenia i dotyczą REST API Apilo w wersji, z którą pracowaliśmy latem 2026. Jeśli czytasz to później, sprawdź swagger swojego konta — część zachowań (na przykład identyfikatory metod przewoźników) zmienia platforma sprzedażowa, nie Apilo.
1. Tokeny: kod jednorazowy i efemeryczny dysk
Autoryzacja Apilo to wariant OAuth z jednorazowym kodem: kod z panelu wymieniasz raz na parę access + refresh, a potem żyjesz z refresh tokena. Kod po użyciu jest martwy — druga próba wymiany kończy się 401, niezależnie od tego, jak poprawny wygląda.
# 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>" }Objaw, który to u nas ujawnił: 401 Invalid credentials po każdym redeployu. Tokeny zapisywały się do pliku, a hosting (Railway) ma efemeryczny system plików — po deployu pliku nie ma, kod autoryzacyjny jest już zużyty i integracja leży. Tokeny muszą siedzieć w bazie, wspólnej dla panelu i dla zadania cyklicznego.
Drugi fałszywy alarm: access token widoczny w panelu Apilo to nie jest twój token. Panel pokazuje ten wygenerowany przy tworzeniu aplikacji, a integracja trzyma własną kopię i odświeża ją niezależnie. Wygasła wartość w panelu nie mówi nic o stanie produkcji — diagnozuj po swoim endpoincie statusu. Warto też mieć keep-alive: u nas token roluje się przy okazji godzinowej synchronizacji, więc rozłączenie grozi tylko po dłuższym postoju serwera.
2. Lista zamówień urywa się na 512 rekordach
GET /rest/api/orders/ ma twardy limit 512 rekordów na żądanie. Wysłanie limit=9999 nie kończy się błędem — Apilo po cichu oddaje jedną porcję:
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
Skutek: zamówienia spoza pierwszej porcji nie istnieją w panelu. Dla operatorki wygląda to na zepsute filtry („nie ma mojego zamówienia”), a w logach synchronizacji widać niewinne Synced 512/512 orders. Naprawa to stronicowanie po offset aż do niepełnej porcji — najlepiej w jednej funkcji, bo u nas to samo zapytanie stało w dwóch plikach i poprawka jednej kopii zostawiła drugą zepsutą na kolejną dobę.
3. method to nie originalCode
Przy tworzeniu przesyłki (POST /rest/api/shipping/shipment/) pole method wygląda na oczywiste: przecież zamówienie ma pozycję wysyłkową z originalCode. To jest pułapka:
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 musi pochodzić z listy metod konta kuriera (GET /rest/api/shipping/carrier-account/:id/method/), a nie z zamówienia. Co gorsza, format zależy od konta:
- konta InPost, DPD czy odbiór osobisty → slugi (
inpost_locker_standard,inpost_locker_allegro,default), - konta Allegro (DHL, DPD, One, Orlen, Poczta, UPS) → identyfikatory UUID, i tam
originalCodezwykle pasuje. Dlatego integracja „działała” do dnia, w którym Allegro zmieniło identyfikatory metod paczkomatowych — i nagle konto InPost dostawało UUID Allegro, - metody paczkomatowe wymagają dodatkowo opcji
sendingMethoditemplate(gabaryt A/B/C), a docelowy paczkomat siedzi waddressDelivery.parcelIdExternalzamówienia — jeśli nie przekażesz go do adresu odbiorcy, przesyłka powstanie bez punktu docelowego.
4. Wymiary: 422 zamiast przesyłki
Wymiary to osobna klasa błędu 422. Jeśli wysyłasz sztywne 60×40×40 dla każdego przewoźnika, u kuriera przejdzie, a punkt odbioru odrzuci całą przesyłkę:
422 DIMENSIONS_VALIDATION_ERROR packages.dimensions Podane wymiary wykraczają poza limit 64 x 41 x 38 cm
Dwie rzeczy warto zrobić od razu: trzymać limity wagi i wymiarów w konfiguracji per kategoria i przycinać do nich domyślne wymiary (lepiej wysłać mniejszą paczkę niż nie wysłać nic), a kategorię rozpoznawać po czytelnej nazwie metody (originalName, np. „ORLEN Paczka — odbiór w punkcie”), nie po originalCode — ten bywa identyfikatorem UUID i nie dopasujesz go do żadnej reguły.
5. createdAt to nie data zamówienia
createdAt w zamówieniu Apilo to data importu do Apilo, a nie data złożenia zamówienia przez klienta. Ta druga to orderedAt, a różnica bywa dwudniowa — u nas zamówienie z 24 lipca pokazywało się na liście jako 26 lipca:
RestOrderListDTO → createdAt (data importu do Apilo) RestOrderDetailDTO → createdAt, orderedAt (orderedAt = data złożenia u sprzedawcy) filtry: createdAfter/Before · orderedAfter/Before · updatedAfter/Before
Kolumna z datą w panelu musi więc pokazywać orderedAt (z fallbackiem na createdAt, bo lista go nie zwraca). Ale filtr pobierania celowo został przy createdAfter: przy orderedAfter z listy do druku wypadłyby zamówienia złożone wcześniej, a zaciągnięte do Apilo dopiero teraz. To dokładnie te „spóźnione”, których zgubić nie wolno.
6. Tagów nie ma na liście zamówień
Tag „Etykieta utworzona” to najpewniejszy dowód, że przesyłka już powstała. Problem w tym, że API traktuje tagi po macoszemu:
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)Konsekwencja jest architektoniczna: dla listy 400 zamówień nie da się sprawdzić tagów — to byłoby 400 zapytań. Sprawdzamy je punktowo: przy pre-flighcie koszyka i osobnym przyciskiem dla bieżącej strony listy, z limitem i ograniczoną współbieżnością.
7. Sprawdź, które DTO wisi pod endpointem
Apilo ma po kilka struktur o tej samej nazwie bazowej (…DTO, …DTO2, …DTO3) i różnych polach. Nazwa nie gwarantuje niczego, a różnica potrafi wyłączyć całą funkcję:
GET /rest/api/orders/{id}/shipment/ → RestOrderShipmentDTO { id, idExternal }
GET /rest/api/shipping/shipment/{id}/ → RestShipmentDetailsDTO { …, media, status }Nasza funkcja „drukuj wszystkie etykiety z koszyka” czytała identyfikator pliku etykiety z listy przesyłek zamówienia. Lokalnie działała, bo mock był hojniejszy niż produkcja i zwracał pełne rekordy. Na produkcji, przy 19 zamówieniach z gotowymi etykietami, komunikat brzmiał „żadne zamówienie nie ma gotowej etykiety”. Morał: mock, który zwraca więcej niż prawdziwe API, to nie wygodny mock, tylko test potwierdzający kod, który nie działa.
8. Webhook anulowania i warunek żyjący poza kodem
Synchronizacja godzinowa nie wystarcza do anulowań: zamówienie anulowane o 10:05 dostawało płatną etykietę o 10:20. Apilo umie zawołać nasz adres URL od razu, ale warunek „Anulowane” żyje w regule w panelu Apilo, nie w kodzie. Samo wywołanie adresu znaczy „anulowane” — zmiana reguły zmienia więc znaczenie endpointu, a w repozytorium nie widać tego w ogóle:
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}- Trasę webhooka rejestruj przed middlewarem autoryzacji. Apilo nie ma ciasteczka sesji — za wymaganiem logowania dostanie przekierowanie na stronę logowania i webhook „działa”, nie robiąc nic. Reguła w Apilo takiej pomyłki nie zgłosi.
- Nie lustrz anulowania w statusie zamówienia. Webhook przychodzi także dla zamówień, których synchronizacja jeszcze nie zaciągnęła — UPDATE nie ma czego zaktualizować, a późniejszy import ustawia status „gotowe do wysyłki”. Jedynym źródłem prawdy powinna być osobna tabela anulowań (u nas świadomie bez klucza obcego, właśnie z powodu tego okna).
- Zapisz cały ładunek zapytania (u nas do kolumny JSONB) i pilnuj sekretu w adresie: brak sekretu = 503, fail closed. Nigdy nie loguj podanej wartości.
Zasady pracy z bojowym API
Apilo jest systemem produkcyjnym klienta, a POST /rest/api/shipping/shipment/ tworzy realną, płatną przesyłkę u kuriera, której integracja nie potrafi cofnąć. To zmienia sposób pracy bardziej niż jakikolwiek szczegół API:
- 1
Zero testów na produkcyjnym koncie
Testy chodzą po lokalnym mocku HTTP (podmiana adresu bazowego API) i po wyrzucalnej bazie. Żaden test nie ma prawa dotknąć konta klienta ani jego bazy z nauczonymi wzorami.
- 2
Mock odwzorowuje ograniczenia, nie wygodę
Limit 512, kształty struktur, brakujące pola — mock musi je powtarzać. Inaczej testy zielenieją na kodzie, który na produkcji nie działa.
- 3
Strażniki tam, gdzie wydawane są pieniądze
Sprawdzenie „czy ta przesyłka już istnieje” i „czy zamówienie nie zostało anulowane” musi stać w endpoincie tworzącym przesyłkę, a nie tylko w podglądzie. Podgląd nie płaci faktur.
- 4
Diagnostyka tylko do odczytu
Kilka endpointów, które wyłącznie czytają (pola listy, słownik tagów, metody konta kuriera), skraca każdą kolejną awarię z godzin do minut.
Jak to samo wygląda od strony magazynu — worki 10 kg, gabaryty paczkomatów i partie po osiemdziesiąt etykiet — opisałem w case study tej integracji.
Mateusz Kozłowski
Założyciel flowbiz · Ekspert automatyzacji procesów
Wdrażam automatyzacje, integracje i AI w średnich firmach na Pomorzu i w Kujawsko-Pomorskiem.
