XpressBees is one of the three big Indian e-commerce parcel carriers alongside Delhivery and, until its merger, Ecom Express. It grew out of FirstCry's in house logistics arm, spun out in 2015, and now carries parcels for marketplaces, D2C brands and aggregators across most of the country. For a commerce database it is a carrier connector: no orders, no listings, no stock. What it gives us is a serviceability answer with a real price attached, an AWB and a label when we book, an automatically raised pickup, a tracking history with per event status codes, cancellation and a set of actions on failed deliveries. Its API is a clean modern REST design, which makes it noticeably easier to work with than Blue Dart or DTDC, and the whole surface lives on one host.
At a glance
The path is shipments2, not shipments, for the current generation of the shipment API. Both route groups exist and both are token gated, which suggests shipments is the older version kept alive for compatibility. Every production integration read for this page uses shipments2. Use it.
What it is
XpressBees was founded in 2015 when FirstCry separated its delivery operation into a standalone company, and it scaled with the Indian e-commerce market rather than with a franchise network. It runs first mile pickup, its own sortation and line haul, and last mile delivery, plus a B2B and part truckload arm and a 3PL arm. It is a pure carrier: no marketplace, no catalogue, no storefront.
In a seller's stack XpressBees usually appears as one of two or three carriers in a rate shopping mix rather than as the sole carrier, chosen where it beats Delhivery on price or on a particular lane. It also appears heavily inside aggregators: Shiprocket alone exposes it as courier_company_id 23, 24, 25, 33 and 51 for its 1kg, 2kg, 5kg, standard and surface variants. If a brand tells you they ship with XpressBees, establish whether they hold a direct XpressBees login or reach it through an aggregator, because the two give completely different credentials and completely different remittance paths.
API access
Not self serve. You contract with XpressBees, and the account team issues an email address and password specifically for API use. There is no developer console, no key rotation UI, no scopes and no partner tier.
Base URL: https://shipment.xpressbees.com/api/.
Verified live on 2026-09-22, the route groups that exist and are token gated:
Anything outside those groups, for example /api/pincodes, /api/rate, /api/pickup or /api/shipping_label, returns a 404 HTML page rather than the JSON 401, which is how the boundaries above were established. The token gate sits above the router within each group, so the group names are confirmed but individual operation names within them are not; those come from production integrations and are marked as such below.
There are no official SDKs. There is no published API version beyond the 2 in shipments2, and no changelog.
Authentication
A plain credential exchange. Verified live: POST /api/users/login with an email shorter than 3 characters or a password shorter than 6 returns {"status":false,"message":"The Email field must be at least 3 characters in length.\nThe Password field must be at least 6 characters in length.\n"}, which confirms both the endpoint and the field names.
POST https://shipment.xpressbees.com/api/users/loginwithContent-Type: application/jsonand{"email": "...", "password": "..."}.- On success the response is
{"status": true, "data": "<token>"}. Note thatdatais the token string itself, not an object wrapping it, which trips up generic clients. - Send
Authorization: Bearer <token>on every later call. GET /api/users/logoutinvalidates it.
curl -X POST 'https://shipment.xpressbees.com/api/users/login' \
-H 'Content-Type: application/json' \
-d '{"email":"api-user@example.com","password":"<api-password>"}'
curl -X POST 'https://shipment.xpressbees.com/api/courier/serviceability' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer <token>' \
-d '{"origin":"110030","destination":"411001","payment_type":"cod","order_amount":"999","weight":"500","length":"20","breadth":"15","height":"5"}'
A missing or expired token returns HTTP 401 with {"status":false,"message":"Missing or invalid Token in request"}, verified live.
The token lifetime is not published and could not be measured without valid credentials. Production integrations take the pragmatic route of logging in again before every call rather than caching, which works but wastes a round trip; a better pattern is to cache the token, re-issue on the 401 message above, and measure the real lifetime once you have credentials.
Multi account is one email and password per XpressBees account. There is no application level identity and no consent flow.
GET /api/warehouse returned HTTP 200 with {"status":true,"data":[]} to a request carrying no Authorization header on 2026-09-22, while every other route group returned 401. The response was an empty list, so nothing leaked, but the endpoint does not appear to enforce the token gate the way its neighbours do. Do not rely on that behaviour, and treat the warehouse list as authenticated data regardless.
Objects we can read
Courier serviceability and rates
POST /api/courier/serviceability. This is the best endpoint on the API, because unlike Delhivery, Blue Dart and DTDC it returns an actual price per service, not just a yes or no.
Note the type discipline: pincodes and dimensions are sent as strings, not numbers. The response is an array of available services under data:
{
"status": true,
"data": [
{
"id": 1, "name": "XpressBees Surface",
"freight_charges": 54, "cod_charges": 25, "total_charges": 79,
"min_weight": 500, "chargeable_weight": 500
}
]
}
id is what you pass back as courier_id at shipment creation to force a service. min_weight and chargeable_weight are in grams, and chargeable_weight is where a volumetric uplift shows up before you book, which makes this endpoint useful as a pre booking cost check as well as a serviceability check.
POST /api/courier/courierlist returns the service catalogue. There is no pagination on either call.
Shipments and tracking
GET /api/shipments2/track/{awb}.
{
"status": true,
"data": {
"awb_number": "141123221084922",
"status": "Delivered", "shipment_info": "", "courier_id": 1,
"history": [
{
"status_code": "RAD",
"location": "Pune_Hub (Maharashtra)",
"event_time": "2026-09-21 09:12:00",
"message": "Reached at destination hub"
}
]
}
}
The design is a lookup by key: no pagination, no date window and no "changed since" query, so you track the AWBs you already know about. history[] is ordered oldest first, and production integrations read the last element as the current event. status_code is the machine value per event and data.status is the shipment level label; the two disagree in one documented case, where a cancelled shipment carries the cancellation in data.status while history still ends on whatever the last physical scan was. Prefer data.status when it starts with Cancel, and the latest history[].status_code otherwise.
There is no proof of delivery artefact: no signature image, no photo, no POD endpoint. The delivered event is all you get.
Locations
GET /api/warehouse returns the pickup locations registered against the account as {"status": true, "data": [...]}. See the warning above about its authentication behaviour. Warehouses can also be created implicitly: the pickup block inside a shipment creation request carries a warehouse_name, and XpressBees registers it if it does not already exist.
Orders, listings, inventory, returns, settlements
None. A reverse shipment is a shipment booked in the other direction, not a separate object. COD remittance and freight invoicing are handled through the XpressBees client panel and finance files, not over this API.
Writing back: listings, price and stock
Not applicable, this is a carrier. There are no listings, no prices and no stock. The write path is the shipment lifecycle.
Create a shipment
POST /api/shipments2 with the bearer token.
{
"order_number": "ORD-10231",
"payment_type": "cod", "order_amount": 999, "collectable_amount": "999",
"shipping_charges": 0, "discount": 0, "cod_charges": 25,
"package_weight": 500, "package_length": 20, "package_breadth": 15, "package_height": 5,
"request_auto_pickup": "yes",
"courier_id": "1",
"consignee": {
"name": "Ramesh Kumar", "company_name": "",
"address": "12, MG Road", "address_2": "",
"city": "Pune", "state": "Maharashtra",
"pincode": "411001", "phone": "9876543210"
},
"pickup": {
"warehouse_name": "MAIN-WH", "name": "Warehouse Manager",
"address": "Plot 7, Industrial Area",
"city": "Delhi", "state": "Delhi",
"pincode": "110030", "phone": "9811111111"
},
"order_items": [
{ "name": "Cotton shirt", "qty": "1", "price": "999", "sku": "SHIRT-BLUE-L" }
]
}
{
"status": true,
"data": {
"order_id": 1234567, "shipment_id": 7654321,
"awb_number": "141123221084922",
"courier_id": 1, "courier_name": "XpressBees Surface",
"status": "Manifested", "payment_type": "cod",
"additional_info": "", "label": "https://..."
}
}
Points that matter:
request_auto_pickup: "yes"folds the pickup request into creation. There is no separate pickup endpoint on this API, and/api/pickupreturns 404, so this flag is how a pickup gets raised. That is a real design difference from Delhivery and Shiprocket, where pickup is a distinct call.package_weightis in grams, while dimensions are in centimetres. Production integrations round every one of these up to at least 1 to avoid rejections.- Field length limits are enforced. Production clients truncate
order_numberto 20 characters,warehouse_nameto 20, names and addresses to 200, city and state to 40, SKU to 100, and strip phone numbers to the last 10 digits. Those limits are not published but are consistently applied, which suggests they are real and were learned the hard way. courier_idis optional. Omit it and XpressBees selects; pass theidfrom the serviceability response to force a service.labelin the response is the shipping label. Fetch and store it at creation; there is a separateshipments2/labelroute but the label arrives here anyway.- Always branch on the
statusboolean in the body, not on the HTTP status.
Cancel a shipment
POST /api/shipments2/cancel with {"awb": "141123221084922"} and the bearer token. The response carries a status boolean and a message.
Label and manifest
POST /api/shipments2/label and POST /api/shipments2/manifest both exist and are token gated, verified live. Their request shapes were not verifiable from a public source; the label also comes back inline on the creation response, which is the path production integrations take.
Act on a failed delivery
POST /api/ndr and POST /api/ndr/create both exist and are token gated, verified live. The action vocabulary and request shape were not verifiable from a public source. Expect the usual set (re-attempt, deferred delivery, address or phone correction, return to origin) and confirm against the documentation pack your account manager provides.
Webhooks and notifications
Not published, and nothing in the route map suggests a subscription endpoint. XpressBees does push status updates to large clients under separate arrangements; that is an account conversation, not an API feature.
Poll GET /api/shipments2/track/{awb}. The endpoint takes one AWB per call, so polling cost is linear in open shipments. With no published rate limit, a workable pattern is to poll in flight shipments every 60 to 120 minutes, stop at a terminal status, and spread calls rather than bursting. If your volume makes that impractical, ask for the push feed before building.
Rate limits and pagination
Neither is published.
- Pagination: none. Every read is a lookup by key or a small bounded list.
- Rate limits: not published, and no quota headers were observed. Implement your own token bucket and exponential backoff. Be especially careful with the login call, since an integration that logs in before every request multiplies its own request rate by two.
Mapping to the unified model
XpressBees contributes shipments and locations. orders, order_items, products, listings, inventory, returns, settlements and customers have no source here.
shipments
ship_to_address maps to the consignee block, which is PII.
locations
location_id and channel_location_id both map to pickup.warehouse_name, which is the key XpressBees uses and is capped at 20 characters. type is always warehouse, name is pickup.name, and address is assembled from pickup.address, city, state and pincode. GET /api/warehouse lists them.
Gaps and open questions
- No public documentation. Every field name here comes from probing the live API or from production integrations. The route groups, the login contract and the 401 envelope are verified; the bodies are not.
- NDR, label and manifest shapes are unknown. All three routes are confirmed to exist and to be token gated, and none could be exercised. Budget a discovery round once you have credentials.
- Token lifetime is unknown. Production integrations avoid the question by logging in constantly. Measure it and cache properly.
GET /api/warehouseanswered without a token. It returned an empty list, so this is a behaviour to note rather than an exposure, but it is inconsistent with every other route group and should be raised with XpressBees.- No sandbox. Every call is production, and creation with
request_auto_pickupraises a real pickup. - No settlement or COD remittance API, and no billed weight after booking, so cost reconciliation and weight disputes cannot be automated from this API alone. Capture
total_chargesat booking or lose it. - Two shipment route generations,
shipmentsandshipments2, with nothing published about the difference or any deprecation. Undocumented field length limits that production clients apply defensively. No published rate limits.
Sources
Verified live on 2026-09-22 by direct request:
POST https://shipment.xpressbees.com/api/users/login: returns field level validation messages namingEmailandPasswordand their minimum lengths, confirming the endpoint and its contracthttps://shipment.xpressbees.com/api/{shipments2, shipments, courier/serviceability, courier/courierlist, shipments2/cancel, shipments2/label, shipments2/manifest, shipments2/track/{awb}, ndr, ndr/create}: all return HTTP 401{"status":false,"message":"Missing or invalid Token in request"}, confirming the route groups and the bearer token gatehttps://shipment.xpressbees.com/api/{pincodes, rate, pickup, shipping_label, nonexistent}: all return a 404 HTML page, which bounds the API surfaceGET https://shipment.xpressbees.com/api/warehouse: HTTP 200{"status":true,"data":[]}with no Authorization header.POSTandPUTto the same path return HTTP 405{"status":false,"error":"Unknown method"}GET https://shipment.xpressbees.com/api/users/logout: HTTP 405 on POST and GET without a token, confirming the route existsPOST https://ship.xpressbees.com/api/users/login: HTTP 404{"status":false,"message":"Please retry this endpoint (https://shipment.xpressbees.com)"}
Secondary, production integrations read for request and response shapes:
- Ogpavan/iron_root_nutrition,
lib/xpressbees.ts: the login response shape with the token as a bare string indata, thecourier/serviceabilityrequest and response fields, the fullshipments2creation body includingrequest_auto_pickup,consignee,pickupandorder_items, the creation response fields, theshipments2/track/{awb}response withhistory[], and the field length limits applied defensively - shipnick/shipnick_2024,
app/Jobs/cancelordersProcess.php: an independent confirmation of the login contract and theBearerheader, and theshipments2/cancelbody withawb