A fully typed TypeScript/Node client for the InPost ShipX API: create and track parcel-locker and courier shipments, search Paczkomat/PaczkoPunkt points, and download shipping labels.
- Strict TypeScript, ships dual ESM + CJS builds with
.d.ts - Zero runtime dependencies — uses the global
fetch(Node.js 20+), or an injected implementation viaShipXClientConfig.fetch - Tree-shakeable; import the whole package or a single subpath
(
/shipments,/points,/tracking,/labels) - Bearer token always sent in the
Authorizationheader, never in the URL, and never logged - Every request has a configurable timeout; every failure mode (network
error, timeout, non-2xx status, malformed JSON) is normalized to a single
ShipXErrortype, withstatus/detailspopulated from the ShipX API's own{ status, error, message, details }error body when available - Zero real network calls in tests — the test suite mocks
fetch, so CI runs deterministically offline
npm install @m8t-jacob/inpost-shipximport { ShipXClient } from '@m8t-jacob/inpost-shipx';
const client = new ShipXClient({
token: process.env.SHIPX_TOKEN!,
organizationId: process.env.SHIPX_ORGANIZATION_ID, // required for createShipment/getShipments
sandbox: true, // omit or set to false for production
});
// Search Paczkomat points (public endpoint, no valid token strictly required)
const points = await client.getPoints({ city: 'Warszawa', type: 'parcel_locker' });
console.log(points.count, points.items[0]?.name);
// Create a locker shipment
const shipment = await client.createShipment({
receiver: { email: 'jan@example.com', phone: '500600700' },
parcels: [{ template: 'small' }],
service: 'inpost_locker_standard',
custom_attributes: { target_point: 'WAW198M' },
});
// Track it
const tracking = await client.trackParcel(shipment.tracking_number!);
// Download the label as a PDF buffer
const label = await client.getLabel(shipment.id, { format: 'pdf', type: 'a6' });
await fs.promises.writeFile('label.pdf', Buffer.from(label));You can also import a subpath if you only need one module, which keeps
bundlers from pulling in the others (note these export plain functions
taking a ShipXRequestContext, not a pre-bound client — see
Advanced: calling module functions directly):
import { getPoints } from '@m8t-jacob/inpost-shipx/points';
import { trackParcel } from '@m8t-jacob/inpost-shipx/tracking';new ShipXClient({
token: string;
organizationId?: string | number; // required for createShipment/getShipments
sandbox?: boolean; // default: false (production)
timeoutMs?: number; // default: 15000
fetch?: typeof fetch; // default: global fetch
})| Method | Description |
|---|---|
createShipment(payload): Promise<Shipment> |
Creates a shipment under organizationId. Requires organizationId. |
getShipment(id): Promise<Shipment> |
Fetches a single shipment by id. Not organization-scoped. |
getShipments(params?): Promise<PaginatedResult<Shipment>> |
Lists shipments under organizationId, filterable by status/tracking_number/reference. |
getPoints(params?): Promise<PaginatedResult<Point>> |
Searches Paczkomat/PaczkoPunkt points by type/city/postCode or geo radius. |
getPoint(name): Promise<Point> |
Fetches a single point by name, e.g. 'WAW198M'. |
trackParcel(trackingNumber): Promise<TrackingResult> |
Public parcel tracking. |
getLabel(shipmentId, options?): Promise<ArrayBuffer> |
Downloads the shipping label as a binary buffer (PDF or ZPL). |
interface CreateShipmentPayload {
receiver: Receiver; // { first_name?, last_name?, company_name?, email?, phone?, address? }
sender?: Sender;
parcels: Parcel[]; // [{ template: 'small' }] or [{ dimensions: {...}, weight: {...} }]
service: ShipmentService; // e.g. 'inpost_locker_standard', 'inpost_courier_standard'
reference?: string;
comments?: string;
custom_attributes?: CustomAttributes; // e.g. { target_point: 'WAW198M' } for locker services
additional_services?: string[];
insurance?: { amount: number; currency?: string };
cod?: { amount: number; currency?: string };
}createShipment validates that receiver, at least one parcels entry,
and service are present before making a network call, throwing
ShipXError immediately otherwise. getShipments accepts
{ status?, tracking_number?, reference?, page?, per_page? } and returns a
PaginatedResult<Shipment> (see Pagination below).
await client.getPoints({
type: 'parcel_locker',
city: 'Warszawa',
postCode: '01-494',
relativePoint: { latitude: 52.2297, longitude: 21.0122 },
maxDistance: 1000, // meters
page: 1,
perPage: 25,
});relativePoint + maxDistance map to the API's relative_point
("lat,lng") and max_distance query parameters for geo-radius search.
await client.trackParcel('681646828000000000000000');Confirmed to work without an Authorization header against the
production API (see Endpoints and versions confirmed
below) — this client still sends the configured token for consistency.
const pdf = await client.getLabel(shipmentId); // defaults to { format: 'pdf' }
const zpl = await client.getLabel(shipmentId, { format: 'zpl', type: 'a6' });Returns an ArrayBuffer — the response is never parsed as JSON, so a
successful call always yields the raw label bytes regardless of format.
List endpoints (getShipments, getPoints) return a { href, count, page, per_page, total_pages, items } response from the API. This package
normalizes that to:
interface PaginatedResult<T> {
items: T[];
count: number;
page: number;
perPage: number;
totalPages?: number;
href?: string;
}There is no built-in "fetch all pages" helper in this release — call
getShipments/getPoints again with an incremented page/page param
using totalPages as the stop condition. See
GOOD_FIRST_ISSUES.md for a tracked helper idea.
Every operation throws ShipXError for invalid input (checked before any
network call), a non-2xx response, a network error, a timeout, or malformed
JSON:
export class ShipXError extends Error {
readonly status?: number; // HTTP status, when applicable
readonly details?: unknown; // the API's `details` field, or the full error body as a fallback
}try {
await client.createShipment({ /* missing target_point */ });
} catch (error) {
if (error instanceof ShipXError) {
console.error(error.status, error.message, error.details);
}
}Every ShipXClient method delegates to a plain function exported from its
module (createShipment, getShipment, getShipments, getPoints,
getPoint, trackParcel, getLabel), taking a ShipXRequestContext as
the first argument instead of the ShipXClient class. This is mostly
useful for testing or building your own thin wrapper; most users should
just use ShipXClient.
This package targets the ShipX API (v1) at:
- Production:
https://api-shipx-pl.easypack24.net/v1 - Sandbox:
https://sandbox-api-shipx-pl.easypack24.net/v1
The following were confirmed by hand (curl) against the production
API on 2026-07-13:
| Endpoint | Auth required? | Confirmed response shape |
|---|---|---|
GET /points |
No (200 without a token) |
{ href, count, page, per_page, total_pages, items: Point[] } |
GET /points/{name} |
No (200 without a token) |
A single Point object |
GET /tracking/{trackingNumber} |
No (404 without a token, not 401) |
{ trackingNumber, message } for an unknown number; success shape not independently confirmed (see GOOD_FIRST_ISSUES.md) |
GET /shipments/{id} |
Yes (401 token_invalid without a token) |
Not independently confirmed for a real shipment (requires a valid token) |
GET /shipments/{id}/label |
Yes (401 token_invalid without a token) |
Not independently confirmed for a real shipment |
POST /organizations/{organizationId}/shipments |
Yes (401 token_invalid without a token) |
Not independently confirmed for a real organization |
The 401/404 error body shape confirmed above —
{"status":401,"error":"token_invalid","message":"Token is missing or invalid.","details":{}} —
is what ShipXError.status/.details are populated from. If InPost changes
any of these endpoints, please open an issue.
@m8t-jacob/inpost-shipx to w pełni typowany klient TS/Node dla API
InPost ShipX: tworzenie i śledzenie przesyłek (paczkomatowych i
kurierskich), wyszukiwanie punktów Paczkomat/PaczkoPunkt oraz pobieranie
etykiet nadania. Pakiet nie ma żadnych zależności uruchomieniowych — korzysta
z globalnego fetch (Node.js 20+). Token API jest zawsze wysyłany w
nagłówku Authorization, nigdy w adresie URL i nigdy nie jest logowany.
Każde żądanie ma konfigurowalny timeout, a każdy rodzaj błędu (błąd sieci,
timeout, status inny niż 2xx, niepoprawny JSON) jest ujednolicony do jednego
typu ShipXError z polami status/details wypełnionymi na podstawie
własnego formatu błędów ShipX ({ status, error, message, details }).
Testy nie wykonują żadnych realnych zapytań sieciowych (mockowany fetch),
dzięki czemu CI działa deterministycznie offline.
Contributions are welcome! See CONTRIBUTING.md for the
development workflow and GOOD_FIRST_ISSUES.md for
ideas if you're looking for a place to start. This project follows the
Contributor Covenant.
MIT © 2026 Jakub Jagiełło