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 normal tracking 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.
Optional API operations can have their own price. In particular, every successful
forced refresh (POST /v1/trackings/{tracking_number}/refresh) is charged separately
at the current price of the tracking.refresh operation, even when the carrier
reports no new events.
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).
The same API is also exposed as an MCP (Model Context Protocol) server, so you can connect directly to a model or agent instead of writing HTTP calls yourself. The model acts on behalf of your account with exactly the rights of the regular Tracking API, and MCP is not billed separately — calls consume the same plan credits as the equivalent HTTP requests.
| What | Value |
|---|---|
| Channel URL | https://mcp.parceltracking.app/mcp |
| Transport | Streamable HTTP (POST) |
| Server name in the client | packyapp-tracking-mcp |
| Dashboard | https://lk.parceltracking.app |
| Access key | Your account API key from the dashboard — the same key as for /v1 |
| Method | When to use | How the model gets access |
|---|---|---|
| API key (Bearer) | Cursor, Claude Desktop — any client where you can set request headers | You paste the account API key into the client config |
| OAuth | Claude Custom connector and similar hosted clients | You sign in to the Packy dashboard in the browser; no key in the config |
For the API-key method, authenticate every request with
Authorization: Bearer <your-api-key>. The X-API-Key: <your-api-key> header is
accepted as a fallback.
Every account has its own key. Do not share it in group chats or shared documents — if it leaks, deactivate it in the dashboard and issue a new one.
Config file: project or user MCP settings (mcp.json).
{
"mcpServers": {
"packyapp-tracking-mcp": {
"url": "https://mcp.parceltracking.app/mcp",
"headers": {
"Authorization": "Bearer <your-api-key>"
}
}
}
}
Replace <your-api-key> with your account API key, then:
mcp.json.Config file:
| OS | Path |
|---|---|
| macOS | ~/Library/Application Support/Claude/claude_desktop_config.json |
| Windows | %APPDATA%\Claude\claude_desktop_config.json |
{
"mcpServers": {
"packyapp-tracking-mcp": {
"url": "https://mcp.parceltracking.app/mcp",
"headers": {
"Authorization": "Bearer <your-api-key>"
}
}
}
}
Replace <your-api-key> the same way as above, then:
claude_desktop_config.json.Prepare first, in the dashboard — not from the model client:
https://lk.parceltracking.app.The OAuth connection runs under the key you pick: the model's calls and credits are attributed to it, it keeps working while that key is active, and it stops as soon as you deactivate the key. Without an account and at least one active key the browser sign-in can succeed while the model still ends up with no working tools.
Then connect:
https://mcp.parceltracking.app/mcp.Access tokens are short-lived and refreshed automatically, so you do not need to reconnect periodically.
The channel covers the core /v1 resources:
| Tool | Maps to |
|---|---|
trackings_create |
POST /v1/trackings |
trackings_list |
GET /v1/trackings |
trackings_get |
GET /v1/trackings/{id} |
trackings_delete |
DELETE /v1/trackings/{id} |
couriers_list |
GET /v1/couriers |
courier_detections_create |
POST /v1/courier-detections |
webhooks_list |
GET /v1/webhooks |
webhooks_create |
POST /v1/webhooks |
webhooks_update |
PATCH /v1/webhooks/{id} |
webhooks_delete |
DELETE /v1/webhooks/{id} |
Credits are consumed exactly as they are over HTTP: a trackings_create call from the
model costs the same credit as POST /v1/trackings, and a batch create spends one
credit per tracking number in the batch. It is the only billable tool — the courier and
webhook tools cost nothing, so the only calls worth running deliberately are creates.
| Check | Expected result |
|---|---|
| Tool list in the client | The tools above appear |
A read call such as couriers_list |
Succeeds within your plan's limits |
| Invalid or deactivated key | The connection or the call is rejected with 401 |
| Channel liveness (no key needed) | GET https://mcp.parceltracking.app/health → {"status":"ok"} |
| Channel readiness (no key needed) | GET https://mcp.parceltracking.app/ready → {"status":"ok","checks":{"api_gateway":"healthy"}} |
API-key connections. Deactivate or rotate the account API key in the dashboard, then
remove or update the packyapp-tracking-mcp entry in the client config. Once the key is
deactivated, the previous model connection stops working.
OAuth connections. Open Connected applications in the dashboard and revoke the application's access, then remove the connector in Claude. Deactivating the API key the connection runs under also stops it immediately.
| Symptom | What to do |
|---|---|
| No tool list appears | Check the URL https://mcp.parceltracking.app/mcp, the server name packyapp-tracking-mcp, the key or a finished OAuth flow, then restart the client |
401 Unauthorized |
Confirm the key is active in the dashboard; for OAuth, confirm the account has an active key and run the connection again |
| OAuth: Could not continue authorization | Sign in with the same account, confirm it has an active key, and start the connection again from Claude — do not reuse the link from the previous attempt |
| Access must be cut right now | Deactivate the key in the dashboard, then remove the entry or connector in the client |
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"
}
]
}Immediately polls the carrier for a tracking that already belongs to the
authenticated account and returns the freshly obtained TrackingDetail,
including events and couriers, in the same response.
This operation does not create, subscribe, or re-create a tracking. If the
tracking number does not belong to the account, the response is 404 Not Found.
Use POST /v1/trackings first when you want to start tracking a new number.
Every successful response is charged at the current price configured for the
tracking.refresh billing operation, even if the carrier reports no new events.
A normal GET /v1/trackings/{id} only reads saved data and is billed according
to its own operation; it does not trigger or charge a forced refresh.
Each tracking has a dedicated rate limit within an account. Exceeding it returns
429 for that tracking without blocking refreshes of other trackings, create,
GET, or other API operations for the account.
| tracking_number required | string Example: 9400111899562537683144 Tracking number of an existing tracking owned by the account. |
{- "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"
}