API Documentation
One API for every courier we support
Introduction
The TrackCourier.io API provides a unified interface to track shipments across major Indian and international couriers. With one integration, you can track packages from Speed Post, Blue Dart, Delhivery, DTDC, Shree Tirupati and many more.
Base URL: https://api.trackcourier.io/v1
All API requests are made over HTTPS. Requests made over plain HTTP will fail.
Authentication
All API requests require authentication via the X-API-Key header.
X-API-Key: your_api_key_here
Get your API key from your dashboard after signing up.
Rate Limits
| Plan | Monthly Requests | Rate Limit |
|---|---|---|
| Free | 100 | 10/minute |
| Starter | 5,000 | 60/minute |
| Pro | 50,000 | 300/minute |
๐ Get Started in 3 Steps
Start tracking shipments in under 5 minutes. No complex setup required.
Get Your Free API Key
Sign up with Google and get 100 free requests/month instantly. No credit card required.
Get API Key โMake Your First Tracking Request
Copy this code and replace your_api_key with your actual key:
curl "https://api.trackcourier.io/v1/track?courier=dtdc&tracking_number=R1000482882" \
-H "X-API-Key: your_api_key"
import requests
response = requests.get(
"https://api.trackcourier.io/v1/track",
params={"courier": "dtdc", "tracking_number": "R1000482882"},
headers={"X-API-Key": "your_api_key"}
)
data = response.json()
print(data["data"]["ShipmentState"]) # "intransit", "delivered", etc.
const response = await fetch(
"https://api.trackcourier.io/v1/track?courier=dtdc&tracking_number=R1000482882",
{ headers: { "X-API-Key": "your_api_key" } }
);
const data = await response.json();
console.log(data.data.ShipmentState); // "intransit", "delivered", etc.
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, "https://api.trackcourier.io/v1/track?courier=dtdc&tracking_number=R1000482882");
curl_setopt($ch, CURLOPT_HTTPHEADER, ["X-API-Key: your_api_key"]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
$data = json_decode($response, true);
echo $data["data"]["ShipmentState"]; // "intransit", "delivered", etc.
Handle the Response
You'll get back a JSON response with the shipment status and tracking history:
{
"success": true,
"data": {
"ShipmentState": "delivered", // Normalised state
"MostRecentStatus": "Delivered", // Our short label, not the courier's wording
"Checkpoints": [...] // Full history; sort by Date+Time yourself
},
"usage": {
"used": 1,
"quota": 100,
"plan": "growth",
"percent": 1.0
}
}
That's it! You're now tracking shipments. Check the API Reference for more details.
Check everything in one command, before importing anything. This runs our Postman collection, with its own checks, from your machine. It needs Node.js. The tracking requests use recently delivered shipments where we have one for that courier; the webhook requests run only if you also pass --env-var "webhook_url=https://your-receiver/path", and are skipped otherwise:
npx newman run https://api.trackcourier.io/postman --env-var "api_key=your_api_key"
Common Use Cases
Show customers their order's tracking status on your website
Push delivery notifications to your app users
Trigger workflows based on delivery status changes
Track Shipment
Track a shipment by courier and tracking number. Returns current status and checkpoint history.
Query Parameters
| Parameter | Type | Description |
|---|---|---|
| courier required | string | Courier slug (e.g., "bluedart", "delhivery") |
| tracking_number required | string | AWB or tracking number |
Response
{
"success": true,
"data": {
"MostRecentStatus": "Delivered",
"ShipmentState": "delivered",
"Checkpoints": [
{
"Activity": "Delivered",
"CheckpointState": "delivered",
"CourierName": "DTDC",
"Date": "18-Sep-2026",
"Time": "17:08",
"Location": "Delhi"
},
{
"Activity": "Out for Delivery - Shipment is out for Delivery 2026-09-18 14:09:12",
"CheckpointState": "outfordelivery",
"CourierName": "DTDC",
"Date": "18-Sep-2026",
"Time": "14:09",
"Location": "Delhi"
},
{
"Activity": "Received at DELHI Delivery Centre",
"CheckpointState": "intransit",
"CourierName": "DTDC",
"Date": "18-Sep-2026",
"Time": "09:31",
"Location": "Delhi"
},
{
"Activity": "On The Way - On its way between JAIPUR and DELHI",
"CheckpointState": "intransit",
"CourierName": "DTDC",
"Date": "17-Sep-2026",
"Time": "22:15",
"Location": "Jaipur to Delhi"
}
],
"CourierHasNoRecordOfShipment": false,
"isEmptyTable": false,
"Result": "success",
"AdditionalInfo": "",
"FetchTime": "4.9084",
"PODImageUrl": "",
"ExecutionTime": ""
},
"usage": {
"used": 45,
"quota": 100,
"plan": "growth",
"percent": 45.0
}
}
Status Values
| Status | Description |
|---|---|
pending | Shipment created, not yet picked up |
in_transit | Shipment moving through network |
out_for_delivery | With delivery agent |
delivered | Successfully delivered |
exception | Delivery issue (RTO, failed attempt) |
Response Codes
| 200 | Success - tracking data returned |
| 401 | Invalid or missing API key |
| 402 | Quota exceeded, or the account needs attention. Check error.code. |
| 404 | Courier not found |
List Couriers
Get every courier you can track, with the slug to use in your requests. This endpoint does not count against your quota.
Response
{
"success": true,
"data": {
"couriers": [
{"slug": "bluedart", "name": "Blue Dart"},
{"slug": "delhivery", "name": "Delhivery"},
{"slug": "dtdc", "name": "DTDC"}
],
"count": 37
}
}
Get Courier Details
Get details for a specific courier by its slug. This endpoint does not count against your quota.
Response
{
"success": true,
"data": {
"slug": "delhivery",
"name": "Delhivery"
}
}
Webhooks
Webhooks push shipment events to your server. We re-check the shipments we follow with the courier at scheduled times, for most couriers at our current 3 re-checks a day, and sometimes in between; you do not need to poll for us to keep following them. Two couriers are followed only when looked up: ARC Carrier and Om Logistics. Poll GET /v1/track for them if you need their changes. Events are not real-time: an event about a shipment reaches you only after one of our re-checks sees the change, so it can reach you hours after the courier recorded it.
Set up a webhook in your dashboard. If your account includes live webhooks (the Growth and Business plans do), sign in, open Webhooks in your dashboard, choose + Create webhook, enter your https:// URL, and tick every event you act on. The same page has Rotate signing secret, which issues a new signing secret, shown once; keep accepting the old one as well for at least 15 minutes after you rotate.
Your signing secret signs every webhook on your account, test and live. The call that generates it gives it to you once, if that call succeeds: your account's first create, or a rotation made before any create. Save it then, and make that first create on its own. A later create returns a notice beginning with < in its place; do not store that, as your secret is unchanged.
Test webhooks need no particular plan: create one through the API with "mode": "test". The webhook requests in our Postman collection create one and send a sample delivery to it.
How It Works
1. Configure your webhook URL in the dashboard
2. Subscribe to tracking numbers you want to monitor
3. Receive POST requests when status changes
Webhook Event Types
We send one of these events when we re-check a shipment and find its status changed to one with an event below, or more checkpoints than before.
| Event | Description |
|---|---|
tracking.updated |
More checkpoints than before, with no change to a state that has its own event |
tracking.in_transit |
The shipment's status changed to in_transit |
tracking.out_for_delivery |
The shipment's status changed to out_for_delivery |
tracking.delivered |
The shipment's status changed to delivered. Terminal: we stop following it |
tracking.exception |
The shipment's status changed to exception, returned or cancelled |
A change to a state that has its own event replaces tracking.updated for that update, so one change never produces two events for one webhook. A webhook receives only the events it lists, so list every event you act on.
tracking.exception covers both a problem the shipment can recover from and a terminal non-delivery: read data.status to tell them apart (returned and cancelled are terminal, exception is not).
Webhook Payload
All webhook events are sent as POST requests with a JSON body:
Note: the webhook payload is snake_case throughout,
including inside each checkpoint. It is not the same shape as the
GET /v1/track response above, which returns our cache document in
PascalCase. If you use both surfaces, write two mappings: a model
generated from one decodes nothing from the other.
{
"event": "tracking.delivered",
"enqueued_at": "2026-09-18T09:14:22Z",
"data": {
"tracking_number": "7X118822611",
"courier": "dtdc",
"status": "delivered",
"most_recent_status": "Delivered",
"origin_city": null,
"destination_city": null,
"delivered_date": null,
"checkpoints": [
{
"activity": "Delivered",
"checkpoint_state": "delivered",
"courier_name": "DTDC",
"date": "18-Sep-2026",
"time": "17:08",
"location": "Delhi"
},
{
"activity": "Out for Delivery - Shipment is out for Delivery 2026-09-18 14:09:12",
"checkpoint_state": "outfordelivery",
"courier_name": "DTDC",
"date": "18-Sep-2026",
"time": "14:09",
"location": "Delhi"
},
{
"activity": "Received at DELHI Delivery Centre",
"checkpoint_state": "intransit",
"courier_name": "DTDC",
"date": "18-Sep-2026",
"time": "09:31",
"location": "Delhi"
},
{
"activity": "On The Way - On its way between JAIPUR and DELHI",
"checkpoint_state": "intransit",
"courier_name": "DTDC",
"date": "17-Sep-2026",
"time": "22:15",
"location": "Jaipur to Delhi"
}
],
"enqueued_at": "2026-09-18T09:14:22Z"
}
}
Webhook Security
Verify that webhook requests are from TrackCourier.io by checking the signature.
Signature Verification
Every webhook includes an X-Trackcourier-Signature header containing an HMAC-SHA256 signature of the request body.
Three further headers accompany every delivery. None of the headers is signed: the signature covers the body alone, so take anything you act on from the verified body.
X-Webhook-Event: the event type, e.g.tracking.delivered. The verified body carries the same name asevent.X-Webhook-Delivery: an attempt identifier, not an event identifier. It is<webhook_id>-<attempt>, so it changes on every retry of the same event, and the first attempt of every event sent to one webhook carries the same value. Do not deduplicate on it. See Handle duplicates below.X-Webhook-Mode:testfor a sample delivery to a test webhook,liveotherwise. To tell a test delivery apart once its signature verifies, readdata.courierin the body: a test delivery's beginssynthetic-test-.
X-Trackcourier-Signature: sha256=a1b2c3d4e5f6...
Verification Example (Python)
import hashlib
import hmac
import json
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
app = FastAPI()
# Your signing secret. We show it only once: save it when we do.
WEBHOOK_SECRET = "your_signing_secret"
def verify(raw_body: bytes, header: str, secret: str) -> bool:
if not header.isascii(): # compare_digest raises on non-ASCII text
return False
expected = "sha256=" + hmac.new(
secret.encode(), raw_body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, header)
@app.post("/webhook")
async def handle_webhook(request: Request):
raw_body = await request.body() # the raw bytes, never a re-serialised object
signature = request.headers.get("X-Trackcourier-Signature", "")
if not verify(raw_body, signature, WEBHOOK_SECRET):
return JSONResponse({"error": "Invalid signature"}, status_code=401)
event = json.loads(raw_body)
# Replace your stored checkpoints for this consignment with
# event["data"]["checkpoints"], unless it is null (see Handle duplicates).
return {"received": True}
Retry Policy
Any 2xx response counts as delivered. We follow redirects, so do not answer with one: after a 301, 302 or 303 the redirected request is sent as a GET without the body, and we record a success for a delivery your endpoint never received; after a 307 or 308 we send the signed body on to wherever the redirect points. Return 2xx at the URL you configured. Anything else is retried on this schedule:
- Retry 1: After 1 minute
- Retry 2: After 5 minutes
- Retry 3: After 30 minutes
- Retry 4: After 2 hours
- Retry 5: After 24 hours
That is up to six attempts in all: the first, then these five retries. After the sixth fails, the delivery is abandoned and is not sent again. We also check your URL before every attempt: a delivery to a host that does not resolve, or that resolves to a private, loopback, link-local or unspecified address, is dropped without a retry. A live webhook is deactivated after 20 of its deliveries in a row have failed in either of these ways; a successful delivery starts that count again. Your dashboard shows each live webhook's failure count and its recent deliveries, and its Reactivate button switches it back on once your endpoint is fixed.
Best Practices
- Respond quickly: Return 200 within 5 seconds. Process async if needed.
- Handle duplicates: deliveries are at-least-once, so the same event can reach you more than once. Make your handler idempotent: a payload whose
checkpointsis an array carries the shipment's full known history, so replace your stored checkpoints for that consignment with the payload's rather than appending, and keep the ones you hold whencheckpointsisnull. Applying the same payload twice then changes nothing. To also skip obvious retries,event+data.courier+data.tracking_number+enqueued_atis fixed for the life of an event, but it is not unique:enqueued_atis to the second, so two differenttracking.updatedevents for one consignment queued in the same second share it. Use it to skip repeated work, never to discard an event. Do not key ondata.statusor the checkpoint list: each retry rebuildsdatafrom our freshest record. - Verify signatures: Always validate the HMAC signature.
- Use HTTPS: Only configure HTTPS webhook URLs.
Error Handling
Errors return a consistent JSON structure with an error code and message.
{
"success": false,
"error": {
"code": "QUOTA_EXCEEDED",
"message": "Monthly quota of 100 requests exceeded. Upgrade your plan for more.\n\nThis same key works again as soon as you upgrade: https://api.trackcourier.io/pricing"
}
}
Error Codes
| Code | HTTP Status | Description |
|---|---|---|
INVALID_API_KEY |
401 | API key is invalid or missing |
QUOTA_EXCEEDED |
402 | Monthly request quota exceeded |
ACCOUNT_NEEDS_ATTENTION |
402 | The account needs attention. Email api@trackcourier.io |
COURIER_NOT_FOUND |
404 | Unknown courier slug. When we can tell which slug you meant, the error also carries did_you_mean. |
COURIER_NOT_SUPPORTED |
400 | Tracking for this courier is not available through the API. GET /v1/couriers lists every courier you can track. |
TRACKING_NOT_FOUND |
404 | Tracking number not found |
TIMEOUT |
504 | Courier backend timeout - retry |
SERVICE_ERROR |
502 | Temporary service error - retry |
Supported Couriers
Every plan, including Free, can track every courier below. Use the slug in your API requests.
bluedartdelhiverydtdcindiapostspeedpostxpressbeesshadowfaxekarttrackonshreemarutiprofessionalgatisafexpresscanadapostshreetirupatishreeanjanistcouriervrlskykingshreemahavirshreenandanshiprocketindiapostinternationaltciexpressomlogisticsvtransrivigotcifreightarccarriermadhurregisteredpostvxpressnitcoposlajugmsworldwidemarkexpressdpworld