delivery-tracker is delivery tracking library for Node.js
Status reflects an endpoint probe run on 2026-08-05 — see Courier status.
| Name | Contributor | Link | Status |
|---|---|---|---|
| Korea Post | @egg- | http://www.koreapost.go.kr/ | reachable |
| Australia Post | @egg- | https://auspost.com.au/ | broken |
| Pantos | @egg- | http://www.epantos.com/ | reachable |
| Rincos | @egg- | http://www.rincos.co.kr/ | broken |
| CJ Korea Express (Korea) | @egg- | http://cjkoreaexpress.co.kr/ (https://www.doortodoor.co.kr) | verified |
| POS Laju | @egg- | http://www.poslaju.com.my | broken |
| EFS | @egg- | http://efs.asia/ | reachable |
| TNT | @egg- | https://www.tnt.com | reachable |
| CESCO | @egg- | https://www.cesco-logistics.com/ | reachable |
| XPOST | @egg- | https://www.xpost.ph/ | broken |
| SICEPAT | @egg- | http://sicepat.com/ | needs API key |
| eParcel | @egg- | https://eparcel.kr/ | reachable |
| LBC | @egg- | https://www.lbcexpress.com/ | reachable |
| J&T (PH) | @egg- | https://www.jtexpress.ph/ | reachable |
| DHL | @carstenschwede | https://www.dhl.com/ | needs API key |
| Canada Post | @egg- | https://www.canadapost-postescanada.ca/ | verified |
Most couriers here are HTML scrapers pointed at pages that have since been rewritten.
The test suite replays responses recorded in test/fixtures, so a green build says the
parser still handles the recorded page — not that the courier still works. Seven of the
recordings date from 2017.
Statuses, most to least trustworthy:
- verified — traced end to end against a real shipment:
cjkoreaexpressandcanadapost. Both were found broken this way and fixed. - needs API key —
dhlanswered401andsicepat403to a dummy key, which is what a correctly wired client should get. Neither has been tried with a real key. - reachable — a probe with a dummy number got a response and no sign of blocking.
This is weak evidence: for the multi-step couriers (
pantos,jnt,lbc) the probe only reaches a landing page, and a page that answers200may still be an empty shell that loads its data from somewhere else. Confirming any of these needs a real tracking number. - broken — the host answers but the endpoint does not:
404forauspost,rincosandxpost, andposlajunow redirects to the pos.com.my home page. These are fixture problems; re-record a live response intotest/fixtures/<code>-<number>and adjust the parser until the test passes.
Ten couriers were dropped in 3.0.0. Five had a hostname that no longer resolves. Five —
usps, fedex, ups, paxel and royalmail — refuse automated requests. This library
scrapes what a courier serves to an ordinary client; where a courier has decided not to
serve that, the answer is its official API or a tracking aggregator, not a workaround.
The parsers remain in git history.
A note on how those were told apart, because it is easy to get wrong in both directions.
A first request returning 200 proves very little. ups served a normal page and then
refused the API call behind it. royalmail served a 175KB page that turned out to be a
shell, while the endpoint holding the actual data simply never answered. In the other
direction canadapost returned 403 and looked blocked, but was only rejecting a
request that did not match the shape its own page sends, and it works again. The only
reliable test is a real tracking number.
Requires Node.js 20 or later. The package is written in TypeScript and ships its own type declarations.
It is published as ESM, so import is the supported form. require() also works on Node
versions that can require an ES module — confirmed on 20.20 and 22.17, while 20.10 fails
with ERR_REQUIRE_ESM. Use import if you want it to work everywhere.
$ npm install delivery-trackertrace() returns a promise. It resolves with the tracking result, or rejects with a
TrackerError carrying a code from ERROR.
import { COURIER, courier } from 'delivery-tracker'
const koreapost = courier(COURIER.KOREAPOST.CODE)
const result = await koreapost.trace('TRACE_NUMBER')
console.log(result.status, result.checkpoints.length)Couriers that need credentials take them as the second argument:
const sicepat = courier(COURIER.SICEPAT.CODE, { apikey: 'YOUR_API_KEY' })Handling failures:
import { ERROR, TrackerError, COURIER, courier } from 'delivery-tracker'
try {
await courier(COURIER.KOREAPOST.CODE).trace('BADNUMBER')
} catch (err) {
if (err instanceof TrackerError && err.code === ERROR.INVALID_NUMBER_LENGTH) {
// ...
}
}Types come with the package — no @types/ install. Courier codes are checked at compile
time, so a typo is a build error rather than a runtime throw.
import { COURIER, courier, type TraceResult } from 'delivery-tracker'
const client = courier(COURIER.SICEPAT.CODE, { apikey: 'YOUR_API_KEY' })
const result: TraceResult = await client.trace('TRACE_NUMBER')$ npm install -g delivery-tracker
$ delivery-tracker -h
Usage: delivery-tracker [options] <tracecode>
Options:
-c, --courier <courier> Courier Namespace
-k, --apikey <apikey> API KEY
-h, --help display help for command
$ delivery-tracker -c KOREAPOST EBXXXXXXXXXKR| Attribute | Type | Description |
|---|---|---|
| courier | Courier Object | courier information |
| number | String | tracking number |
| status | String | delivery status |
| checkpoints | Array of Checkpoint Object | Array of the checkpoint information. |
| Attribute | Type | Description |
|---|---|---|
| code | String | Unique code of courier. |
| name | String | Courier name |
| Attribute | Type | Description |
|---|---|---|
| courier | Courier Object | courier information |
| location | String | Location info of the checkpoint provided by the courier. |
| message | String | Checkpoint message |
| time | String | The date and time of the checkpoint provided by the courier. The values can be: Empty string, YYYY-MM-DD, YYYY-MM-DDTHH:mm:ss YYYY-MM-DDTHH:mm:ss+Timezone |
All three are named exports: import { COURIER, STATUS, ERROR } from 'delivery-tracker'.
COURIER.{NAMESPACE}
| NAMESPACE | CODE | NAME |
|---|---|---|
| KOREAPOST | koreapost | Korea Post |
| AUSPOST | auspost | Australia Post |
| PANTOS | pantos | Pantos |
| RINCOS | rincos | RINCOS |
| CJKOREAEXPRESS | cjkoreaexpress | CJ Korea Express |
| POSLAJU | poslaju | POS Laju |
| EFS | efs | EFS |
| TNT | tnt | TNT |
| CESCO | cesco | CESCO |
| XPOST | xpost | XPOST |
| SICEPAT | sicepat | SICEPAT |
| EPARCEL | eparcel | eParcel |
| LBC | lbc | LBC |
| JNT | jnt | J&T |
| DHL | dhl | DHL |
| CANADAPOST | canadapost | Canada Post |
STATUS.{CODE}
| Code | Value | Description |
|---|---|---|
| INFO_RECEIVED | InfoReceived | The carrier received a request from the shipper and wants to start shipping. |
| PENDING | Pending | New pending shipment to track or a new shipment without tracking information added. |
| IN_TRANSIT | InTransit | The carrier has received or received the carrier. Shipment is in progress. |
| DELIVERED | Delivered | The shipment was successfully delivered. |
| RETURNED | Returned | The shipment was returned. |
| EXCEPTION | Exception | Custom hold, undeliverable, shipper has shipped or shipped an exception. |
| FAIL_ATTEMPT | FailAttempt | The courier tried to send but failed, but usually reminds and tries again. |
ERROR.{CODE} — the value a rejected trace() carries on TrackerError.code.
| Code | Value | Description |
|---|---|---|
| UNKNOWN | -1 | Unknown error |
| INVALID_NUMBER | 10 | invalid trace number. |
| INVALID_NUMBER_LENGTH | 11 | invalid trace number. |
| INVALID_NUMBER_HEADER | 12 | invalid trace number. |
| INVALID_NUMBER_COUNTRY | 13 | invalid trace number. |
| NOT_SUPPORT_SHIPMENT | 20 | shipment does not support. |
| SEARCH_AGAIN | 21 | working on it. Please search it again. |
| REQUIRED_APIKEY | 30 | required apikey. |
| SERVER_ERROR | 500 | upstream server error |
// KOREAPOST
{
"courier": {
"code": "koreapost",
"name": "Korea Post"
},
"number": "EBCOMPLETE0KR",
"status": "Delivered",
"checkpoints": [
{
"courier": {
"code": "koreapost",
"name": "Korea Post"
},
"location": "MY4332",
"message": "Delivery complete\nRecipient : K*NG()\nResult : Delivery complete",
"time": "2016-07-04T11:40:00"
},
// ...
]
}
// FEDEX
{
"courier": {
"code": "fedex",
"name": "FedEx"
},
"number": "DELIVEREDNUM",
"status": "Delivered",
"checkpoints": [
{
"courier": {
"code": "fedex",
"name": "FedEx"
},
"location": "SOUTH JORDAN, UT",
"message": "Package delivered by U.S. Postal Service to addressee",
"status": "Delivered",
"time": "2016-12-14T13:17:00-07:00"
},
// ...
]
}
// PANTOS
{
"courier": {
"code": "pantos",
"name": "Pantos"
},
"number": "DELIVEREDNUM-AUSPOST",
"status": "Delivered",
"checkpoints": [
{
"courier": {
"code": "auspost",
"name": "Australia Post"
},
"location": "Canning Vale, WA",
"message": "Delivered",
"status": "Delivered",
"time": "2017-01-03T15:24:00+08:00"
},
// ...
{
"courier": {
"code": "pantos",
"name": "Pantos"
},
"location": "KRICN",
"message": "Pick-Up (Pick-Up)",
"status": "InfoReceived",
"time": "2016-12-20T11:25"
}
]
}Lint (Biome) + typecheck (tsc) + test (mocha):
$ npm testIndividually:
$ npm run lint # biome check
$ npm run lint:fix # biome check --write
$ npm run typecheck # tsc --noEmit, covers src and test
$ npm run test:unit # mocha only
$ npm run test:watch
$ npm run build # emit dist/ (js + .d.ts)Tests run straight off the TypeScript sources via tsx, and replay recorded responses
from test/fixtures with nock — nothing hits the network.
- Add an entry to
COURIERinsrc/core.ts. - Add
src/courier/<code>.tsexporting a default factory built withcreateCourier(). - Register the factory in
FACTORIESinsrc/index.ts. - Record a response into
test/fixtures/<code>-<number>and addtest/<code>.test.ts.
These couriers are scraped from pages that change without notice, and the maintainers cannot reproduce a failure without seeing the response the courier actually returned. A report that says only "koreapost is broken" cannot be acted on.
Please include one of the following — the second option if the first is not acceptable to you:
The most useful thing you can send. Pick a shipment that is already delivered and no longer sensitive to you, since anyone reading the issue can look it up.
If you cannot share a number — a tracking number resolves to a delivery address, times and often a recipient name, so treating it as personal data is reasonable — send the raw response instead. It is what the test suite replays, so it is just as useful:
import { writeFileSync } from 'node:fs'
import { COURIER, courier, request } from 'delivery-tracker'
const client = courier(COURIER.KOREAPOST.CODE)
const info = client.trackingInfo('YOUR_NUMBER')
// `data` holds the POST payload for the couriers that use one.
const response = await request({ ...info, form: info.data ?? info.form })
writeFileSync('koreapost-DELIVERED', response.body)Before attaching the file, please redact it:
- Replace every occurrence of the real tracking number with a placeholder that says
what the case is —
DELIVERED,INTRANSIT,INVALIDNUM. Replace it inside the body too, not just in the filename: the parsers read the number back out of the response, so the tests match on it. - Remove recipient and sender names, phone numbers, full addresses and signature images. The parsers only need the status text, location and timestamp.
Name the file <code>-<placeholder>, matching test/fixtures.
- the courier code, and the tracking number's country if the courier serves several
- what you expected and what you got — an error (with its
code), an emptycheckpoints, or wrongtimevalues delivery-trackerand Node.js versions
Bug reports and pull requests are welcome on Github at https://github.com/egg-/delivery-tracker
- Fork it
- Create your feature branch.
- Commit your changes.
- Push to the branch.
- Create a new Pull Request.
See the CHANGELOG.md
delivery-tracker is licensed under the MIT license.