VulCode
Blog

InPost ShipX API krok po kroku: przesyłka, etykieta i statusy

Tworzysz przesyłkę do Paczkomatu albo kurierską, czekasz na opłacenie oferty, pobierasz etykietę i odbierasz zmiany statusu webhookiem. Kod w TypeScript na Node.js, sprawdzony z dokumentacją i sandboksem InPost.

VulCodeVulCode14 min czytania
InPost ShipX API krok po kroku: przesyłka, etykieta i statusy - VulCode
Co zbudujeszTypeScript, 8 kroków
  1. PrzesyłkaZamówieniePOST z odbiorcą, gabarytem i usługą.
  2. Oferta i opłataShipXAsynchronicznie: created do confirmed.
  3. EtykietaMagazynPDF, ZPL albo EPL od statusu confirmed.
  4. StatusyKlientWebhook przy każdej zmianie.

Poradnik w skrócie

Zamówienie w Twoim sklepie samo zamienia się w przesyłkę InPost: do Paczkomatu albo kurierem. Magazyn dostaje gotową etykietę do druku, a klient widzi status paczki bez wchodzenia na stronę przewoźnika.

InPost ShipX API

Grupa
Sprzedaż i wysyłka
Poziom
średni
Czas czytania
około 14 min
Kod
TypeScript

Pliki: shipx.ts, webhook.ts, statusy.ts.

Kroki
8 kroków

W 8 z nich kod albo polecenia do skopiowania.

Sprawdzenie
Sandbox ShipX, Node.js i tsx

Adres, format żądań i obsługa błędów potwierdzone bez tokenu

Dokumentacja
4 oficjalne dokumenty

InPost ShipX: autoryzacja i środowiska; InPost ShipX: tworzenie przesyłki w trybie uproszczonym; InPost ShipX: pobieranie etykiet; InPost ShipX: webhooki

Z naszego kodu
Flaem

frozcore/src/store_integrations/webhooks.rs

Zanim zaczniesz

ShipX działa asynchronicznie: po utworzeniu przesyłki InPost przygotowuje oferty, wybiera i opłaca ofertę dla wskazanej usługi, a dopiero potem przesyłka ma status confirmed i numer do śledzenia. Większość błędów w integracjach bierze się z pobierania etykiety, zanim to nastąpi.

Co musisz mieć
InPost ShipX API
Konto na sandboksie Managera Paczek
Na sandbox-manager.paczkomaty.pl zakładasz osobne konto: dane produkcyjne tam nie działają. Token i ID organizacji generujesz w zakładce Moje konto > API, po uzupełnieniu wszystkich danych, także do faktury.
Środki na koncie testowym
Przesyłka do Paczkomatu wymaga środków na koncie. Na sandboksie doładowujesz je wirtualnie w zakładce Płatności.
Node.js 18 lub nowszy
Kod używa wbudowanego fetch. Uruchamialiśmy go na Node.js 22 przez tsx. Na webhook przyda się dowolny serwer HTTP, w przykładzie Express.
Kod Paczkomatu odbiorcy
Przy przesyłce do Paczkomatu klient wybiera punkt, na przykład w widżecie map InPost, a Ty przekazujesz jego nazwę w target_point.

Gabaryty Paczkomatu (inpost_locker_standard)

SzablonWymiaryWaga i gabaryt
small8 x 38 x 64 cmdo 25 kg, gabaryt A
medium19 x 38 x 64 cmdo 25 kg, gabaryt B
large41 x 38 x 64 cmdo 25 kg, gabaryt C
xlarge50 x 50 x 80 cmdo 25 kg, gabaryt D, tylko inpost_courier_c2c

Krok po kroku

8 kroków. Okno obok pokazuje plik kroku, który właśnie czytasz.

Napisz klienta API

Każde żądanie ma token w nagłówku Authorization: Bearer. Sandbox to sandbox-api-shipx-pl.easypack24.net, produkcja api-shipx-pl.easypack24.net. Nagłówek X-User-Agent jest opcjonalny i pomaga InPost w diagnozie, gdy zgłaszasz problem.

Sprawdź organizację i usługi

GET /v1/organizations/:id zwraca nazwę i listę usług dostępnych dla Twojej organizacji. Przesyłkę tworzysz tylko w organizacji, której jesteś członkiem, i tylko dla usługi z tej listy.

Utwórz przesyłkę do Paczkomatu

W trybie uproszczonym wysyłasz od razu usługę. Do Paczkomatu potrzebny jest telefon i e-mail odbiorcy, szablon gabarytu i Paczkomat docelowy w custom_attributes.target_point. sending_method mówi, jak nadasz paczkę: parcel_locker to nadanie w Paczkomacie, dispatch_order to odbiór przez kuriera.

Nadawcy nie musisz podawać: domyślnie to dane organizacji. Odpowiedź ma status created i pusty tracking_number.

Albo przesyłkę kurierską

Kurier potrzebuje adresu odbiorcy oraz imienia i nazwiska albo nazwy firmy. Zamiast szablonu podajesz wymiary i wagę paczki. Przy pobraniu (cod) usługi kurierskie wymagają też ubezpieczenia (insurance).

Poczekaj, aż przesyłka będzie opłacona

Po utworzeniu przesyłki InPost w tle przygotowuje oferty, wybiera ofertę i ją opłaca. Etykietę można pobrać dopiero dla przesyłki w statusie confirmed lub późniejszym. W kodzie odpytujesz przesyłkę, a w produkcji lepiej czekać na webhook shipment_confirmed.

Uwaga
Parametr only_choice_of_offer: true wybiera ofertę bez opłacania. Taką przesyłkę musisz opłacić osobno, zanim oferta wygaśnie.

Pobierz etykietę

GET /v1/shipments/:id/label zwraca plik: PDF, ZPL albo EPL. Typ normal albo A6; dla usług kurierskich bez podania typu dostajesz A6. Kilka etykiet naraz pobierzesz z /v1/organizations/:id/shipments/labels, do 100 przesyłek.

Odbieraj zmiany statusu webhookiem

Adres webhooka dodajesz w Managerze Paczek w zakładce Moje konto > API. Musi odpowiadać 200 na GET. InPost wysyła zdarzenia shipment_confirmed, shipment_status_changed i offers_prepared z adresów 91.216.25.0/24, z produkcji i z sandboksu.

Webhook nie ma podpisu. Ogranicz go do tej puli adresów i nie ufaj treści: po zdarzeniu pobierz przesyłkę z API i zapisz jej status stamtąd.

Pokaż klientowi status po ludzku

GET /v1/statuses nie wymaga tokenu i zwraca każdy status z nazwą, krótkim tytułem i opisem, po polsku albo po angielsku (lang=pl_PL albo en_GB). Zamiast tłumaczyć nazwy samemu, pokaż klientowi tytuł z tej listy.

Okno kodukrok 1 z 8
shipx.tsNowy plik, 19 linii
const API = process.env.SHIPX_URL ?? 'https://sandbox-api-shipx-pl.easypack24.net';const TOKEN = process.env.SHIPX_TOKEN ?? '';const ORG = process.env.SHIPX_ORG ?? ''; export async function shipx<T>(method: string, path: string, body?: unknown): Promise<T> {  const response = await fetch(`${API}${path}`, {    method,    headers: {      Authorization: `Bearer ${TOKEN}`,      'Content-Type': 'application/json',      'X-User-Agent': 'sklep-example',    },    body: body === undefined ? undefined : JSON.stringify(body),  });  if (!response.ok) {    throw new Error(`ShipX ${method} ${path}: ${response.status} ${await response.text()}`);  }  return response.json() as Promise<T>;}
shipx.tsZmiana w pliku: 4 linie
interface Organization { id: number; name: string; services: string[] } const org = await shipx<Organization>('GET', `/v1/organizations/${ORG}`);console.log(org.name, org.services);// ["inpost_locker_standard", "inpost_courier_standard", ...]
shipx.tsZmiana w pliku: 11 linii
interface Shipment { id: number; status: string; tracking_number: string | null } export const lockerShipment = (order: { email: string; phone: string; point: string; ref: string }) =>  shipx<Shipment>('POST', `/v1/organizations/${ORG}/shipments`, {    receiver: { email: order.email, phone: order.phone },    parcels: { template: 'small' },    custom_attributes: { target_point: order.point, sending_method: 'parcel_locker' },    service: 'inpost_locker_standard',    reference: order.ref,  }); const created = await lockerShipment({  email: 'klient@example.com', phone: '888000000', point: 'KRA010', ref: 'zamowienie-1042',});// { id: 12345, status: "created", tracking_number: null, ... }
shipx.tsZmiana w pliku: 17 linii
export const courierShipment = (order: CourierOrder) =>  shipx<Shipment>('POST', `/v1/organizations/${ORG}/shipments`, {    receiver: {      first_name: order.firstName,      last_name: order.lastName,      email: order.email,      phone: order.phone,      address: {        street: order.street,        building_number: order.building,        city: order.city,        post_code: order.postCode,        country_code: 'PL',      },    },    parcels: [{      dimensions: { length: '300', width: '200', height: '100', unit: 'mm' },      weight: { amount: '2', unit: 'kg' },    }],    service: 'inpost_courier_standard',    reference: order.ref,  });
shipx.tsZmiana w pliku: 6 linii
export async function waitForConfirmed(id: number): Promise<Shipment> {  for (let attempt = 0; attempt < 30; attempt++) {    const shipment = await shipx<Shipment>('GET', `/v1/shipments/${id}`);    if (shipment.status === 'confirmed' || shipment.tracking_number) return shipment;    await new Promise(resolve => setTimeout(resolve, 2000));  }  throw new Error(`Przesyłka ${id} nie została opłacona`);}
shipx.tsZmiana w pliku: 8 linii
import { writeFile } from 'node:fs/promises'; export async function label(id: number): Promise<Buffer> {  const response = await fetch(`${API}/v1/shipments/${id}/label?format=pdf&type=A6`, {    headers: { Authorization: `Bearer ${TOKEN}` },  });  if (!response.ok) throw new Error(`Etykieta ${id}: ${response.status} ${await response.text()}`);  return Buffer.from(await response.arrayBuffer());} const confirmed = await waitForConfirmed(created.id);await writeFile(`etykieta-${confirmed.tracking_number}.pdf`, await label(confirmed.id));
webhook.tsNowy plik, 15 linii
import express from 'express'; const app = express(); // InPost wymaga, żeby adres odpowiadał 200 na GET.app.get('/inpost/webhook', (_req, res) => res.sendStatus(200)); app.post('/inpost/webhook', express.json(), async (req, res) => {  res.sendStatus(200);  // odpowiedz od razu, przetwarzaj po  const { event, payload } = req.body as { event: string; payload: { shipment_id: number } };  if (event !== 'shipment_status_changed' && event !== 'shipment_confirmed') return;  // Webhook nie ma podpisu: traktuj go jak sygnał i pobierz przesyłkę z API.  const shipment = await shipx<Shipment>('GET', `/v1/shipments/${payload.shipment_id}`);  await saveShipmentStatus(shipment.id, shipment.status, shipment.tracking_number);});
statusy.tsNowy plik, 5 linii
const response = await fetch(`${API}/v1/statuses?lang=pl_PL`);  // bez tokenuconst { items } = await response.json() as { items: { name: string; title: string; description: string }[] };const titles = new Map(items.map(status => [status.name, status.title])); titles.get('ready_to_pickup');  // "Umieszczona w automacie Paczkomat (odbiorczym)."

Sprawdź, czy działa

Klienta uruchomiliśmy 6 października 2026 r. na sandboksie. Lista statusów przyszła bez tokenu i miała 53 pozycje, a utworzenie przesyłki bez tokenu zwróciło 401 z kodem token_invalid. To potwierdza adres, format żądań i obsługę błędów. Z tokenem z Managera Paczek dostaniesz w tym miejscu przesyłkę w statusie created.

terminal6 października 2026Sandbox ShipX, Node.js i tsx
$ npx tsx shipx.ts
statuses 53
created: Przesyłka utworzona.
offers_prepared: Przygotowano oferty.
offer_selected: Oferta wybrana.
ShipX POST /v1/organizations/1/shipments: 401
{"error":"token_invalid","message":"Token is missing or invalid."}
Adres, format żądań i obsługa błędów potwierdzone bez tokenu
Co znaczą linie wyniku
  1. linie 2Lista statusów

    Słownik statusów przychodzi bez tokenu i ma 53 pozycje.

  2. linie 3-5Pierwsze statusy przesyłki

    created, offers_prepared i offer_selected: InPost przygotowuje i wybiera ofertę, zanim przesyłka dostanie status confirmed.

  3. linie 6-7Przesyłka bez tokenu

    Bez tokenu ShipX zwraca 401 z kodem token_invalid. Z tokenem z Managera Paczek dostaniesz tu przesyłkę w statusie created.

Po czym poznasz, że działa u Ciebie

  • GET /v1/organizations/:id zwraca Twoją organizację i jej usługi.
  • Nowa przesyłka ma status created, a po chwili confirmed i tracking_number.
  • Etykieta pobiera się jako PDF dopiero po confirmed.
  • Webhook odpowiada 200 na GET i dostaje shipment_status_changed.

Najczęstsze błędy

Ta sama droga co w "Co zbudujesz" na górze strony, z miejscami, w których praca staje najczęściej. Przy każdym błędzie: objaw, przyczyna i co zrobić.

Gdzie staje i dlaczego

Przesyłka

Zamówienie

POST z odbiorcą, gabarytem i usługą.

  • Zatrzymuje się: 401 token_invalid

    Brak tokenu, token produkcyjny na sandboksie albo odwrotnie. Co zrobić: Sandbox ma osobne konto i osobny token z sandbox-manager.paczkomaty.pl.

  • Zatrzymuje się: Błąd walidacji przy przesyłce kurierskiej z pobraniem

    Brak ubezpieczenia, które usługi kurierskie wymagają przy pobraniu. Co zrobić: Dodaj insurance z kwotą i walutą.

Oferta i opłata

ShipX

Asynchronicznie: created do confirmed.

  • Zatrzymuje się: Przesyłka do Paczkomatu nie przechodzi na sandboksie

    Brak środków na koncie testowym. Co zrobić: Doładuj konto wirtualnie w zakładce Płatności.

Etykieta

Magazyn

PDF, ZPL albo EPL od statusu confirmed.

  • Zatrzymuje się: invalid_action przy pobieraniu etykiety

    Przesyłka nie jest jeszcze opłacona, ma status wcześniejszy niż confirmed. Co zrobić: Poczekaj na confirmed albo na webhook shipment_confirmed.

Statusy

Klient

Webhook przy każdej zmianie.

  • Zatrzymuje się: InPost nie zapisuje adresu webhooka

    Adres nie odpowiada 200 na GET albo jest nieosiągalny. Co zrobić: Dodaj obsługę GET zwracającą 200 i sprawdź, że adres działa z internetu.

Jak to robimy u siebie

Ten sam temat w kodzie, który utrzymujemy: Flaem.

Dziennik integracji InPost z webhookami i dozwoloną pulą adresów nadawcy

Webhook to sygnał, nie dowód

W Flaem przyjmujemy webhooki ze sklepów Tebex i CraftingStore. Oba podpisują treść HMAC-SHA256, więc najpierw liczymy oczekiwany podpis i porównujemy go w czasie stałym, zanim cokolwiek trafi do kolejki.

InPost webhooków nie podpisuje, więc tej funkcji nie da się tu użyć. Ta sama zasada działa jednak inaczej: ograniczasz adres do puli 91.216.25.0/24, a stan przesyłki bierzesz z API, nie z treści zdarzenia.

frozcore/src/store_integrations/webhooks.rsrust, 20 linii
pub fn verify(    provider: &str,    secret: &str,    body: &[u8],    presented: &str,) -> Result<(), WebhookError> {    let expected = expected_signature(provider, secret, body);    let presented = presented.trim().to_ascii_lowercase();    if expected.len() == presented.len()        && expected            .bytes()            .zip(presented.bytes())            .fold(0u8, |acc, (a, b)| acc | (a ^ b))            == 0    {        Ok(())    } else {        Err(WebhookError::BadSignature)    }}

Weryfikacja podpisu webhooka w Flaem, porównanie w czasie stałym.

Powiązane strony i źródła

Usługa, narzędzia i hasła, do których odsyła ten poradnik, i kolejne poradniki.

Oficjalna dokumentacja

Wolisz, żeby zrobił to ktoś, kto już to robił?

Opisz swój system. Odpiszemy w godzinę z planem integracji i wyceną.

Pytania