In this noteShowHide
To track multiple USPS packages from code, put up to 35 tracking numbers in one API request. It counts as one lookup. A free key comes with 1,000 lookups a month: 1,000 packages if you send them one at a time, up to 35,000 if you send 35 at a time.
1The batch request
Send the carrier and an array of tracking numbers. The array is required even for one number, so the code that sends one is the code that sends 35.
curl https://api.shipmenttrackingapi.com/v1/track \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"carrier": "usps",
"trackingNumbers": [
"YOUR_TRACKING_NUMBER_1",
"YOUR_TRACKING_NUMBER_2",
"YOUR_TRACKING_NUMBER_3"
]
}' const res = await fetch("https://api.shipmenttrackingapi.com/v1/track", {
method: "POST",
headers: {
Authorization: "Bearer YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({
carrier: "usps",
trackingNumbers: [
"YOUR_TRACKING_NUMBER_1",
"YOUR_TRACKING_NUMBER_2",
"YOUR_TRACKING_NUMBER_3",
],
}),
});
const body = await res.json();
if (!body.success) throw new Error(body.error.code);
for (const r of body.data.results) {
console.log(r.trackingNumber, r.status ?? r.error.code);
} import requests
res = requests.post(
"https://api.shipmenttrackingapi.com/v1/track",
headers={"Authorization": "Bearer YOUR_API_KEY"},
json={
"carrier": "usps",
"trackingNumbers": [
"YOUR_TRACKING_NUMBER_1",
"YOUR_TRACKING_NUMBER_2",
"YOUR_TRACKING_NUMBER_3",
],
},
timeout=60,
)
body = res.json()
if not body["success"]:
raise SystemExit(body["error"]["code"])
for r in body["data"]["results"]:
print(r["trackingNumber"], r.get("status") or r["error"]["code"]) A batch has four rules: one carrier per request, at most 35 numbers, no number twice, and every number 6 to 40 letters and digits. USPS prints its numbers with spaces, so strip them first. Break one and the whole request is rejected with a 400. The message names the rule and points to the number at fault.
You can also send a batch without writing code: paste your key and up to 35 USPS numbers, one per line, into the Playground.
2The response
results has one entry for each number, in the order you sent them. An entry is either tracking data or an error for that one number. The HTTP status is 200 either way.
An example, not a real shipment. The scans are left out.
Because the order is kept, results[i] is the answer for trackingNumbers[i]. Each number comes back in uppercase, with leading and trailing spaces removed.
3When one number fails
One number without an answer does not fail the others. Its entry carries an error in place of a status. Of these errors, only TRACKING_NOT_FOUND makes the request count, the same as tracking data does.
| Code | What it means |
|---|---|
| TRACKING_NOT_FOUND | USPS has no record of that number. |
| INVALID_TRACKING_NUMBER | Not a valid USPS tracking number. |
| CARRIER_ERROR | USPS did not answer for that number. Send it again. |
INVALID_TRACKING_NUMBER can also reject the whole request. If a number is not shaped like a USPS number, the API answers 400, nothing is counted, and the message lists up to five of the numbers to fix.
4How a batch is counted
A request counts as one lookup when at least one number in it comes back with tracking data or with TRACKING_NOT_FOUND. A request where every entry is INVALID_TRACKING_NUMBER or CARRIER_ERROR is not counted. Neither is a request that was rejected with a 400, 429 or 503.
| Lookups per account | 1,000 a month | One request is one lookup. Resets on the 1st, UTC. |
| Lookups per IP address | 1,000 a month | Counted across every key that calls from that address. |
| Numbers per USPS request | 35 | UPS, FedEx and DHL take 1. |
| USPS packages, at most | 35,000 a month | 1,000 requests with 35 numbers in each. |
| Requests per account | 30 a minute | A 429 tells you when to retry. |
The response header X-RateLimit-Remaining-Month says how many lookups are left after each request.
5Tracking more than 35 packages
Split the list into groups of 35 and send them one after another. Each request takes about 30 seconds, so a loop sends about two a minute, well under the limit of 30 requests a minute. Requests sent at the same moment are more likely to come back 503. If a group does come back 503, send it again: it was not counted.
// Node 18 or later, saved as track.mjs
const API = "https://api.shipmenttrackingapi.com/v1";
const headers = {
Authorization: "Bearer YOUR_API_KEY",
"Content-Type": "application/json",
};
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
// Your USPS numbers. Remove spaces and repeats: the API rejects both.
const list = ["YOUR_TRACKING_NUMBER_1", "YOUR_TRACKING_NUMBER_2"];
const clean = list.map((n) => n.replace(/[^0-9A-Za-z]/g, "").toUpperCase());
const numbers = [...new Set(clean)];
// How many numbers one USPS request takes. This call needs no key.
const { data } = await (await fetch(`${API}/carriers`)).json();
const size = data.carriers.find((c) => c.id === "usps").maxTrackingNumbers;
const results = [];
for (let i = 0; i < numbers.length; i += size) {
const group = numbers.slice(i, i + size);
let res;
for (let attempt = 1; attempt <= 3; attempt++) {
res = await fetch(`${API}/track`, {
method: "POST",
headers,
body: JSON.stringify({ carrier: "usps", trackingNumbers: group }),
signal: AbortSignal.timeout(60_000),
});
if (res.status !== 503) break; // a 503 was not counted
await sleep(5_000);
}
const body = await res.json();
if (!body.success) throw new Error(body.error.code);
results.push(...body.data.results);
} # Python 3, with the requests package
import re
import time
import requests
API = "https://api.shipmenttrackingapi.com/v1"
HEADERS = {"Authorization": "Bearer YOUR_API_KEY"}
# Your USPS numbers. Remove spaces and repeats: the API rejects both.
raw = ["YOUR_TRACKING_NUMBER_1", "YOUR_TRACKING_NUMBER_2"]
clean = [re.sub(r"[^0-9A-Za-z]", "", n).upper() for n in raw]
numbers = list(dict.fromkeys(clean))
# How many numbers one USPS request takes. This call needs no key.
carriers = requests.get(f"{API}/carriers", timeout=30).json()["data"]["carriers"]
size = next(c for c in carriers if c["id"] == "usps")["maxTrackingNumbers"]
results = []
for i in range(0, len(numbers), size):
group = numbers[i:i + size]
for attempt in range(3):
res = requests.post(
f"{API}/track",
headers=HEADERS,
json={"carrier": "usps", "trackingNumbers": group},
timeout=60,
)
if res.status_code != 503: # a 503 was not counted
break
time.sleep(5)
body = res.json()
if not body["success"]:
raise SystemExit(body["error"]["code"])
results.extend(body["data"]["results"]) results ends up in the same order as numbers, one entry each.
6How long a batch takes
A batch is not instant. In our tests a USPS request took 24 to 28 seconds, whether it carried 2 numbers or 35. Some take longer.
A request that runs too long comes back 503, is not counted, and can be sent again. Set your HTTP client’s timeout to 60 seconds: a client that gives up sooner can miss a result that was still counted.
7UPS, FedEx and DHL
UPS, FedEx and DHL take one number per request. Your code does not have to hard-code that: GET /v1/carriers returns the limit for each carrier, and it needs no key.
curl https://api.shipmenttrackingapi.com/v1/carriers
The other three entries are shortened.