postnl-client 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +206 -0
- package/dist/index.cjs +2017 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +12752 -0
- package/dist/index.d.ts +12752 -0
- package/dist/index.js +1951 -0
- package/dist/index.js.map +1 -0
- package/package.json +57 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 NickTacke
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# postnl-client
|
|
2
|
+
|
|
3
|
+
A well-typed TypeScript client for the PostNL eCommerce API: V4-first, with the legacy endpoints supported where no V4 equivalent exists.
|
|
4
|
+
|
|
5
|
+
> Unofficial. Not affiliated with or endorsed by PostNL. You need your own PostNL API key.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
bun add postnl-client
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install postnl-client
|
|
15
|
+
pnpm add postnl-client
|
|
16
|
+
yarn add postnl-client
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Requires Node 18+ (or Bun / Deno / any runtime with global `fetch` and `atob`). Ships ESM + CJS + types.
|
|
20
|
+
|
|
21
|
+
## Quick start
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import { writeFileSync } from "node:fs";
|
|
25
|
+
import { PostNLClient } from "postnl-client";
|
|
26
|
+
|
|
27
|
+
const client = new PostNLClient({
|
|
28
|
+
apiKey: process.env.POSTNL_APIKEY!,
|
|
29
|
+
environment: "sandbox", // omit for "production"
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// create + confirm a shipment in one call, then decode the label
|
|
33
|
+
const result = await client.shipping.create({
|
|
34
|
+
shipmentType: "parcel",
|
|
35
|
+
sender: {
|
|
36
|
+
customerNumber: "11223344",
|
|
37
|
+
customerCode: "DEVC",
|
|
38
|
+
address: { countryIso: "NL", city: "Hoofddorp", street: "Siriusdreef", houseNumber: "42", postalCode: "2132WT" },
|
|
39
|
+
},
|
|
40
|
+
receiver: {
|
|
41
|
+
address: { countryIso: "NL", city: "Utrecht", street: "Stationsplein", houseNumber: "1", postalCode: "3511ED" },
|
|
42
|
+
contact: { email: "buyer@example.com", firstName: "Jane", lastName: "Doe" },
|
|
43
|
+
},
|
|
44
|
+
labelSettings: { outputType: "pdf" },
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const item = result.items[0];
|
|
48
|
+
console.log(item?.barcode);
|
|
49
|
+
const label = item?.labels?.[0];
|
|
50
|
+
// label.bytes() is a Uint8Array; works in node, bun, and deno
|
|
51
|
+
if (label) writeFileSync("label.pdf", label.bytes());
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
See [`examples/`](./examples) for runnable scripts (`create-shipment.ts`, `track.ts`, `nearest-locations.ts`).
|
|
55
|
+
|
|
56
|
+
## The V4-first surface
|
|
57
|
+
|
|
58
|
+
V4 is the default everywhere a V4 endpoint exists. Older operations that PostNL never migrated (tracking, delivery options, locations, checkout, address) are exposed directly under their namespace. Where both a V4 and a legacy variant exist, the legacy one lives under `.legacy`.
|
|
59
|
+
|
|
60
|
+
| Call | Method + endpoint | Notes |
|
|
61
|
+
| --- | --- | --- |
|
|
62
|
+
| `client.barcode.generate(input)` | `POST /shipment/delivery/v4/barcode` | V4 |
|
|
63
|
+
| `client.barcode.legacy.generate(input)` | `GET /shipment/v1_1/barcode` | legacy |
|
|
64
|
+
| `client.shipping.create(input)` | `POST /shipment/delivery/v4/labelconfirm` | V4 (label + confirm) |
|
|
65
|
+
| `client.shipping.label(input)` | `POST /shipment/delivery/v4/label` | V4 (label only) |
|
|
66
|
+
| `client.shipping.confirm(input)` | `POST /shipment/delivery/v4/confirm` | V4 |
|
|
67
|
+
| `client.shipping.legacy.label(input, { confirm? })` | `POST /shipment/v2_2/label` | legacy; `confirm` defaults `true` |
|
|
68
|
+
| `client.shipping.legacy.confirm(input)` | `POST /shipment/v2/confirm` | legacy |
|
|
69
|
+
| `client.return.generate(input)` | `POST /shipment/delivery/v4/return/generate` | V4 |
|
|
70
|
+
| `client.tracking.byBarcode(barcode, opts?)` | `GET /shipment/v2/status/barcode/{barcode}` | legacy-only |
|
|
71
|
+
| `client.tracking.byReference(referenceId, opts)` | `GET /shipment/v2/status/reference/{referenceId}` | legacy-only |
|
|
72
|
+
| `client.tracking.signature(barcode)` | `GET /shipment/v2/status/signature/{barcode}` | legacy-only |
|
|
73
|
+
| `client.tracking.updated(customerNumber, opts)` | `GET /shipment/v2/status/{customernumber}/updatedshipments` | legacy-only |
|
|
74
|
+
| `client.deliveryDate.calculate(input)` | `GET /shipment/v2_2/calculate/date/delivery` | legacy-only |
|
|
75
|
+
| `client.deliveryDate.sentDate(input)` | `GET /shipment/v2_2/calculate/date/shipping` | legacy-only |
|
|
76
|
+
| `client.timeframe.get(input)` | `GET /shipment/v2_1/calculate/timeframes` | legacy-only |
|
|
77
|
+
| `client.location.nearest(input)` | `GET /shipment/v2_1/locations/nearest` | legacy-only |
|
|
78
|
+
| `client.location.nearestByGeocode(input)` | `GET /shipment/v2_1/locations/nearest/geocode` | legacy-only |
|
|
79
|
+
| `client.location.area(input)` | `GET /shipment/v2_1/locations/area` | legacy-only |
|
|
80
|
+
| `client.location.lookup(input)` | `GET /shipment/v2_1/locations/lookup` | legacy-only |
|
|
81
|
+
| `client.checkout.get(input)` | `POST /shipment/v1/checkout` | legacy-only |
|
|
82
|
+
| `client.address.check(input)` | `GET /shipment/checkout/v1/postalcodecheck` | legacy-only, **production-only** |
|
|
83
|
+
| `client.request(args)` | any | raw escape hatch (see below) |
|
|
84
|
+
|
|
85
|
+
## Environments
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
new PostNLClient({ apiKey, environment: "sandbox" }); // https://api-sandbox.postnl.nl
|
|
89
|
+
new PostNLClient({ apiKey, environment: "production" }); // https://api.postnl.nl (default)
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
`address.check` is production-only: calling it on `sandbox` rejects with a `PostNLError` before any request is sent.
|
|
93
|
+
|
|
94
|
+
## Authentication
|
|
95
|
+
|
|
96
|
+
A single API key, sent as the `apikey` header on every request. Get one from the [PostNL developer portal](https://developer.postnl.nl/).
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
new PostNLClient({ apiKey: process.env.POSTNL_APIKEY! });
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Error handling
|
|
103
|
+
|
|
104
|
+
Failed requests reject with a typed error. `PostNLApiError` is the base for all HTTP error responses and carries `status`, `code`, `detail`, and `raw`. Subclasses let you branch on the failure kind:
|
|
105
|
+
|
|
106
|
+
| Class | When |
|
|
107
|
+
| --- | --- |
|
|
108
|
+
| `PostNLAuthError` | 401 (bad / missing API key) |
|
|
109
|
+
| `PostNLRateLimitError` | 429 (has `retryAfter` in seconds when sent) |
|
|
110
|
+
| `PostNLBadRequestError` | 400 (validation / business errors) |
|
|
111
|
+
| `PostNLMethodNotAllowedError` | 405 |
|
|
112
|
+
| `PostNLServerError` | 5xx |
|
|
113
|
+
| `PostNLApiError` | any other non-2xx |
|
|
114
|
+
|
|
115
|
+
Two non-HTTP errors extend `PostNLError` directly: `PostNLValidationError` (response failed schema validation) and `PostNLTimeoutError`.
|
|
116
|
+
|
|
117
|
+
```ts
|
|
118
|
+
import { PostNLClient, PostNLApiError, PostNLRateLimitError } from "postnl-client";
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
await client.barcode.generate(/* ... */);
|
|
122
|
+
} catch (err) {
|
|
123
|
+
if (err instanceof PostNLRateLimitError) {
|
|
124
|
+
console.warn(`rate limited, retry after ${err.retryAfter}s`);
|
|
125
|
+
} else if (err instanceof PostNLApiError) {
|
|
126
|
+
console.error(`postnl ${err.status} (${err.code ?? "?"}): ${err.message}`);
|
|
127
|
+
} else {
|
|
128
|
+
throw err;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Labels and signatures
|
|
134
|
+
|
|
135
|
+
Label and signature payloads come back base64-encoded. Each decodes to a small helper object:
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
const label = result.items[0]?.labels?.[0];
|
|
139
|
+
if (label) {
|
|
140
|
+
label.base64; // raw base64 string
|
|
141
|
+
label.contentType; // e.g. "application/pdf" (derived from the output type)
|
|
142
|
+
label.bytes(); // Uint8Array, ready to write to disk or stream
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
`client.tracking.signature(barcode)` returns the signature image the same way (`signature.signatureImage.bytes()`).
|
|
147
|
+
|
|
148
|
+
## Configuration
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
new PostNLClient({
|
|
152
|
+
apiKey,
|
|
153
|
+
environment: "production",
|
|
154
|
+
timeoutMs: 60_000, // per-attempt timeout (default 60s) -> PostNLTimeoutError
|
|
155
|
+
fetch: customFetch, // inject your own fetch (default: global fetch)
|
|
156
|
+
retry: {
|
|
157
|
+
maxRetries: 3, // default 3
|
|
158
|
+
backoffFactor: 2, // default 2 (exponential)
|
|
159
|
+
retryMethods: ["GET", "PUT"], // only idempotent methods by default
|
|
160
|
+
retryStatuses: [408, 413, 429, 500, 502, 503, 504, 521, 522, 524],
|
|
161
|
+
},
|
|
162
|
+
hooks: {
|
|
163
|
+
onRequest: (req) => console.debug(req.method, req.url),
|
|
164
|
+
onResponse: (res) => console.debug(res.status),
|
|
165
|
+
onError: (err) => console.error(err),
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Retries apply only to the methods in `retryMethods` (idempotent by default, so POSTs are never silently retried) and only for the listed statuses and transient network/timeout failures.
|
|
171
|
+
|
|
172
|
+
## Wire quirks normalized for you
|
|
173
|
+
|
|
174
|
+
The PostNL API has an inconsistent wire format. This client absorbs that so you work with clean, typed, camelCase objects:
|
|
175
|
+
|
|
176
|
+
- numbers and booleans returned as strings (`"2"`, `"true"`) are coerced to real `number` / `boolean`
|
|
177
|
+
- single-value-or-array fields are always given to you as arrays
|
|
178
|
+
- `{ string: ... }` wrapper objects are unwrapped
|
|
179
|
+
- `dd-MM-yyyy[ HH:mm:ss]` dates are parsed to `Date`; `Date` inputs are formatted back to the wire format
|
|
180
|
+
- PascalCase wire keys become camelCase
|
|
181
|
+
|
|
182
|
+
## Raw request escape hatch
|
|
183
|
+
|
|
184
|
+
For anything not yet wrapped, call the transport directly. Auth, retry, timeout, and hooks still apply; pass an optional Zod `schema` to validate/parse the response.
|
|
185
|
+
|
|
186
|
+
```ts
|
|
187
|
+
const data = await client.request({
|
|
188
|
+
family: "legacy",
|
|
189
|
+
method: "GET",
|
|
190
|
+
path: "/shipment/v2/status/barcode/{barcode}",
|
|
191
|
+
pathParams: { barcode: "3SDEVC1234567" },
|
|
192
|
+
query: { detail: "true" },
|
|
193
|
+
});
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## Releasing (maintainers)
|
|
197
|
+
|
|
198
|
+
Releases are driven by [Changesets](https://github.com/changesets/changesets) and the `release` GitHub Actions workflow, which publishes via npm [trusted publishing (OIDC)](https://docs.npmjs.com/trusted-publishers) — no `NPM_TOKEN` secret required.
|
|
199
|
+
|
|
200
|
+
1. On npmjs.com, configure a trusted publisher for the package: org/user `NickTacke`, repository `postnl-client`, workflow `release.yml`, action `npm publish`.
|
|
201
|
+
2. Merging a PR with a changeset to `main` opens an auto-generated "Version Packages" PR.
|
|
202
|
+
3. Merging that "Version Packages" PR versions the package and publishes it to npm via short-lived OIDC credentials (with provenance).
|
|
203
|
+
|
|
204
|
+
## License
|
|
205
|
+
|
|
206
|
+
MIT
|