Shipment Tracking API Reference — REST Endpoints | WhereParcel
Real-time package & shipment tracking REST API. Single and bulk endpoints, auto carrier detection, code examples in cURL/Node/Python/PHP/Go. 7-day free trial.
Track parcels (billable)
Overview
Track up to 5 parcels at once. Average response time 5 seconds per item. Supports 500+ carriers including USPS, UPS, FedEx, DHL, and more.
Some carriers are limited to 1 item per request:
The carriers listed below take substantially longer to look up, so a single synchronous request may contain at most 1 item across all of them combined — mixing two of them in one request is rejected as well. You can still batch them with other carriers: 1 item from this list + up to 4 items from any other carrier = 5 items total.
us.ups, us.usps, us.fedex, us.dhl, us.dhl.express, us.dhl.ecommerce, intl.ups, intl.usps, intl.fedex, intl.dhl, gb.royalmail, gb.royalmail.special, gb.evri, gb.ups, ca.ups, ca.fedex, de.ups, es.ups, ie.ups, au.post
This list may change. Exceeding the limit returns INVALID_REQUEST, and the error message names the exact carriers that triggered it. To track several items from these carriers, use /v2/webhooks/register (up to 100 items, asynchronous).
Events without a time: some carriers give only a date for certain events and no scan time (USPS does this for "In Transit to Next Facility" and "Pre-Shipment"). In that case timestamp is filled with noon on that date to stay valid ISO 8601, and the event carries timeUnknown: true. When the field is absent, the time came from the carrier and is real.
When to use this endpoint:
Use /v2/track for on-demand, one-off lookups — for example, when a customer checks their order status on your website.
For continuous monitoring, use Webhooks instead.
If you need to keep your database in sync with delivery status (e.g., updating order records, triggering notifications), register a webhook subscription via /v2/webhooks/register with recurring: true. The webhook approach is far more efficient — instead of polling repeatedly, you receive a push notification only when the status actually changes.
Rate Limits:
Each API key has per-minute and per-month request quotas based on your plan. When exceeded, the API returns HTTP 429. See response headers X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset for current usage.
| Plan | Requests/min | Requests/month |
|---|---|---|
| Starter | 30 | 10,000 |
| Pro | 60 | 30,000 |
| Business | 200 | 300,000 |
Example Request
// npm install whereparcel
import { WhereParcel } from 'whereparcel';
const wp = new WhereParcel('wp_test_public_demo_d5waw8abfqor', 'sk_test_public_demo_mj7ya1taqmaqfkv6lpwe');
const response = await wp.trackBulk([
{
"carrier": "us.ups",
"trackingNumber": "1Z999AA10123456784"
},
{
"carrier": "us.ontrac",
"trackingNumber": "D10017048734180"
}
]);
console.log(response.summary);
for (const result of response.results) {
if (result.status === 'success') {
console.log(result.data.deliveryStatus);
}
}curl -X POST https://api.whereparcel.com/v2/track \
-H "Authorization: Bearer wp_test_public_demo_d5waw8abfqor:sk_test_public_demo_mj7ya1taqmaqfkv6lpwe" \
-H "Content-Type: application/json" \
-d '{
"trackingItems": [
{
"carrier": "us.ups",
"trackingNumber": "1Z999AA10123456784"
},
{
"carrier": "us.ontrac",
"trackingNumber": "D10017048734180"
}
]
}'const response = await fetch('https://api.whereparcel.com/v2/track', {
method: 'POST',
headers: {
"Authorization": "Bearer wp_test_public_demo_d5waw8abfqor:sk_test_public_demo_mj7ya1taqmaqfkv6lpwe",
"Content-Type": "application/json"
},
body: JSON.stringify({
"trackingItems": [
{
"carrier": "us.ups",
"trackingNumber": "1Z999AA10123456784"
},
{
"carrier": "us.ontrac",
"trackingNumber": "D10017048734180"
}
]
})
});
const data = await response.json();
console.log(data);<?php
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'https://api.whereparcel.com/v2/track');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
$headers = [
'Authorization: Bearer wp_test_public_demo_d5waw8abfqor:sk_test_public_demo_mj7ya1taqmaqfkv6lpwe',
'Content-Type: application/json'
];
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
$data = [
'trackingItems' => [
'0' => [
'carrier' => "us.ups",
'trackingNumber' => "1Z999AA10123456784"
],
'1' => [
'carrier' => "us.ontrac",
'trackingNumber' => "D10017048734180"
]
]
];
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
$response = curl_exec($ch);
curl_close($ch);
$result = json_decode($response, true);
print_r($result);import requests
import json
url = 'https://api.whereparcel.com/v2/track'
headers = {
'Authorization': 'Bearer wp_test_public_demo_d5waw8abfqor:sk_test_public_demo_mj7ya1taqmaqfkv6lpwe',
'Content-Type': 'application/json'
}
data = {
'trackingItems': {
'0': {
'carrier': "us.ups",
'trackingNumber': "1Z999AA10123456784"
},
'1': {
'carrier': "us.ontrac",
'trackingNumber': "D10017048734180"
}
}
}
response = requests.post(url, headers=headers, json=data)
result = response.json()
print(result)package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
)
func main() {
url := "https://api.whereparcel.com/v2/track"
payload := []byte(`{"trackingItems":[{"carrier":"us.ups","trackingNumber":"1Z999AA10123456784"},{"carrier":"us.ontrac","trackingNumber":"D10017048734180"}]}`)
req, _ := http.NewRequest("POST", url, bytes.NewBuffer(payload))
req.Header.Add("Authorization", "Bearer wp_test_public_demo_d5waw8abfqor:sk_test_public_demo_mj7ya1taqmaqfkv6lpwe")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}TIP: Replace the test API key with your actual API key from the dashboard.
Request Body
| Name | Type | Required | Description |
|---|---|---|---|
trackingItems | array | Required | |
trackingItems[].carrier | string | Required | Carrier code (e.g., 'kr.cj', 'us.fedex', 'de.gls') |
trackingItems[].trackingNumber | string | Required | Tracking number |
trackingItems[].clientId | string | Optional | Client tracking ID (returned as-is in the response) |
trackingItems[].postalCode | string | Optional | Postal code (required/optional for some carriers, e.g., GLS Germany) |
trackingItems[].phoneNumber | string | Optional | Phone number (required/optional for some carriers) |
Response
Success Response (200)
Successful tracking response
Response Body
{
"success": true,
"results": [
{
"carrier": "us.fedex",
"trackingNumber": "231300687629630",
"clientId": "order-123",
"status": "success",
"billable": true,
"cached": false,
"data": {
"deliveryStatus": "delivered",
"events": [
{
"timestamp": "2026-02-05T14:30:00-05:00",
"status": "delivered",
"location": "New York, NY",
"description": "Delivered, left at front door"
},
{
"timestamp": "2026-02-05T09:15:00-05:00",
"status": "out_for_delivery",
"location": "Brooklyn, NY",
"description": "On FedEx vehicle for delivery"
},
{
"timestamp": "2026-02-04T18:00:00-05:00",
"status": "in_transit",
"location": "Newark, NJ",
"description": "At local FedEx facility"
}
],
"from": {
"name": "J. Smith"
},
"to": {
"name": "M. Johnson"
},
"lastUpdated": "2026-02-05T14:30:00-05:00"
}
},
{
"carrier": "us.usps",
"trackingNumber": "9400111899223197428490",
"status": "success",
"billable": true,
"cached": false,
"data": {
"deliveryStatus": "in_transit",
"events": [
{
"timestamp": "2026-02-05T12:00:00-05:00",
"status": "in_transit",
"location": "Chicago, IL",
"description": "In transit to next facility"
}
],
"lastUpdated": "2026-02-05T12:00:00-05:00"
}
}
]
} Error Response (401)
Authentication failed - API key missing or invalid
Response Body
{
"success": false,
"error": {
"code": "UNAUTHORIZED",
"message": "Invalid or missing API key"
}
} Error Response (429)
Rate limit exceeded
Response Body
{
"success": false,
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"message": "Rate limit exceeded. Please try again later."
}
}