Authentication
Every tracking request carries an API key as a bearer token. There is no login step, no token exchange and nothing that expires mid-session: one header on every call is all of it.
Authorization: Bearer trk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Getting a key
Create one in API keys in your dashboard. It appears once, at the moment you create it — we store only a hash, so a key that is lost cannot be shown again, only replaced.
Give each integration its own key and name it after what it is: a staging environment, a partner, a scheduled job. It costs nothing, and it is what makes it possible to revoke one without taking the others down with it. Afterwards the dashboard shows each key by its last four characters, which is enough to tell them apart in a list.
A key is one continuous string beginning with trk_. Nothing is encoded inside it for you to read: it is not a JWT, it carries no account id, no expiry and no list of permissions, and there is nothing in it to split apart or decode. Send it whole, exactly as the dashboard gave it to you.
Sending the key
curl -H "Authorization: Bearer $TRACKING_ONE_API_KEY" \
"https://api.tracking.one/v1/shipments/ocean?referenceNumber=MEDU9270627"
import os
import requests
response = requests.get(
'https://api.tracking.one/v1/shipments/ocean',
headers={'Authorization': f"Bearer {os.environ['TRACKING_ONE_API_KEY']}"},
params={'referenceNumber': 'MEDU9270627'},
timeout=130,
)
response.raise_for_status()
shipment = response.json()
// Node, on your server - not in a page the browser loads.
const url = new URL('https://api.tracking.one/v1/shipments/ocean');
url.searchParams.set('referenceNumber', 'MEDU9270627');
const response = await fetch(url, {
headers: { Authorization: `Bearer ${process.env.TRACKING_ONE_API_KEY}` },
signal: AbortSignal.timeout(130_000),
});
const shipment = await response.json();
The key comes from the environment in all three, never from the source file. That is the whole of the advice, and it is worth following literally: a key committed to a repository is the way most of them get replaced.
Give the request a generous timeout. Some carriers take a while, and we hold a lookup for up to two minutes before answering 202. A client with a 30-second timeout will give up on requests that were about to succeed.
Which endpoints need a key
The tracking endpoints do. The two carrier-coverage endpoints do not:
| Endpoint | Key |
|---|---|
GET /v1/shipments/ocean | required |
GET /v1/shipments/air | required |
GET /v1/carriers/ocean | not required |
GET /v1/carriers/air | not required |
Sending a key to a carrier endpoint anyway is harmless — it is ignored, and the call is still free.
Keep the key on your server
A key is bearer credentials: whoever holds it can spend your credits. It belongs in server-side configuration — an environment variable, a secrets manager — and nowhere else.
In particular, never put it in front-end code. Anything a browser can read, a visitor can read: a key in JavaScript, in an HTML attribute or in a URL your page requests is a key you have published. The same goes for logs, screenshots, support tickets and issue trackers.
If you need tracking on a public web page, the pattern is a thin endpoint of your own that holds the key and forwards the request. That is exactly what the tracking widgets are built around, and their guide has working proxy samples for Node, Python, PHP and WordPress.
Rotating and revoking
Delete a key in API keys and it stops working immediately — there is no grace period and no cache to wait out.
To rotate without downtime, create the replacement first, deploy it, confirm traffic has moved, and only then delete the old key. Both are valid at the same time, so there is no window where your integration is unauthenticated.
If a key has leaked, do it in the other order: delete first and accept the failed requests. A revoked key cannot be un-revoked, and a leaked one cannot be made safe any other way.
Authentication errors
A failed request answers with a status code and a JSON body carrying a human-readable message; where there is something a client should branch on, an error slug sits beside it.
| Status | error | What happened |
|---|---|---|
401 | — | The Authorization header was missing or malformed, or the key is invalid, expired or revoked. |
402 | insufficient_credits | The key is valid; the account has no credits left for that product. |
402 | usage_limit_reached | The key is valid; the account's monthly usage cap for that product is reached. |
429 | — | Too many requests on this key. Wait Retry-After seconds. |
401 is about the key. 402 is about the account behind it — the request authenticated fine, there is just nothing left to spend:
{
"message": "Insufficient credits for product [tracking].",
"error": "insufficient_credits",
"product": "tracking"
}
A usage_limit_reached body additionally carries limit and resets_at, so a client can tell "raise the cap" from "wait for the 1st of the month" without asking a human.
Worth handling separately in your client: 401 means stop and fix the credentials — retrying will not help. 402 means top up or raise the cap. 429 means wait and retry. Only 5xx and 202 are worth an automatic retry, and 202 is not a failure at all.
Next: the endpoints themselves — Container Tracking API and Air Tracking API.