DocsOpenAPI

API documentation. How to ask, and what comes back.

doc·u·men·ta·tion n. 1The written account of how something works. 2Here: two endpoints, one key, nine statuses, and the limits of a free API.

On this pageShowHide
  1. 1Quick start
  2. 2Authentication
  3. 3The request
  4. 4The response
  5. 5When one number fails
  6. 6The nine statuses
  7. 7Carriers
  8. 8Errors
  9. 9Limits
  10. 10Retries and timeouts
  11. 11For tools and agents

The API has two endpoints. One looks up tracking numbers, the other lists the carriers. This page covers both, and what surrounds them: the key, the nine statuses, the errors and the limits.

1Quick start

Every request goes to https://api.shipmenttrackingapi.com over HTTPS, and every answer is JSON.

First, get a key: sign in to the console with Google and create one. It is free. The key is shown once, so copy it then. Next, send a carrier and an array of tracking numbers.

RequestPOST /v1/track
curl https://api.shipmenttrackingapi.com/v1/track \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"carrier":"ups","trackingNumbers":["YOUR_TRACKING_NUMBER"]}'
Put your own key and tracking number in place of YOUR_API_KEY and YOUR_TRACKING_NUMBER.

The answer has one result for each number. Read its status: it is always one of nine words. To send a request without writing code, open the Playground.

A lookup is not instant, so give your HTTP client a timeout of 60 seconds. Retries and timeouts has the details.

2Authentication

POST /v1/track needs your key in the Authorization header. GET /v1/carriers needs no key.

Header
Authorization: Bearer YOUR_API_KEY

A key starts with sta_. Each account has one key. We store only a hash of it, so the console cannot show it to you again. If you lose it, or it leaks, replace it in the console. A deleted key can keep working for up to a minute.

The limits belong to the account, not to the key: a new key carries on with the same count.

Keep the key on your server. A key placed in a web page or a mobile app can be read by anyone who uses it.

3The request

POST /v1/track looks up one or more tracking numbers for one carrier. Send the body as JSON, with Content-Type: application/json.

Request bodyPOST /v1/track
{
  "carrier": "usps",
  "trackingNumbers": [
    "YOUR_TRACKING_NUMBER_1",
    "YOUR_TRACKING_NUMBER_2"
  ]
}
The fields of the request body
FieldTypeRule
carrierstringOne of usps, ups, fedex, dhl, in lowercase.
trackingNumbersarray of stringsAn array even for one number. USPS takes 35 at once; UPS, FedEx and DHL take 1.

A request has four rules: one carrier per request, no more numbers than that carrier takes, no number twice, and every number 6 to 40 letters and digits. Carriers print their numbers with spaces, so strip them first. Break a rule and the whole request is rejected with a 400. The message names the rule and points to the number at fault.

Letters can be in either case. Each number comes back in uppercase, with leading and trailing spaces removed.

4The 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.

Response200 OK
{
"success": true,
"data": {
"carrier": "ups",
"results": [
{
"trackingNumber": "YOUR_TRACKING_NUMBER",
"status": "delivered",
"events": [
… 4 earlier scans …
{
"timestamp": "2026-11-05T13:36:00-06:00",
"status": "delivered",
"description": "DELIVERED",
"location": "Chicago, IL, US"
}
]
}
]
}
}

An example, not a real shipment. Four earlier scans are left out.

The fields of one result
FieldWhat it holds
results[]One entry for each number, in the order you sent them.
trackingNumberThe number you sent, in uppercase.
statusOne of the nine. The package’s latest state.
events[]Every scan, oldest first.
events[].timestampISO 8601. USPS times are in UTC; the other carriers keep the local offset.
events[].timeUnknowntrue when the carrier gave a date without a time. The time in timestamp is then a placeholder.
events[].statusOne of the nine, for that scan.
events[].descriptionThe carrier’s own words.
events[].locationWhere it was scanned, when the carrier says.
estimatedDeliveryWhen the carrier gives one.
lastUpdatedWhen this data was read from the carrier.
errorIn place of a status, when that one number has no answer.

timeUnknown, location, estimatedDelivery and lastUpdated are left out when there is nothing to put in them.

Your code only has to read status. description is the carrier’s wording, written for people: show it, but do not build logic on it.

5When one number fails

One number without an answer does not fail the others. Its entry carries an error in place of status and events.

One entry in results
{
"trackingNumber": "YOUR_TRACKING_NUMBER",
"error": {
"code": "TRACKING_NOT_FOUND",
"message": "No tracking information found for this tracking number"
}
}

Of these errors, only TRACKING_NOT_FOUND makes the request count, the same as tracking data does.

Errors for one number
CodeCountsWhat it means
TRACKING_NOT_FOUNDYesThe carrier has no record of that number.
INVALID_TRACKING_NUMBERNoNot a valid number for that carrier.
CARRIER_ERRORNoThe carrier did not answer for that number. Send it again.
Caution

INVALID_TRACKING_NUMBER can also reject the whole request. If a number is not shaped like that carrier’s numbers, the API answers 400 and nothing is counted. When the request had several numbers, the message lists up to five of the ones to fix.

6The nine statuses

A status is always one of these nine words, for the package as a whole and for each scan.

The nine statuses and what they mean
StatusMeaning
pendingA label exists, but the carrier does not have the package yet.
in_transitMoving through the carrier’s network. Also a missed delivery that can still be picked up or redelivered.
out_for_deliveryOn the vehicle for the final delivery.
deliveredHanded over at the destination.
failedThe delivery could not be completed.
returnedGoing back to the sender, or already back.
cancelledThe shipment was cancelled before delivery.
lostThe carrier reports the package missing.
unknownThe carrier said something the table does not cover yet.

The words come from a table of what each carrier says, shown on the home page. The translation is automatic, and a status can be late, missing or wrong. Each scan also carries the carrier’s own words, in description.

7Carriers

GET /v1/carriers lists the carriers and how many tracking numbers each one takes in a request. It needs no key and is not counted.

The carriers and how many numbers each takes in one request
carrierNameNumbers per request
uspsUSPS1 to 35
upsUPS1
fedexFedEx1
dhlDHL1
RequestGET /v1/carriers
curl https://api.shipmenttrackingapi.com/v1/carriers
Response200 OK
{
"success": true,
"data": {
"carriers": [
{
"id": "usps",
"name": "USPS",
"website": "https://www.usps.com",
"maxTrackingNumbers": 35
},
{
"id": "ups",
"name": "UPS",
"website": "https://www.ups.com",
"maxTrackingNumbers": 1
},
{
"id": "fedex",
"name": "FedEx",
"website": "https://www.fedex.com",
"maxTrackingNumbers": 1
},
{
"id": "dhl",
"name": "DHL",
"website": "https://www.dhl.com",
"maxTrackingNumbers": 1
}
]
}
}

Read maxTrackingNumbers from this response instead of writing the numbers into your code.

A request names one carrier, and the API does not work out the carrier from a number. For packages from two carriers, send two requests.

8Errors

When the whole request fails, the HTTP status says so, and the body has the same shape every time: a code your program can switch on and a message a person can read.

Response400 Bad Request
{
"success": false,
"error": {
"code": "TOO_MANY_TRACKING_NUMBERS",
"message": "UPS accepts 1 tracking number per request (2 sent)"
}
}

The answer to a UPS request that carried 2 numbers.

Errors for the whole request
CodeHTTPWhen
INVALID_CARRIER400The carrier is not one of usps, ups, fedex, dhl.
INVALID_TRACKING_NUMBER400A number is malformed, repeated, or not valid for that carrier. trackingNumbers is always an array.
TOO_MANY_TRACKING_NUMBERS400More numbers than the carrier takes in one request: 35 for USPS, 1 for the others.
AUTH_MISSING_API_KEY401There is no Authorization header.
AUTH_INVALID_API_KEY401The key does not exist.
KEY_INACTIVE403The key was turned off.
NOT_FOUND404There is no such endpoint. Check the path and the method.
RATE_LIMIT_EXCEEDED429More than 30 requests in a minute. Retry-After says when to come back.
MONTHLY_QUOTA_EXCEEDED429The account has used its 1,000 lookups for the month.
IP_QUOTA_EXCEEDED429The IP address has used its 1,000 lookups for the month, across every key.
INTERNAL_ERROR500Something unexpected on our side. Try again.
SERVICE_UNAVAILABLE503Something on our side. Try again.

One exception: a body that is not valid JSON is refused before it reaches the API, with a plain 400 and no JSON.

An error for a single number is not in this table. It comes inside results with HTTP 200, as When one number fails describes.

Switch on code. The wording of a message can change.

9Limits

Limits, their values, and details
Lookups per account1,000 a monthOne request is one lookup. Resets on the 1st, UTC.
USPS packages, at most35,000 a monthA USPS request carries up to 35 numbers and still counts as one lookup.
Requests per account30 a minuteA 429 tells you when to retry.
Lookups per IP address1,000 a monthCounted across every key and the Playground. An address over the limit is blocked until the 1st, UTC.
Numbers per request1 to 35USPS takes 35 at once; UPS, FedEx and DHL take 1.

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.

The answers to POST /v1/track carry headers that say where you stand.

Response headers about the limits
HeaderWhat it says
X-RateLimit-Limit-MinuteRequests your account can send in one minute.
X-RateLimit-Remaining-MinuteRequests left in the current minute.
X-RateLimit-Limit-MonthLookups your account has for the month.
X-RateLimit-Remaining-MonthLookups left this month.
Retry-AfterOnly with RATE_LIMIT_EXCEEDED: the seconds until the next minute starts.

The minute is the clock minute: the count starts again at second 0. Every request with a valid key uses one of the 30, whatever the answer. The month is the calendar month, in UTC.

The IP limit adds up every lookup from one address, whichever key made it. An IPv6 address is counted by its /64 network. If you share an outbound address with other people, as on a serverless platform or behind a shared NAT, send your requests through a static IP address of your own.

10Retries and timeouts

A lookup 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.

When to send a request again
CodeHTTPWhen to send it again
RATE_LIMIT_EXCEEDED429After the seconds in Retry-After.
MONTHLY_QUOTA_EXCEEDED429On the 1st of next month, UTC.
IP_QUOTA_EXCEEDED429On the 1st of next month, UTC.
SERVICE_UNAVAILABLE503After a few seconds. It was not counted.
INTERNAL_ERROR500After a few seconds.
CARRIER_ERROR200Send that number again. The other numbers in the request have their answers.

Do not resend a 400, 401, 403 or 404 as it is. Fix the request first.

Send requests one after another. Requests sent at the same moment are more likely to come back 503. For a list longer than one request holds, this note has a loop that splits it and retries.

There are no webhooks. To follow a package, ask again later, and stop once it is delivered.

11For tools and agents

The same reference is published as two files that a program can read.

Machine-readable files
FileWhat it is
/openapi.jsonAn OpenAPI 3.1 description of both endpoints, for API clients and code generators.
/llms.txtA plain-text summary for language models and coding agents.
See also
StartAbout a minute

Sign in with Google. Your key is ready in seconds.