Packy is a high-performance RESTful shipment tracking API and multicarrier tracking API designed to normalize delivery data across 1385+ carriers globally. Built for developers, e-commerce platforms, and logistics providers, it functions as a comprehensive package tracking API and parcel tracking API in a single integration.
Packy Tracking API offers a generous free tier (free package) for developers to start testing and tracking immediately with no credit card required.
Key features include carrier auto-detection, realtime updates via high-frequency webhooks, and unified delivery tracking API endpoints. Automate your post-purchase experience, reduce WISMO (Where Is My Order) tickets, and get normalized JSON status updates for any courier worldwide using our free plan or scalable commercial tiers.
1 credit = 1 unique tracking number.
Once you add a tracking number, that single credit covers the package's entire lifecycle. This includes everything from the moment it's created, through transit, final delivery, potential returns (if things don't go as planned), and even a little extra buffer time afterward—just to ensure you have all the data you need. No hidden fees, no per-request charges.
Every parcel in the system is assigned a status that reflects the latest stage of its journey. As new tracking events arrive, the status is updated automatically. The possible statuses are:
| Status | Code | Meaning |
|---|---|---|
| Awaiting | pending |
Tracking is registered, but no carrier updates have been received yet. |
| Created | create |
A shipping label/order has been created; the parcel is not yet moving. |
| Info Received | info |
The carrier has received shipment information and is preparing to handle the parcel. |
| In Transit | transit |
The parcel is on its way through the carrier's network. |
| Out for Delivery | out-for-delivery |
The parcel is with a courier and scheduled for delivery today. |
| Delivered | delivered |
The parcel has been successfully delivered to the recipient. |
| Failed Attempt | fail |
A delivery attempt was made but was unsuccessful (e.g. recipient unavailable). |
| Exception | exception |
A problem occurred during delivery that requires attention. |
| Cancelled | cancel |
The shipment was cancelled. |
| Lost | lost |
The parcel is reported as lost by the carrier. |
| Returning | return |
The parcel is being returned to the sender. |
| Out for Delivery to Sender | out-for-delivery-sender |
The returned parcel is out for delivery back to the original sender. |
| Delivered to Sender | delivered-sender |
The returned parcel has been delivered back to the sender. |
| Tracking Ended | end |
Tracking for this parcel has been closed; no further updates are expected. |
Note: When a parcel has several events at once, the most significant status is shown (for example, Delivered takes precedence over In Transit).
Returns a paginated list of the account's trackings.
| page | integer >= 1 Default: 1 Page number. |
| limit | integer >= 1 Default: 20 Page size. |
{- "data": [
- {
- "id": "2f8b1c4d5e6f7a8b9c0d1e2f3a4b5c6d",
- "tracking_number": "9400111899562537683144",
- "courier_code": "usps",
- "delivery_status": "transit",
- "updated_at": "2026-05-26T14:30:00Z"
}
], - "meta": {
- "page": 1,
- "limit": 20,
- "total": 1
}
}Creates one or more trackings. The body is either a single object or an array of objects. Up to 40 trackings can be created per request.
Specifying the courier is optional: if courier_code is omitted, the
courier is detected automatically from the tracking number.
This operation consumes account credits.
Behavior depends on ?async:
async=true (default) — trackings are created and refreshed in the
background; the response contains the created trackings without events
yet.async=false — legacy sync mode: polls the carrier first, then
subscribes. One credit is charged per item in the request (including
when courier_code is unknown). Re-tracking an existing number is
allowed. Unknown courier_code returns invalid_courier in errors.
The response includes normalized events and crawler couriers
(with icon URLs) per created item.The response is a {data, errors} envelope:
201 Created — all items were created;207 Multi-Status — partial success (both data and errors present);422 Unprocessable Entity — nothing was created, only errors.| async | boolean Default: true
|
| tracking_number required | string |
| courier_code | string Courier code; if omitted, it is detected automatically. |
{- "tracking_number": "9400111899562537683144",
- "courier_code": "usps"
}{- "data": [
- {
- "id": "2f8b1c4d5e6f7a8b9c0d1e2f3a4b5c6d",
- "tracking_number": "9400111899562537683144",
- "courier_code": "usps",
- "delivery_status": "transit",
- "updated_at": "2026-05-26T14:30:00Z"
}
], - "errors": [
- {
- "tracking_number": "788702674979",
- "code": "already_exists",
- "message": "Tracking already exists"
}
]
}Returns crawler Trackcode JSON with subscription overlay (id, courier_code).
Same crawler blocks as webhook (events, source, couriers, info, …).
| id required | string Tracking identifier. |
{- "id": "2f8b1c4d5e6f7a8b9c0d1e2f3a4b5c6d",
- "courier_code": "usps",
- "trackcode": "9400111899562537683144",
- "status": "transit",
- "updatedAt": "2026-05-26 14:30:00",
- "origin_country": "string",
- "destination_country": "string",
- "info": {
- "property1": {
- "property1": "string",
- "property2": "string"
}, - "property2": {
- "property1": "string",
- "property2": "string"
}
}, - "events": [
- {
- "id": 0,
- "date": "2026-05-25 08:00:00",
- "status": "string",
- "attribute": "Accepted at USPS facility",
- "place": "New York, NY",
- "details": "string",
- "courier": "usps",
- "zip": "string",
- "weight": 0,
- "courier_locale": "string"
}
], - "source": [
- {
- "code": "ukrposhta",
- "name": "Ukrposhta",
- "country": "ua",
- "phone": "0-800-300-545",
- "track": "string",
- "external_link": "string"
}
], - "couriers": [
- {
- "code": "ukrposhta",
- "name": "Ukrposhta",
- "country": "ua",
- "phone": "0-800-300-545"
}
]
}Returns the list of possible couriers for the given tracking number.
| tracking_number required | string |
{- "tracking_number": "MGRAE0003506539YQ"
}[- {
- "id": 5,
- "code": "meest",
- "name": "Meest",
- "country": "ua",
- "required_fields": [
- {
- "name": "meest.zip",
- "example": "01001",
- "mask": "^[0-9]{5}$",
- "length": 5
}
]
}
]Manage webhook subscriptions for delivery updates, and the contract for the deliveries we send to your endpoint.
When a tracking you are subscribed to receives an update, we send an HTTP
POST to every enabled subscription whose account owns that tracking. The
request body is the tracking payload, forwarded verbatim — the same JSON,
byte-for-byte, that GET /v1/trackings/{id} returns for the parcel. No
wrapper object and no event field are added, so you must sign/verify
against the raw request body (see below).
{
"trackcode": "RA123456789CN",
"status": "delivered",
"origin_country": "CN",
"destination_country": "UA",
"events": [],
"source": [
{
"code": "ukrposhta",
"name": "Ukrposhta",
"track": "RA123456789CN",
"external_link": "https://track.ukrposhta.ua/tracking_RA123456789CN"
}
]
}
| Header | Value |
|---|---|
Content-Type |
application/json |
X-Webhook-Id |
Delivery id. Stable across every retry of the same delivery — use it to deduplicate. |
X-Webhook-Timestamp |
Unix time (seconds) of the delivery attempt. |
X-Webhook-Signature |
sha256=<hex(HMAC-SHA256(secret, raw_body))>. Sent only when the subscription has a secret. |
Compute the expected signature over the raw request body (before any deserialization, since the body is re-sent byte-for-byte) and compare in constant time:
expected = "sha256=" + hex(HMAC_SHA256(secret, raw_request_body))
compare(expected, X-Webhook-Signature) // constant-time
Any 2xx response is treated as a successful delivery. Any other status
code, or a network/timeout error, triggers a retry. Because X-Webhook-Id
is stable across retries, treat deliveries as at-least-once and deduplicate
on that id.
Creates a webhook subscription. url must be https. secret is never
returned — its presence is surfaced via the has_secret field.
| url required | string <uri> Must use |
| secret | string Secret used to sign deliveries (never returned in responses). |
| enabled | boolean Default: true |
{- "secret": "s3cr3t",
- "enabled": true
}{- "id": 1,
- "has_secret": true,
- "enabled": true,
- "created_at": "2026-05-26T14:30:00Z",
- "updated_at": "2026-05-26T14:30:00Z"
}Partial update. Only the fields to be changed are sent. An empty
secret clears the secret (has_secret=false).
| id required | integer <int64> Subscription identifier. |
| url | string <uri> |
| secret | string An empty string clears the secret. |
| enabled | boolean |
{- "enabled": false
}{- "id": 1,
- "has_secret": true,
- "enabled": true,
- "created_at": "2026-05-26T14:30:00Z",
- "updated_at": "2026-05-26T14:30:00Z"
}Delivers a mock tracking payload to the subscription's endpoint and waits for the answer. Nothing is persisted and nothing is retried — use it to verify connectivity and signature checking after setting a subscription up.
The request looks like a real delivery (same headers, same Trackcode
body, signed with the subscription's secret when it has one) with two
markers so it can be told apart: the header X-Webhook-Test: true and an
extra top-level "test": true field in the body. X-Webhook-Id is 0,
since a test has no delivery row.
Disabled subscriptions can be tested too. A delivery that fails is still
reported with HTTP 200 here — check ok and http_code in the body.
Not billed.
| id required | integer <int64> Subscription identifier. |
{- "ok": true,
- "http_code": 200,
- "duration_ms": 143,
- "error": "http 500"
}