HLD API Documentation
Hyperlocal Delivery (HLD) client integration guide: book jewellery deliveries with AL Kangaroo from your system, track them live and receive a webhook at every stage.
Overview
Recommended for new integrationsHLD (Hyperlocal Delivery) is AL Kangaroo's on-demand delivery inside a city. A Field Executive collects the parcel at your store and delivers it to your customer, usually within 120 minutes. Pickup and delivery are both confirmed with a 6-digit OTP and photo proof. API v2 lets your system check serviceability, book orders, follow every stage by webhook, read the Field Executive's live location, cancel orders and pull service reports.
Base URL
Sandbox (start your integration here)
Production (shared when your integration is signed off)
https://staging.alfast.in/api/v2/hld runs the same API on test data. Nothing there is delivered by a real Field Executive: you move test orders through their stages with the simulator and receive the same webhooks as in production. The sandbox has its own API token; ask the AL Kangaroo team for one.
Endpoints
POST Serviceability
Are the store and the customer address served (or the store alone), and the promised times
GET Service Zones
The served areas as GeoJSON polygons, to check addresses on your side
POST Create Order
Book an order; returns the OTPs and both tracking links
GET Order Details
The full order: status, Field Executive, proof, failures, return
PATCH Edit Order
Change the customer, drop point, COD or package before pickup
GET Live Location
Light answer for polling the Field Executive's position
WSS Live Updates
Subscribe once per order; position and status are pushed until it ends
GET Events
Every event of an order, to catch up after an outage
POST Ready to Ship
For an order booked before packing: the parcel is packed, send a Field Executive
POST Cancel Order
Cancel before pickup; after pickup the parcel returns to the store
GET KPI Report
Service figures for your orders over a date range
POST Webhooks
Sent by AL Kangaroo to your server at every stage
Typical Integration
- Check serviceability for the store and the customer address.
- Create the order with your own order number. You receive the AL order id, the pickup and delivery OTPs and two tracking links.
- Receive a webhook at every stage: Field Executive assigned, at the store, picked up, out for delivery, near the customer, delivered, failed attempt, re-attempt, return to store, cancelled.
- Read the order details or the live location whenever you need them, and replay events after an outage on your side.
Conventions
| Topic | Rule |
|---|---|
| Format | Requests and responses are JSON. Send Content-Type: application/json. |
| Times | ISO 8601 in India time, for example 2026-10-17T10:30:04+05:30. |
| Success | {"success": true, "data": {...}} |
| Error | {"success": false, "error_code": "...", "message": "..."}; validation errors add errors per field. See Errors. |
{id} in a path | Either the AL order id (ALHLD...) or your own partner_order_id. |
| Rate limit | 600 requests per minute per API token. Above it the API answers 429; wait for the number of seconds in the Retry-After header. |
| Idempotency | One AL order per partner_order_id, for ever. Retrying a create after a timeout is safe: you get the same order back. |
| Naming | Some field and event names say rider (for example rider_location, RIDER_ASSIGNED). They refer to the Field Executive. |
Moving from v1
| Topic | v1 | v2 |
|---|---|---|
| API token | The same token works for both versions. Nothing new to request. | |
| Your order number | customer_order_no | partner_order_id (customer_order_no is still accepted) |
| Repeated order number | Rejected for 5 minutes | Returns the existing order (200, "duplicate": true) |
| Cancel after pickup | Not allowed | Starts the return to the store, with a return OTP |
| Tracking links | Customer link only | Customer link and a private link for your team |
| Webhooks | Generic order.* events | One named event per stage, with event_id, sequence and the full order, optionally signed |
Authentication
Send your client API token as a Bearer token on every request. It is the same token as the v1 client API. Contact the AL Kangaroo team to get a token or to replace one.
Authorization: Bearer {your_api_token} Content-Type: application/json Accept: application/json
curl -X POST https://delivery.alfast.in/api/v2/hld/serviceability \
-H "Authorization: Bearer $AL_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"pickup_lat":18.5362,"pickup_lng":73.8939,"drop_lat":18.5485,"drop_lng":73.9020}'
Keep the token on your server only; never put it in a mobile app or a web page.
| HTTP | error_code | When |
|---|---|---|
| 401 | UNAUTHENTICATED | No Authorization: Bearer header |
| 401 | INVALID_TOKEN | The token is not known |
| 403 | CLIENT_DEACTIVATED | Your account is deactivated |
| 403 | HLD_NOT_ENABLED | Hyperlocal delivery is not enabled for your account |
// 401
{
"success": false,
"message": "Unknown API token",
"error_code": "INVALID_TOKEN"
}
Serviceability
Whether AL Kangaroo serves the pickup and the drop, the zone and city, and the pickup and delivery times promised there. The answer does not depend on which Field Executives are online at that moment: an order placed while none is online waits for the next free one.
Pickup only: leave out drop_lat and drop_lng to check just the store location, for example when you add a new store. See Pickup only below.
Same rule as Create Order: if this says
serviceable: true, Create Order accepts the order.
Request Parameters
| Field | Type | Required | Description |
|---|---|---|---|
pickup_lat | number | Required | Store latitude in decimal degrees, e.g. 18.5362 |
pickup_lng | number | Required | Store longitude in decimal degrees |
drop_lat | number | Optional | Customer latitude in decimal degrees. Send it together with drop_lng, or leave both out to check the pickup only. |
drop_lng | number | Optional | Customer longitude in decimal degrees. Required when drop_lat is sent. |
Sample Request
{
"pickup_lat": 18.5362,
"pickup_lng": 73.8939,
"drop_lat": 18.5485,
"drop_lng": 73.9020
}
Success Response 200 OK
{
"success": true,
"data": {
"serviceable": true,
"reason": null,
"city": "Pune",
"zone": { "zone_code": "PUN-HLD-001", "zone_name": "Pune Central", "city": "Pune" },
"pickup": {
"serviceable": true,
"zone": { "zone_code": "PUN-HLD-001", "zone_name": "Pune Central", "city": "Pune" }
},
"drop": {
"serviceable": true,
"zone": { "zone_code": "PUN-HLD-001", "zone_name": "Pune Central", "city": "Pune" }
},
"promised": { "pickup_minutes": 30, "delivery_minutes": 90 },
"pickup_estimate": { "riders_available": true, "eta_minutes": 11, "eta_at": "2026-10-17T10:41:00+05:30" },
"delivery_estimate": { "distance_km": 2.4, "drive_minutes": 8, "eta_minutes": 24, "eta_at": "2026-10-17T10:54:00+05:30" }
}
}
Pickup Estimate
pickup_estimate is live: how soon a Field Executive could reach the store now, from the nearest one who is free (online, not carrying an order, not answering another offer) in the pickup zone. Use it for your pre-order promise; promised stays the fixed service level.
| Field | Description |
|---|---|
riders_available | true when a free Field Executive is near the store now. false does not stop the order: it books and is assigned as soon as one is free. |
eta_minutes | Minutes until a Field Executive could be at the store: time to accept the order plus the drive. null when riders_available is false. |
eta_at | The same as a time (ISO 8601, India time), or null |
The estimate changes from minute to minute; ask again right before you book. The number of Field Executives is not shared. pickup_estimate is null when the order is not serviceable.
Delivery Estimate (for your customer promise)
Sent when you give the drop point. Build the time you promise your customer from it.
| Field | Description |
|---|---|
distance_km | Road distance from the store to the customer (estimated) |
drive_minutes | Drive time from the store to the customer |
eta_minutes | Minutes until delivery if you book now: pickup_estimate.eta_minutes + handover at the store (about 5 minutes for OTP and photo) + drive_minutes. null when no Field Executive is free now. |
eta_at | The same as a time (ISO 8601, India time), or null |
Promise = time until your parcel is packed (your side) + eta_minutes. If you book before packing (Ready to Ship), count the delivery from the moment you mark it ready. delivery_estimate is null without a drop point or when the order is not serviceable.
Not Serviceable 200 OK, serviceable = false
The HTTP status is still 200: check serviceable. reason says which end failed, and pickup.reason / drop.reason say why. promised is null.
{
"success": true,
"data": {
"serviceable": false,
"reason": "HLD service not available at delivery location",
"city": "Pune",
"zone": { "zone_code": "PUN-HLD-001", "zone_name": "Pune Central", "city": "Pune" },
"pickup": {
"serviceable": true,
"zone": { "zone_code": "PUN-HLD-001", "zone_name": "Pune Central", "city": "Pune" }
},
"drop": {
"serviceable": false,
"reason": "No service zone found for this location",
"alternatives": [
{ "zone_code": "PUN-HLD-003", "zone_name": "Pune East", "city": "Pune", "distance_km": 6.42 }
]
},
"promised": null,
"pickup_estimate": null
}
}
pickup.reason / drop.reason | Meaning |
|---|---|
| No service zone found for this location | The point is outside every HLD zone; alternatives lists the nearest zones |
| Service not available in this zone at this time | The zone is closed now; service_hours gives start and end (HH:MM, India time) |
Pickup Only
Send only pickup_lat and pickup_lng. serviceable, zone, city and promised then describe the pickup point, and drop is null. Check the customer address again (with the drop point, or with Create Order) before you book.
{
"pickup_lat": 18.5362,
"pickup_lng": 73.8939
}
{
"success": true,
"data": {
"serviceable": true,
"reason": null,
"city": "Pune",
"zone": { "zone_code": "PUN-HLD-001", "zone_name": "Pune Central", "city": "Pune" },
"pickup": {
"serviceable": true,
"zone": { "zone_code": "PUN-HLD-001", "zone_name": "Pune Central", "city": "Pune" }
},
"drop": null,
"promised": { "pickup_minutes": 30, "delivery_minutes": 90 },
"pickup_estimate": { "riders_available": true, "eta_minutes": 11, "eta_at": "2026-10-17T10:41:00+05:30" }
}
}
A pickup outside every zone answers serviceable: false with reason "HLD service not available at pickup location", and pickup.alternatives lists the nearest zones. Sending only one of drop_lat / drop_lng is a 422 VALIDATION_FAILED.
Service Zones
The areas AL Kangaroo serves, as a GeoJSON FeatureCollection, so you can check addresses on your side (for example to show or hide HLD at checkout). These are the same zones Serviceability uses. Fetch the list once a day; Serviceability stays the final word for one address.
Sandbox: https://staging.alfast.in/api/v2/hld/zones
Success Response 200 OK
{
"success": true,
"data": {
"type": "FeatureCollection",
"generated_at": "2026-10-17T12:05:00+05:30",
"features": [
{
"type": "Feature",
"geometry": {
"type": "Polygon",
"coordinates": [[ [73.8567, 18.9701], [73.9055, 18.9679], "...", [73.8567, 18.9701] ]]
},
"properties": {
"zone_code": "PUN-HLD-001",
"zone_name": "Pune HLD Zone",
"city": "Pune",
"shape": "circle",
"center": { "lat": 18.5204, "lng": 73.8567 },
"radius_km": 50,
"service_hours": { "start": "00:00", "end": "23:59" }
}
}
]
}
}
| Field | Description |
|---|---|
geometry.coordinates | One closed ring of [lng, lat] points (GeoJSON order: longitude first). A point inside the ring is served. |
properties.shape | polygon: the zone's drawn boundary. circle: the zone is a circle around center with radius_km, given as a 64-point polygon; you may also test the distance to center yourself. |
properties.service_hours | Daily hours (HH:MM, India time) when orders are taken in the zone |
Create Order
Books the order. AL Kangaroo finds the nearest free Field Executive automatically. The response is the full order plus duplicate.
New order: HTTP
201, "duplicate": false.Order number already used: HTTP
200, "duplicate": true and the existing order. Nothing new is booked, so a retry after a timeout is safe.
Request Parameters
| Field | Type | Required | Description |
|---|---|---|---|
| Order | |||
partner_order_id | string, max 100 | Required | Your order number. One AL order per number, for ever (also accepted as customer_order_no). |
product_name | string, max 255 | Required | Short description of the parcel contents |
product_value | number | Required | Declared value in INR, minimum 1 |
payment_mode | string | Optional | Prepaid, COD or Partial. When omitted: COD if cod_value > 0, otherwise Prepaid. |
cod_value | number | Optional | Cash the Field Executive collects from the customer, in INR (minimum 0, default 0) |
special_instructions | string, max 500 | Optional | Handling notes, shown to the Field Executive |
| Customer (drop) | |||
contact_person | string, max 100 | Required | Customer name |
contact_number | string, max 15 | Required | Customer mobile number |
delivery_address | string, max 500 | Required | Full delivery address |
delivery_pincode | string, max 6 | Optional | Delivery pincode |
delivery_lat | number | Required | Delivery latitude in decimal degrees |
delivery_lng | number | Required | Delivery longitude in decimal degrees |
| Pickup (store) | |||
pickup_address | string, max 500 | Required | Full pickup address |
pickup_pincode | string, max 6 | Optional | Pickup pincode |
pickup_lat | number | Required | Pickup latitude in decimal degrees |
pickup_lng | number | Required | Pickup longitude in decimal degrees |
pickup_store_code | string, max 50 | Optional | Your store id |
pickup_store_name | string, max 150 | Optional | Your store name |
pickup_contact_name | string, max 100 | Optional | Person at the store who hands over the parcel |
pickup_contact_number | string, max 15 | Optional | That person's phone number |
pickup_window_start | date-time | Optional | When the parcel is ready (the packing time), e.g. 2026-10-17T10:45:00+05:30. With "ready_to_ship": false a Field Executive is looked for 20 minutes before this time, see Ready to Ship. |
pickup_window_end | date-time | Optional | End of the pickup window; must be after pickup_window_start |
| Delivery Preferences | |||
expected_delivery_minutes | integer | Optional | Promised delivery time in minutes, 15 to 120 (default 120). Used for the on-time figure in the KPI report. |
priority_level | integer | Optional | 1 Normal (default), 2 High, 3 Urgent |
requires_signature | boolean | Optional | Customer signature needed at delivery (default false) |
delivery_preferences | JSON string | Optional | Extra preferences as a JSON-encoded string |
| Package | |||
package_count | integer | Optional | Number of packages, 1 to 50 (default 1) |
shipment_category | string | Optional | One of jewellery, repair_return, store_transfer, other |
awb_number | string, max 100 | Optional | The AWB / tracking number on your own shipping label. Shown to the Field Executive at pickup to match the parcel, and returned in package.awb_number. |
bag_id | string, max 100 | Optional | The bag the parcel travels in, if you use bags. Shown at pickup and returned in package.bag_id. |
ready_to_ship | boolean | Optional | Default true: the parcel is packed and a Field Executive is sent at once (manifest after packing). Send false to book at order creation, before packing: no Field Executive is sent until you say the parcel is packed (Ready to Ship), or until 20 minutes before pickup_window_start when you send it, whichever comes first. |
weight | number | Optional | Weight in kg |
length / width / height | number | Optional | Dimensions in cm |
| Your own OTPs | |||
pickup_otp | string, exactly 6 digits | Optional | The OTP the store gives the Field Executive at pickup. Left out: AL makes one. |
delivery_otp | string, exactly 6 digits | Optional | The OTP the customer gives the Field Executive at delivery. Left out: AL makes one. |
return_otp | string, exactly 6 digits | Optional | The OTP the store gives when the parcel comes back (return to store). Left out: AL makes one when a return starts. |
Coordinates must be decimal degrees with a decimal point (18.5362, not 18). Latitude -90 to 90, longitude -180 to 180.
Sample Request
{
"partner_order_id": "ORD-48213",
"product_name": "Smartphone (sealed box)",
"product_value": 32999,
"payment_mode": "Prepaid",
"contact_person": "Neha Joshi",
"contact_number": "9876543210",
"delivery_address": "Flat 302, Green Meadows, Kalyani Nagar, Pune",
"delivery_pincode": "411006",
"delivery_lat": 18.5485,
"delivery_lng": 73.9020,
"pickup_address": "Shop 7, Lane 5, Koregaon Park, Pune",
"pickup_pincode": "411001",
"pickup_lat": 18.5362,
"pickup_lng": 73.8939,
"pickup_store_code": "KP-07",
"pickup_store_name": "Koregaon Park store",
"pickup_contact_name": "Store desk",
"pickup_contact_number": "9822012345",
"package_count": 1,
"shipment_category": "other",
"weight": 0.6,
"special_instructions": "Call the customer 5 minutes before arrival"
}
Success Response 201 Created
{
"success": true,
"data": {
"duplicate": false,
"order_id": "ALHLD2610171030",
"partner_order_id": "ORD-48213",
"status": { "event": "ORDER_CREATED", "code": 13, "text": "Finding a rider" },
"tracking_url": "https://delivery.alfast.in/track/hld/9v72Px9CSnS3tvnhhpXwuYUE9dXsTdq8",
"tracking": {
"token": "9v72Px9CSnS3tvnhhpXwuYUE9dXsTdq8",
"customer_url": "https://delivery.alfast.in/track/hld/9v72Px9CSnS3tvnhhpXwuYUE9dXsTdq8",
"client_token": "Ec89McE7AviFx27ax9ZbnZoDW237TDKQ9Hv8cMay",
"client_url": "https://delivery.alfast.in/track/hld/client/Ec89McE7AviFx27ax9ZbnZoDW237TDKQ9Hv8cMay"
},
"created_at": "2026-10-17T10:30:04+05:30",
"updated_at": "2026-10-17T10:30:05+05:30",
"pickup": {
"store_code": "KP-07",
"store_name": "Koregaon Park store",
"address": "Shop 7, Lane 5, Koregaon Park, Pune",
"pincode": "411001",
"lat": 18.5362,
"lng": 73.8939,
"contact_name": "Store desk",
"contact_number": "9822012345",
"window_start": null,
"window_end": null
},
"drop": {
"address": "Flat 302, Green Meadows, Kalyani Nagar, Pune",
"pincode": "411006",
"lat": 18.5485,
"lng": 73.902,
"contact_name": "Neha Joshi",
"contact_number": "9876543210"
},
"package": { "description": "Smartphone (sealed box)", "declared_value": 32999, "count": 1, "category": "other", "weight_kg": 0.6, "awb_number": "MYN7788120045", "bag_id": "BAG-0042" },
"payment": { "mode": "Prepaid", "cod_amount": 0, "cod_collected": false, "cod_collected_amount": null },
"otp": {
"pickup_otp": "482913",
"delivery_otp": "739204",
"pickup_verified": false,
"pickup_verified_at": null,
"delivery_verified": false,
"delivery_verified_at": null
},
"rider": null,
"rider_location": null,
"eta": { "pickup_at": null, "delivery_at": null },
"picked_up_at": null,
"delivered_at": null,
"proof": { "pickup_photos": [], "delivery_photos": [] },
"failure": null,
"rto": null,
"cancellation": null,
"timeline": [
{ "event": "ORDER_CREATED", "sequence": 1, "status_code": 1, "occurred_at": "2026-10-17T10:30:04+05:30" }
]
}
}
- Give the store the
otp.pickup_otpand the customer theotp.delivery_otp(6 digits each). The Field Executive cannot complete the pickup or the delivery without them. - If your app already shows OTPs to the customer and the store, send them in
pickup_otp,delivery_otpandreturn_otp; the response then carries those same codes. Each must be exactly 6 digits, otherwise the answer is422 VALIDATION_FAILEDwith the field inerrors. The OTPs cannot be changed after the order is booked. status.codeis often already13(a Field Executive is being asked) when the response comes back; the event staysORDER_CREATEDuntil one accepts.tracking.customer_urlis for your customer;tracking.client_urlis for your own team only. See Tracking Links.tracking_urlis the same astracking.customer_url.
Error Responses
422 - Validation Error
{
"success": false,
"error_code": "VALIDATION_FAILED",
"message": "Validation failed",
"errors": {
"product_value": ["The product value field is required."],
"delivery_lat": ["The delivery lat field is required."]
}
}
422 - Service Not Available
{
"success": false,
"error_code": "SERVICE_NOT_AVAILABLE",
"message": "HLD service not available at delivery location",
"data": {
"pickup": { "serviceable": true, "zone": { "zone_code": "PUN-HLD-001", "zone_name": "Pune Central", "city": "Pune" } },
"drop": { "serviceable": false, "reason": "No service zone found for this location", "alternatives": [] }
}
}
500 - Order Could Not Be Booked
{
"success": false,
"error_code": "ORDER_CREATION_FAILED",
"message": "The order could not be booked. Please retry with the same partner_order_id."
}
Order Details
The full order. Create and Cancel return the same shape, and every webhook carries it in order (without the OTP codes). {id} is the AL order id or your partner_order_id.
Or: https://delivery.alfast.in/api/v2/hld/orders/ORD-48213
Success Response 200 OK
An order that is out for delivery:
{
"success": true,
"data": {
"order_id": "ALHLD2610171030",
"partner_order_id": "ORD-48213",
"status": { "event": "OUT_FOR_DELIVERY", "code": 25, "text": "Out for delivery" },
"tracking_url": "https://delivery.alfast.in/track/hld/9v72Px9CSnS3tvnhhpXwuYUE9dXsTdq8",
"tracking": {
"token": "9v72Px9CSnS3tvnhhpXwuYUE9dXsTdq8",
"customer_url": "https://delivery.alfast.in/track/hld/9v72Px9CSnS3tvnhhpXwuYUE9dXsTdq8",
"client_token": "Ec89McE7AviFx27ax9ZbnZoDW237TDKQ9Hv8cMay",
"client_url": "https://delivery.alfast.in/track/hld/client/Ec89McE7AviFx27ax9ZbnZoDW237TDKQ9Hv8cMay"
},
"created_at": "2026-10-17T10:30:04+05:30",
"updated_at": "2026-10-17T10:58:41+05:30",
"pickup": { "store_code": "KP-07", "store_name": "Koregaon Park store", "address": "Shop 7, Lane 5, Koregaon Park, Pune",
"pincode": "411001", "lat": 18.5362, "lng": 73.8939, "contact_name": "Store desk",
"contact_number": "9822012345", "window_start": null, "window_end": null },
"drop": { "address": "Flat 302, Green Meadows, Kalyani Nagar, Pune", "pincode": "411006", "lat": 18.5485, "lng": 73.902,
"contact_name": "Neha Joshi", "contact_number": "9876543210" },
"package": { "description": "Smartphone (sealed box)", "declared_value": 32999, "count": 1, "category": "other", "weight_kg": 0.6, "awb_number": "MYN7788120045", "bag_id": "BAG-0042" },
"payment": { "mode": "Prepaid", "cod_amount": 0, "cod_collected": false, "cod_collected_amount": null },
"ready_to_ship": true,
"ready_to_ship_at": "2026-10-17T10:30:04+05:30",
"otp": { "pickup_otp": "482913", "delivery_otp": "739204",
"pickup_verified": true, "pickup_verified_at": "2026-10-17T10:58:40+05:30",
"delivery_verified": false, "delivery_verified_at": null },
"rider": { "id": 203, "name": "Sachin More", "phone": "9890011223", "photo_url": "https://delivery.alfast.in/storage/executives/203.jpg", "vehicle": null },
"rider_location": { "lat": 18.5431, "lng": 73.8987, "accuracy_m": 8, "speed_kmph": 22, "bearing": 35,
"updated_at": "2026-10-17T11:02:12+05:30" },
"eta": { "pickup_at": "2026-10-17T10:52:10+05:30", "delivery_at": "2026-10-17T12:58:40+05:30" },
"picked_up_at": "2026-10-17T10:58:40+05:30",
"delivered_at": null,
"proof": { "pickup_photos": ["https://delivery.alfast.in/storage/hld/ALHLD2610171030/pickup_1.jpg"], "delivery_photos": [] },
"failure": null,
"rto": null,
"cancellation": null,
"timeline": [
{ "event": "ORDER_CREATED", "sequence": 1, "status_code": 1, "occurred_at": "2026-10-17T10:30:04+05:30" },
{ "event": "RIDER_ASSIGNED", "sequence": 2, "status_code": 2, "occurred_at": "2026-10-17T10:31:10+05:30" },
{ "event": "HEADING_TO_PICKUP", "sequence": 3, "status_code": 24, "occurred_at": "2026-10-17T10:42:10+05:30" },
{ "event": "PICKED_UP", "sequence": 4, "status_code": 3, "occurred_at": "2026-10-17T10:58:40+05:30" },
{ "event": "OUT_FOR_DELIVERY", "sequence": 5, "status_code": 25, "occurred_at": "2026-10-17T10:58:41+05:30" }
]
}
}
Blocks
| Field | Description |
|---|---|
status | event = the stage the order is in (see Status & Event Codes), code = internal status number, text = short description |
tracking | Both tracking links, see Tracking Links |
pickup / drop | Store and customer as booked |
package / payment | Parcel and payment. package.awb_number and package.bag_id are your own references, null when not sent. cod_collected and cod_collected_amount are filled after a COD delivery with the cash the Field Executive recorded. |
ready_to_ship / ready_to_ship_at | false while an order booked before packing waits for Ready to Ship; then true with the time. Orders booked as packed show true and their booking time. |
otp | Pickup and delivery OTP codes (API only, never in webhooks) and whether and when each was verified |
rider | The Field Executive (id, name, phone, photo_url) from the moment one accepts until the order ends. null while one is being found or after cancellation. vehicle is not recorded yet and is always null. |
rider_location | The Field Executive's last position while working on the order: on the way to the store, carrying the parcel, and on the way back to the store. null otherwise, including while waiting after a failed attempt. |
eta | Expected pickup and delivery times, once known |
proof | Pickup photos after PICKED_UP; delivery photos after DELIVERED |
failure | After a failed delivery attempt, and kept afterwards: reason_code (see failure reasons), reason (text with the Field Executive's note), attempts, failed_at, photos |
rto | Once the parcel is going back to the store: reason_code, reason, initiated_by (client, al_services, system), initiated_at, return_otp (API only), return_verified, returned_at, photos. Keys without a value are left out. |
cancellation | After cancellation: cancelled_by (client, customer, al_services, system), reason_code, reason, cancelled_at |
timeline | Every event so far, oldest first, with its sequence |
Error Response 404 Not Found
{
"success": false,
"error_code": "ORDER_NOT_FOUND",
"message": "No order with this id for your account"
}
Edit Order
Changes an order before the Field Executive picks it up. Send only the fields you want to change; everything else stays as it is. {id} is the AL order id or your partner_order_id. The response is the full order plus updated_fields.
Sandbox: https://staging.alfast.in/api/v2/hld/orders/{id}
| Order is | Result |
|---|---|
| Before pickup status 1, 12, 15, 23, 13, 10, 2, 24 | The order is changed. 200 with the order and updated_fields. A Field Executive already on the way to the store is told and sees the new details at once. |
| Picked up, delivered, returned or cancelled | 409 ORDER_NOT_EDITABLE. Once the parcel is picked up, cancel the order instead (the parcel goes back to the store) and book a new one. |
Fields You Can Change
Same rules as in Create Order. A field you send may not be empty, except those marked clearable (send null to clear them).
| Field | Type | Notes |
|---|---|---|
| Customer | ||
contact_person | string, max 100 | Customer name |
contact_number | string, max 15 | Customer phone |
special_instructions | string, max 500 | Clearable |
| Drop point | ||
delivery_address | string, max 500 | Address text |
delivery_pincode | string, 6 digits | Clearable |
delivery_lat, delivery_lng | number | Send both. The new point must be served from the order's pickup point, else 422 SERVICE_NOT_AVAILABLE and nothing changes. Distance and drive time are worked out again. |
| Payment | ||
cod_value | number, ≥ 0 | Amount to collect. Sent alone, payment_mode follows it: above 0 → COD, 0 → Prepaid. |
payment_mode | COD, Prepaid, Partial | Prepaid sent alone sets cod_value to 0. COD / Partial need a cod_value above 0, Prepaid needs 0 (else 422 INVALID_PAYMENT). |
| Package | ||
product_name | string, max 255 | What is being sent |
product_value | number, ≥ 1 | Declared value |
length, width, height, weight | number, ≥ 0 | Clearable |
package_count | integer, 1 to 50 | Number of packages |
awb_number, bag_id | string, max 100 | Clearable. Your label and bag references. |
| Pickup contact and time window | ||
pickup_contact_name, pickup_contact_number | string | Clearable |
pickup_window_start, pickup_window_end | datetime | Clearable. The end must stay after the start, also against the value already stored (else 422 INVALID_PICKUP_WINDOW). For an order booked before packing, a new pickup_window_start moves the time a Field Executive is looked for; one less than 20 minutes away sends the order out at once. |
| Packing | ||
ready_to_ship | true | For an order booked before packing: the parcel is packed, a Field Executive is looked for at once. Same as Ready to Ship; updated_fields then lists ready_to_ship. Only true is accepted (false gives 422 VALIDATION_FAILED); for an order already out it changes nothing. |
Cannot be changed: partner_order_id, the pickup address and location, the OTPs. Sending them gives 422 VALIDATION_FAILED with the field in errors. To move the pickup point, cancel and book a new order.
Sample Request
{
"contact_number": "9822000111",
"delivery_address": "Flat 9, Aundh, Pune 411007",
"delivery_lat": 18.5590,
"delivery_lng": 73.8070,
"cod_value": 2000
}
Success Response 200 OK
{
"success": true,
"data": {
"updated_fields": ["cod_value", "contact_number", "delivery_address", "delivery_lat", "delivery_lng", "payment_mode"],
"order_id": "ALHLD2610171030",
"partner_order_id": "ORD-48213",
"status": { "event": "HEADING_TO_PICKUP", "code": 24, "text": "Rider on the way to pickup" },
"drop": {
"address": "Flat 9, Aundh, Pune 411007",
"pincode": "411007",
"lat": 18.559,
"lng": 73.807,
"contact_name": "Asha Patil",
"contact_number": "9822000111"
},
"payment": { "mode": "COD", "cod_amount": 2000, "cod_collected": false, "cod_collected_amount": null },
"...": "the other order fields as in Order Details"
}
}
updated_fieldslists only what really changed. Sending the values the order already has returns200withupdated_fields: [], so a retry is safe.- An edit is logged as an
ORDER_UPDATEDevent (see Events) and shows on your team's tracking page. No webhook is sent for it, since the change came from you.
Error Responses
{
"success": false,
"error_code": "ORDER_NOT_EDITABLE",
"message": "The order can be changed only before the rider picks it up",
"data": {
"status": { "event": "PICKED_UP", "code": 3, "text": "Picked up" }
}
}
{
"success": false,
"error_code": "VALIDATION_FAILED",
"message": "Validation failed",
"errors": { "pickup_lat": ["pickup_lat cannot be changed"] }
}
Live Location
A light answer for showing the Field Executive on a map. Poll it every poll_after_seconds (5) while the order is in progress. That is as fresh as the data gets: no websocket is needed.
Success Response 200 OK
{
"success": true,
"data": {
"order_id": "ALHLD2610171030",
"partner_order_id": "ORD-48213",
"status": { "event": "OUT_FOR_DELIVERY", "code": 25, "text": "Out for delivery" },
"rider_location": {
"lat": 18.5431,
"lng": 73.8987,
"accuracy_m": 8,
"speed_kmph": 22,
"bearing": 35,
"updated_at": "2026-10-17T11:02:12+05:30"
},
"eta": { "pickup_at": "2026-10-17T10:52:10+05:30", "delivery_at": "2026-10-17T12:58:40+05:30" },
"poll_after_seconds": 5
}
}
| Field | Description |
|---|---|
rider_location | The Field Executive's last position. accuracy_m, speed_kmph and bearing can be null. null before a Field Executive accepts, while waiting after a failed attempt, and after the order ends. |
poll_after_seconds | How long to wait before the next call |
Stop on a final event:
DELIVERED, CANCELLED or RTO_COMPLETED. During an order the Field Executive's app sends its position about every 5 seconds, so polling more often than every 5 seconds returns the same position.Rate limit
One order polled every 5 seconds is 12 requests a minute. Within the limit of 600 requests a minute per API token you can follow about 50 orders at the same time; poll only orders that are on the way, and use webhooks for the stages. Need more? Ask the AL Kangaroo team.
Many orders on a map? Use Live Updates (WebSocket) instead of polling: subscribe once per order and every new position is pushed to you.
No map of your own? Send your customer
tracking.customer_url (see Tracking Links): that page shows the Field Executive live, pushed as it moves.
Live Updates (WebSocket)
Subscribe once per order and receive the Field Executive's position (about every 5 seconds while the order is on the way), every status change and the arrival at the store and at the customer, pushed to you until the order ends. No polling: following 100 orders is 100 subscriptions on one connection, not 100 requests every 5 seconds. Our own customer tracking page works this way.
The server speaks the Pusher WebSocket protocol (Laravel Reverb), so any Pusher client library works: pusher-js for web and Node.js, the Pusher SDKs for Android and iOS.
Connection
| Production | Sandbox | |
|---|---|---|
| Host | delivery.alfast.in | staging.alfast.in |
| Port / TLS | 443, TLS (wss://) | 443, TLS (wss://) |
| App key | cfa29167b95250425708 | vi9qe4bew6eh7whspyv3 |
| URL | wss://delivery.alfast.in/app/cfa29167b95250425708?protocol=7 | wss://staging.alfast.in/app/vi9qe4bew6eh7whspyv3?protocol=7 |
| Channel | hld.order.{token}, where {token} is tracking.token from Create Order or Order Details. Public channel: no auth call; the long random token is the key, as for the tracking link. | |
Example (pusher-js)
import Pusher from 'pusher-js';
const pusher = new Pusher('cfa29167b95250425708', {
wsHost: 'delivery.alfast.in',
wsPort: 443,
wssPort: 443,
forceTLS: true,
enabledTransports: ['ws', 'wss'],
cluster: 'mt1', // required by the library, not used by our server
});
// tracking.token from Create Order
const channel = pusher.subscribe('hld.order.' + order.tracking.token);
channel.bind('executive.location_updated', (e) => {
moveMarker(e.location.lat, e.location.lng, e.location.bearing);
});
channel.bind('hld.order.status_changed', (e) => {
showStatus(e.order.status); // status code, see Status & Event Codes
if ([4, 9, 11, 7, 17, 30, 32].includes(e.order.status)) {
pusher.unsubscribe('hld.order.' + order.tracking.token); // final: stop
}
});
channel.bind('hld.order.arrived', (e) => {
// e.leg is "pickup" (at the store) or "drop" (at the customer)
});
On Android and iOS use the Pusher SDK with the same host, port 443, TLS on and the app key above (set the host instead of a cluster).
Events
| Event | When | Data |
|---|---|---|
executive.location_updated | Each new position from the Field Executive's phone, about every 5 seconds while the order is in progress | order_id, location.lat, location.lng, location.accuracy (m), location.speed, location.bearing, location.last_updated, timestamp |
hld.order.status_changed | Every status change | order.id, order.status, order.previous_status (status codes, see Status & Event Codes), order.status_text, timestamp |
hld.order.arrived | The Field Executive came within 200 m of the store (leg: "pickup") or the customer (leg: "drop") | leg, at, distance_m, title, message, timestamp |
{
"order_id": "ALHLD2610171030",
"location": {
"lat": 18.5431,
"lng": 73.8987,
"accuracy": 8,
"speed": 22,
"bearing": 35,
"last_updated": "2026-10-17T05:32:12.000000Z"
},
"timestamp": "2026-10-17T05:32:12.402119Z"
}
{
"order": {
"id": "ALHLD2610171030",
"tracking_token": "9v72Px9CSnS3tvnhhpXwuYUE9dXsTdq8",
"status": 25,
"previous_status": 3,
"status_text": "Out for Delivery"
},
"timestamp": "2026-10-17T05:30:02.711414Z"
}
• Times in WebSocket events are UTC (
Z); the REST API uses India time.•
status_text here is the customer wording; build your logic on order.status.• Live updates are for showing the order live. Webhooks stay the record of each stage: they are signed and retried, a socket message is not. After a reconnect, read Order Details once to catch up.
• Stop (unsubscribe) on a final status: 4, 9, 11 delivered; 7, 17, 30 cancelled; 32 returned to the store.
• The sandbox simulator moves the status only; positions come from a real Field Executive app.
Events
Every event of the order, oldest first: the same events the webhooks delivered, with the same event_id and sequence. Pass the last sequence you processed to get only what you missed. Up to 200 events per call.
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
after_sequence | integer | Optional | Return only events with a higher sequence (default 0: all events) |
Success Response 200 OK
{
"success": true,
"data": {
"order_id": "ALHLD2610171030",
"partner_order_id": "ORD-48213",
"events": [
{
"event_id": "f13657f7-c753-451d-bd1a-72e4bda8297c",
"event": "RIDER_ASSIGNED",
"sequence": 2,
"occurred_at": "2026-10-17T10:31:10+05:30",
"status_code": 2,
"previous_status_code": 13,
"data": {
"status_text": "Rider assigned",
"rider": { "id": 203, "name": "Sachin More", "phone": "9890011223" }
}
},
{
"event_id": "282a38c8-68de-4e47-b578-5f7f376abcf8",
"event": "HEADING_TO_PICKUP",
"sequence": 3,
"occurred_at": "2026-10-17T10:42:10+05:30",
"status_code": 24,
"previous_status_code": 2,
"data": { "status_text": "Rider on the way to pickup" }
}
]
}
}
What each event puts in data is listed under Webhooks.
Ready to Ship
AL Kangaroo supports both ways of manifesting:
| You book | Send | A Field Executive is sent |
|---|---|---|
| After packing | Create Order as usual (ready_to_ship left out or true) | At once |
| At order creation, before packing | Create Order with "ready_to_ship": false, then this call when the parcel is packed | When you call Ready to Ship |
| At order creation, with the packing time | Create Order with "ready_to_ship": false and pickup_window_start = when packing will be done. When it is packed, call Ready to Ship (or Edit Order with "ready_to_ship": true) | The earlier of: 20 minutes before pickup_window_start, or when you say it is packed |
Until then, the order is booked (OTPs, tracking links, AWB) but no Field Executive is sent, and the assignment time is not counted against us. From then, a Field Executive is looked for at once and the usual RIDER_ASSIGNED webhook follows.
With the packing time
The 20 minutes cover finding a Field Executive and the ride to the store, so the Field Executive arrives as the parcel is packed. Example: order placed at 12:41, packing time 13:41.
| Packing | You send | A Field Executive is looked for |
|---|---|---|
| Takes the hour | 12:41 Create Order ("ready_to_ship": false, "pickup_window_start": "…T13:41:00+05:30"); 13:41 Ready to Ship (optional, changes nothing then) | 13:21, on its own |
| Done in 10 minutes | 12:41 Create Order as above; 12:51 Ready to Ship or Edit Order "ready_to_ship": true | 12:51 |
| Will take longer | Edit Order with the new pickup_window_start, e.g. 14:10 | 13:50 |
| Packing time less than 20 minutes away when you book | Create Order as above | At once |
An order sent out by its packing time is logged as a READY_TO_SHIP event with data.released_by = packing_time. Without pickup_window_start a held order waits for you, as before. Times without a time zone are taken as IST.
Body: none.
{id} is the AL order id or your partner_order_id.
Success Response 200 OK
{
"success": true,
"data": {
"released": true,
"order_id": "ALHLD2610171030",
"partner_order_id": "ORD-48213",
"status": { "event": "ORDER_CREATED", "code": 13, "text": "Finding a rider" },
"ready_to_ship": true,
"ready_to_ship_at": "2026-10-17T12:45:09+05:30",
"...": "the other order fields as in Order Details"
}
}
releasedistruewhen this call sent the order out. Calling again, or for an order booked as packed, answers200withreleased: falseand changes nothing, so retries are safe.- Logged as a
READY_TO_SHIPevent (see Events); no webhook, since you sent it. - A held order can still be edited or cancelled as usual.
Error Response 409 Conflict
{
"success": false,
"error_code": "ORDER_CANCELLED",
"message": "The order is cancelled",
"data": { "status": { "event": "CANCELLED", "code": 17, "text": "Cancelled" } }
}
Cancel Order
What cancel does depends on where the parcel is.
| Order is | Result |
|---|---|
| Before pickup status 1, 12, 15, 23, 13, 10, 2, 24 | The order is cancelled (status 17). A Field Executive already on it is released and told. A CANCELLED webhook follows. Response: 200 with the order, cancellation filled. |
| Picked up: the Field Executive has the parcel status 3, 25, 20, or a failed attempt 5, 26, 27, 28, 29 | The parcel goes back to the store. The order moves to RTO_INITIATED (status 31) with rto.reason_code CLIENT_CANCELLED and a 6-digit rto.return_otp for the store. Response: 200 with the order, rto filled. This also covers a customer who cancels while the order is out for delivery (status 25 or 20), e.g. through your customer care. See Return to store. |
| Already cancelled or returning / returned | 200 with the order unchanged. Cancelling twice is safe. |
| Delivered (4, 9, 11) | 409 CANCELLATION_NOT_ALLOWED |
Request Parameters
| Field | Type | Required | Description |
|---|---|---|---|
reason_code | string | Required | One of the codes below |
reason | string, max 500 | Optional | Free text. Required when reason_code is OTHER. |
| reason_code | Meaning |
|---|---|
CUSTOMER_CANCELLED | Customer cancelled the order |
ORDER_MODIFIED | Order changed; a new order will be booked |
DUPLICATE_ORDER | Booked twice by mistake |
ADDRESS_ISSUE | Delivery address cannot be served |
STORE_NOT_READY | Pickup store cannot hand over the parcel |
PAYMENT_ISSUE | Payment problem |
OTHER | Anything else (send reason) |
Sample Request
{
"reason_code": "CUSTOMER_CANCELLED",
"reason": "Customer no longer needs the order"
}
Before Pickup 200 OK
{
"success": true,
"data": {
"order_id": "ALHLD2610171030",
"partner_order_id": "ORD-48213",
"status": { "event": "CANCELLED", "code": 17, "text": "Cancelled" },
"rider": null,
"cancellation": {
"cancelled_by": "client",
"reason_code": "CUSTOMER_CANCELLED",
"reason": "Customer no longer needs the order",
"cancelled_at": "2026-10-17T10:36:15+05:30"
},
"...": "the other order fields as in Order Details"
}
}
After Pickup 200 OK, return to store started
{
"success": true,
"data": {
"order_id": "ALHLD2610171030",
"partner_order_id": "ORD-48213",
"status": { "event": "RTO_INITIATED", "code": 31, "text": "Returning to the store" },
"cancellation": null,
"rto": {
"reason_code": "CLIENT_CANCELLED",
"reason": "Customer no longer needs the order",
"initiated_by": "client",
"initiated_at": "2026-10-17T11:05:02+05:30",
"return_otp": "605118",
"return_verified": false,
"photos": []
},
"...": "the other order fields as in Order Details"
}
}
Give rto.return_otp to the store. The store tells it to the Field Executive when it takes the parcel back; the return completes only with this OTP.
Error Response 409 Conflict
{
"success": false,
"error_code": "CANCELLATION_NOT_ALLOWED",
"message": "The order is finished and can no longer be cancelled",
"data": {
"status": { "event": "DELIVERED", "code": 4, "text": "Delivered" }
}
}
Webhooks
Every event of every order is sent to your server as an HTTP POST with a JSON body, as it happens.
Request Headers
| Header | Description |
|---|---|
Content-Type | application/json |
X-Webhook-Event | The event name, same as event in the body, e.g. PICKED_UP |
X-Webhook-Timestamp | When this attempt was sent (ISO 8601) |
X-Event-Id | Same as event_id in the body. Sent with the signature. |
X-Timestamp | Unix time in seconds when this attempt was signed. Sent with the signature. |
X-Signature | sha256= followed by the lowercase hex HMAC-SHA256 of {X-Timestamp}.{raw body}, keyed with your webhook secret. Sent when your account has a signing secret. |
Sample Payload
{
"event_id": "84696452-62d6-4cfc-935b-d887893224c7",
"event": "PICKED_UP",
"sequence": 4,
"occurred_at": "2026-10-17T10:58:40+05:30",
"status_code": 3,
"previous_status_code": 24,
"data": { "status_text": "Picked up" },
"order_id": "ALHLD2610171030",
"partner_order_id": "ORD-48213",
"order": {
"order_id": "ALHLD2610171030",
"partner_order_id": "ORD-48213",
"status": { "event": "PICKED_UP", "code": 3, "text": "Picked up" },
"otp": { "pickup_verified": true, "pickup_verified_at": "2026-10-17T10:58:40+05:30",
"delivery_verified": false, "delivery_verified_at": null },
"...": "the rest of the order as in Order Details, without OTP codes"
}
}
| Field | Description |
|---|---|
event_id | Unique id of the event (UUID). The same event can arrive more than once: ignore an event_id you have already processed. |
event | The event name, see Status & Event Codes |
sequence | 1, 2, 3 ... per order. Use it to put events in order and to spot a missed one. |
status_code / previous_status_code | Internal status after and before the event |
data | The facts of this event (table below) |
order | The order as it is when the webhook is built, same shape as Order Details but without the OTP codes (otp.pickup_otp, otp.delivery_otp, rto.return_otp). Photos, COD and timings are here. |
Event Data
| Event | data fields |
|---|---|
| Every event | status_text |
RIDER_ASSIGNED, RIDER_REASSIGNED | rider: id, name, phone of the Field Executive |
RIDER_AT_PICKUP | distance_m: how far from the store the Field Executive was when the 200 m mark was crossed |
OUT_FOR_DELIVERY | attempt: 1 on the first trip, 2 when the Field Executive goes out again after a failed attempt |
DELIVERY_FAILED, DELIVERY_RESCHEDULED | reason_code, reason, attempt (attempt number) |
RTO_INITIATED | reason_code, reason, return_otp. This is the only webhook that carries the return OTP: pass it to the store. |
CANCELLED | cancelled_by, reason. For the normalised values (client, customer, al_services, system) read order.cancellation.cancelled_by. |
Verifying the Signature
Compute the HMAC over the raw request body, exactly as received, before parsing it. Reject the request when the signature does not match or X-Timestamp is more than 5 minutes away from your clock.
const crypto = require('crypto');
app.post('/hld-webhook', express.raw({ type: 'application/json' }), (req, res) => {
const ts = req.get('X-Timestamp') || '';
const expected = 'sha256=' + crypto.createHmac('sha256', WEBHOOK_SECRET)
.update(ts + '.' + req.body.toString('utf8')).digest('hex');
const given = req.get('X-Signature') || '';
const ok = given.length === expected.length
&& crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(given));
if (!ok || Math.abs(Date.now() / 1000 - Number(ts)) > 300) return res.sendStatus(401);
const event = JSON.parse(req.body);
// already processed event.event_id? reply 200 and stop
res.sendStatus(200);
// process the event after replying
});
$body = file_get_contents('php://input');
$ts = $_SERVER['HTTP_X_TIMESTAMP'] ?? '';
$expected = 'sha256=' . hash_hmac('sha256', $ts . '.' . $body, $webhookSecret);
$given = $_SERVER['HTTP_X_SIGNATURE'] ?? '';
if (!hash_equals($expected, $given) || abs(time() - (int) $ts) > 300) {
http_response_code(401);
exit;
}
$event = json_decode($body, true);
import hashlib, hmac, time
def is_valid(raw_body: bytes, ts: str, signature: str, secret: str) -> bool:
expected = 'sha256=' + hmac.new(secret.encode(), ts.encode() + b'.' + raw_body,
hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature or '') and abs(time.time() - int(ts or 0)) <= 300
Responding, Retries and Order
- Duplicates: the same event can arrive more than once. Deduplicate on
event_id. - Order: after a retry, events can arrive out of order. Sort by
sequence; a gap means a missed event. - Catching up: if your endpoint was down, fetch what you missed from Events with
after_sequencerather than waiting for retries. - Current state: Order Details always returns the order as it is now.
Tracking Links
Every order has two tracking pages, returned in tracking by Create Order, Order Details and Cancel. They use different random tokens, so knowing the customer link never opens the client link.
| Customer link | Client link | |
|---|---|---|
| Field | tracking.customer_url | tracking.client_url |
| URL | https://delivery.alfast.in/track/hld/{token} | https://delivery.alfast.in/track/hld/client/{client_token} |
| For | Your customer (SMS, email, app) | Your own team only: store, support, operations |
| Shows | Live map with the Field Executive, progress and ETA; the Field Executive's name and phone while on the way; the delivery OTP only while the order is out for delivery and not yet delivered; COD amount to pay | Pickup, delivery and return OTPs; the Field Executive's name, phone and live location; proof photos for every step; the full timeline; failed attempts and the return to the store; COD amount and collection |
| Never shows | The pickup OTP, the return OTP, internal notes | - |
Status & Event Codes
An order moves through these events. Each event is logged, numbered and sent as a webhook. status.code is the internal status behind it, for reference. Event names and status texts are literal API values.
Events
| Event | When | Status codes |
|---|---|---|
ORDER_CREATED | Order accepted by AL Kangaroo; a Field Executive is being found | 1 |
RIDER_ASSIGNED | A Field Executive accepted the order (name, phone and photo now in order.rider) | 2 |
RIDER_REASSIGNED | AL Kangaroo moved the order to another Field Executive | 2 |
RIDER_UNASSIGNED | The Field Executive was taken off before pickup; a new one is being found | 1 |
HEADING_TO_PICKUP | The Field Executive is on the way to the store | 24 |
RIDER_AT_PICKUP | Sent automatically when the Field Executive's GPS position comes within 200 m of the store; data.distance_m. Sent once per order. The status does not change. | unchanged |
PICKED_UP | Pickup OTP verified, pickup photo taken | 3 |
OUT_FOR_DELIVERY | On the way to the customer. Sent again when a second delivery attempt starts: data.attempt is 1, then 2 for the re-attempt. | 25 |
NEAR_DROP | Sent automatically when the Field Executive's GPS position comes within 200 m of the customer address | 20 |
DELIVERED final | Delivery OTP verified, delivery photo taken; for COD, cash collected (9) | 4 9 |
DELIVERY_FAILED | A delivery attempt failed (see Failed attempts) | 5 26 27 28 |
DELIVERY_RESCHEDULED | The customer asked for another time; counts as a failed attempt | 29 |
RTO_INITIATED | The parcel is going back to the store; return OTP in data.return_otp | 31 |
RTO_COMPLETED final | Parcel handed back at the store against the return OTP, handover photo in order.rto.photos | 32 |
CANCELLED final | Cancelled before pickup | 7 17 30 |
ORDER_UPDATED | You changed the order with Edit Order; data.updated_fields lists the fields. In the Events API only, no webhook. | unchanged |
READY_TO_SHIP | You marked an order booked before packing as packed (Ready to Ship). In the Events API only, no webhook. | unchanged |
Offering the order to Field Executives who do not accept is internal and sends no event. No event follows a final one.
Status Codes
| Code | status.text | status.event |
|---|---|---|
| 1 12 15 23 | Order placed | ORDER_CREATED |
| 13 10 | Finding a rider | ORDER_CREATED |
| 2 | Rider assigned | RIDER_ASSIGNED |
| 24 | Rider on the way to pickup | HEADING_TO_PICKUP |
| 3 | Picked up | PICKED_UP |
| 25 | Out for delivery | OUT_FOR_DELIVERY |
| 20 | Rider near the drop | NEAR_DROP |
| 4 11 | Delivered | DELIVERED |
| 9 | Delivered, cash collected | DELIVERED |
| 5 | Delivery attempt failed | DELIVERY_FAILED |
| 26 | Customer not available | DELIVERY_FAILED |
| 27 | Wrong or incomplete address | DELIVERY_FAILED |
| 28 | Payment issue | DELIVERY_FAILED |
| 29 | Rescheduled by the customer | DELIVERY_RESCHEDULED |
| 31 | Returning to the store | RTO_INITIATED |
| 32 | Returned to the store | RTO_COMPLETED |
| 7 17 30 | Cancelled | CANCELLED |
Failed Attempts and the Second Attempt
- The Field Executive can report a failed attempt only when within 200 m of the customer address, with a reason and a photo. The Field Executive keeps the parcel.
DELIVERY_FAILED(orDELIVERY_RESCHEDULED) is sent withdata.reason_code,data.reasonanddata.attempt; the photo is inorder.failure.photos. - After the first failed attempt, AL Kangaroo operations can send the Field Executive back to the customer or start the return. If they have not acted within about a minute, the second attempt opens automatically:
OUT_FOR_DELIVERYis sent again (status 25). Meanwhile the Field Executive calls the customer. - If the second attempt also fails, the return to the store starts at once:
RTO_INITIATEDwithrto.reason_codeDELIVERY_FAILEDandrto.initiated_bysystem.
Failure reasons
| reason_code | Meaning | Status |
|---|---|---|
CUSTOMER_NOT_AVAILABLE | Customer not available at the address | 26 |
CUSTOMER_UNREACHABLE | Customer not answering the phone | 26 |
PREMISES_CLOSED | Premises closed | 26 |
WRONG_ADDRESS | Wrong or incomplete address | 27 |
CUSTOMER_REFUSED | Customer refused the parcel | 5 |
OTP_NOT_SHARED | Customer did not share the delivery OTP | 5 |
PAYMENT_NOT_READY | Customer not ready to pay the COD amount | 28 |
CUSTOMER_RESCHEDULED | Customer asked for another time (event DELIVERY_RESCHEDULED) | 29 |
OTHER | Other; reason carries the Field Executive's note | 5 |
Return to Store (RTO)
RTO_INITIATED(status 31): the Field Executive is taking the parcel back. A 6-digit return OTP is created for the store:data.return_otpin the webhook,rto.return_otpin Order Details. Give it to the store.- At the store, the store checks the parcel and tells the Field Executive the return OTP. The Field Executive enters it and takes a handover photo.
RTO_COMPLETED(status 32, final): the parcel is back at the store;rto.return_verifiedistrueandrto.photosholds the handover photo.
If the store is closed or cannot take the parcel, the order stays at 31 and AL Kangaroo operations arrange the handover.
| rto.reason_code | Meaning |
|---|---|
DELIVERY_FAILED | Delivery could not be completed (including the automatic return after the second failed attempt) |
CLIENT_CANCELLED | You cancelled the order after pickup |
CUSTOMER_REFUSED | Customer refused the parcel |
ADDRESS_NOT_SERVICEABLE | Address cannot be served |
OTHER | Other |
Error Codes Reference
Errors come as {"success": false, "error_code": "...", "message": "..."}. message is readable text and may change; build your logic on error_code.
| HTTP Code | Error Code | Description |
|---|---|---|
| 401 | UNAUTHENTICATED | No Authorization: Bearer token |
| 401 | INVALID_TOKEN | Unknown API token |
| 403 | CLIENT_DEACTIVATED | The account is deactivated |
| 403 | HLD_NOT_ENABLED | Hyperlocal delivery is not enabled for the account |
| 404 | ORDER_NOT_FOUND | No order with this id for your account |
| 409 | CANCELLATION_NOT_ALLOWED | The order is delivered and can no longer be cancelled |
| 409 | ORDER_NOT_EDITABLE | Edit Order: the order is already picked up or finished |
| 409 | ORDER_CANCELLED | Ready to Ship: the order is cancelled |
| 422 | VALIDATION_FAILED | Request validation failed; see errors |
| 422 | SERVICE_NOT_AVAILABLE | Pickup or drop not served, or the zone is closed now; details in data |
| 422 | NOTHING_TO_UPDATE | Edit Order: no field to change was sent |
| 422 | INVALID_PAYMENT | Edit Order: payment_mode and cod_value do not match |
| 422 | INVALID_PICKUP_WINDOW | Edit Order: the pickup window would end before it starts |
| 422 | RANGE_TOO_LONG | KPI report: more than 31 days asked for |
| 429 | - | Rate limit reached; retry after Retry-After seconds |
| 500 | ORDER_CREATION_FAILED | The order could not be booked; retry with the same partner_order_id |
| Sandbox simulator only | ||
| 404 | NOT_FOUND | The simulator was called outside the sandbox |
| 409 | INVALID_TRANSITION | The requested step is not possible from the order's current status |
KPI Report
Service figures for your orders placed in the date range. Without parameters: yesterday. At most 31 days per call.
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
from | date | Optional | First day, YYYY-MM-DD (default yesterday) |
to | date | Optional | Last day, included, on or after from (default: same as from) |
Success Response 200 OK
{
"success": true,
"data": {
"from": "2026-10-01",
"to": "2026-10-15",
"orders": 412,
"delivered": 389,
"cancelled": 11,
"returned_to_store": 10,
"returning_to_store": 0,
"open": 2,
"on_time_pct": 93.1,
"first_attempt_pct": 96.4,
"pickup_compliance_pct": 99.2,
"pod_compliance_pct": 98.7,
"cancellation_pct": 2.7,
"rto_pct": 2.4,
"avg_accept_minutes": 2.6,
"avg_pickup_minutes": 17.9,
"avg_delivery_minutes": 31.4,
"avg_total_minutes": 52.8,
"failed_attempts": 27,
"orders_with_failed_attempt": 24,
"failure_reasons": { "CUSTOMER_NOT_AVAILABLE": 12, "CUSTOMER_UNREACHABLE": 7, "WRONG_ADDRESS": 5 },
"top_failure_reasons": { "CUSTOMER_NOT_AVAILABLE": 12, "CUSTOMER_UNREACHABLE": 7, "WRONG_ADDRESS": 5 },
"returns_started": 10,
"returns_completed": 10,
"returns_automatic": 6,
"rto_rate_pct": 2.5,
"avg_return_minutes": 24.3,
"cancellation_reasons": { "CUSTOMER_CANCELLED": 8, "ORDER_MODIFIED": 3 },
"return_reasons": { "DELIVERY_FAILED": 6, "CLIENT_CANCELLED": 4 },
"sla_breaches": { "pickup": 4, "delivery": 9 }
}
}
| Field | Meaning |
|---|---|
orders, delivered, cancelled, returned_to_store, returning_to_store, open | Order counts |
on_time_pct | Delivered within the order's expected_delivery_minutes |
first_attempt_pct | Delivered without a failed attempt |
pickup_compliance_pct, pod_compliance_pct | Pickups / deliveries with the OTP verified and the required photos |
cancellation_pct, rto_pct | Share of orders cancelled / sent back to the store |
avg_accept_minutes, avg_pickup_minutes, avg_delivery_minutes, avg_total_minutes | Placed to accepted, accepted to picked up, picked up to delivered, placed to delivered |
failed_attempts, orders_with_failed_attempt, failure_reasons, top_failure_reasons | Failed attempts in total, orders with at least one, counts by reason code (top 3) |
returns_started, returns_completed, returns_automatic, rto_rate_pct, avg_return_minutes | Returns to the store: started, handed back, started automatically, share of picked-up parcels, average minutes from start to handover |
cancellation_reasons, return_reasons, sla_breaches | Counts by reason code / SLA stage |
Percentages and averages are null when there is nothing to measure (for example no deliveries in the range).
Sandbox
On the sandbox server, move a test order through its stages. Each step sends the same webhook as in production. The response is the order after the last step.
Not available in production (answers
404 NOT_FOUND).
Request Parameters
| Field | Type | Required | Description |
|---|---|---|---|
to | string | Optional | RIDER_ASSIGNED, HEADING_TO_PICKUP, PICKED_UP, OUT_FOR_DELIVERY, NEAR_DROP, DELIVERED, DELIVERY_FAILED, RTO_INITIATED or RTO_COMPLETED. Without it the order moves one step. With it the order walks every step up to that event, one webhook per step. |
DELIVERY_FAILED (simulated as CUSTOMER_NOT_AVAILABLE) and RTO_INITIATED are possible after pickup and before delivery; RTO_COMPLETED after RTO_INITIATED. A step that is not possible answers 409 INVALID_TRANSITION.
Suggested Test Plan
- Book an order, simulate to
DELIVEREDand check every webhook. - Book one and cancel it before pickup.
- Book one, simulate to
PICKED_UP, cancel it, then simulateRTO_COMPLETED. - Book one, simulate to
OUT_FOR_DELIVERY, thenDELIVERY_FAILED. - Book the same
partner_order_idtwice and check the second answer is"duplicate": true. - Send one request with a wrong token and check the
401.
Overview
HLD (Hyperlocal Delivery) is a hyperlocal, on-demand delivery service by AL Kangaroo. Deliveries are completed within 120 minutes. This document covers all client-facing APIs for order creation, real-time tracking, order management, and cancellation.
Base URL
Available Endpoints
POST Check Serviceability
Verify HLD service availability with pricing & ETA
POST Create Order
Place a new HLD order with pickup & delivery details
GET Track Order
Real-time order tracking with executive location (Public)
POST Cancel Order
Cancel an HLD order before pickup
Authentication
All client APIs (except public tracking) require a Bearer Token in the request header. Contact the AL Kangaroo team to get your API credentials.
Authorization: Bearer {your_api_token} Content-Type: application/json Accept: application/json
Serviceability
Check if HLD service is available between pickup and delivery locations. Returns pricing, ETA, and nearby executive availability. Recommended to call before creating an order.
Rule: pickup and delivery must each be inside an AL Kangaroo HLD zone that is open at that time. Maximum distance: 50 km.
Same rule as Create Order: if this API says serviceable, Create Order accepts the order.
Request Parameters
| Field | Type | Required | Description |
|---|---|---|---|
pickup_lat | numeric | Required* | Pickup latitude (required if no pickup_pincode) |
pickup_lng | numeric | Required* | Pickup longitude (required if no pickup_pincode) |
pickup_pincode | string | Required* | 6-digit pincode (required if no lat/lng) |
delivery_lat | numeric | Required* | Delivery latitude (required if no delivery_pincode) |
delivery_lng | numeric | Required* | Delivery longitude (required if no delivery_pincode) |
delivery_pincode | string | Required* | 6-digit pincode (required if no lat/lng) |
product_value | numeric | Optional | Product value for pricing calculation |
weight | numeric | Optional | Package weight in kg |
priority_level | integer | Optional | 1-3 priority level |
* Either lat/lng OR pincode is required for each location
Sample Request
{
"pickup_lat": 18.9488,
"pickup_lng": 72.8310,
"delivery_lat": 19.0596,
"delivery_lng": 72.8295,
"product_value": 45000,
"weight": 0.5
}
Success Response 200 OK
{
"success": true,
"serviceable": true,
"no_rider_online": false,
"message": "HLD service available",
"distance_km": 12.4,
"duration_minutes": 35,
"executives": {
"summary": {
"total_found": 4,
"available_executives": 3,
"busy_executives": 1,
"average_pickup_eta_minutes": 15,
"fastest_pickup_eta_minutes": 10
},
"pickup_estimates": {
"fastest_eta_minutes": 6,
"fastest_eta_range": { "min": 5, "max": 10 },
"average_eta_minutes": 15,
"estimated_pickup_window": "Standard pickup (10-20 minutes)"
}
},
"pricing": {
"delivery_charge": 120.00,
"insurance_charge": 0,
"total_charge": 120.00,
"currency": "INR"
}
}
Response Fields
| Field | Description |
|---|---|
serviceable | true when both locations are covered. Create the order only when this is true. |
no_rider_online | true when no Field Executive is online near the pickup right now. The order can still be placed; a Field Executive is assigned as soon as one comes online. |
message | Human-readable result, safe to show to your team |
distance_km / duration_minutes | Road distance and travel time from pickup to delivery |
executives.summary | How many Field Executives are near the pickup and how soon the fastest one can reach it. Times are in steps of 5 minutes. |
executives.pickup_estimates | Expected time for a Field Executive to reach the pickup, in steps of 5 minutes |
pricing | Estimated delivery charge in INR. Final billing follows your agreed tariff. |
Not Serviceable 200 OK, serviceable = false
The HTTP status is 200; check serviceable. reason tells which check failed.
{
"success": false,
"serviceable": false,
"no_rider_online": false,
"message": "HLD service not available at delivery location",
"reason": "No service zone found for this location",
"pickup_zone": {
"zone_code": "PUN-HLD-001",
"zone_name": "Pune Central",
"city": "Pune"
},
"alternatives": [],
"details": {
"pickup": { "serviceable": true, "no_rider_online": false, "zone": { "zone_code": "PUN-HLD-001", "zone_name": "Pune Central", "city": "Pune" } },
"delivery": { "serviceable": false, "reason": "No service zone found for this location", "no_rider_online": false }
}
}
reason | Meaning |
|---|---|
| No service zone found for this location | The location is outside every HLD zone |
| Service not available in this zone at this time | The zone is closed now (outside service hours) |
| Distance exceeds maximum limit of 50km | Pickup and delivery are too far apart |
Error Responses
// 422 - Validation Error { "success": false, "message": "Invalid request parameters", "errors": { "delivery_lat": ["The delivery lat field is required when delivery pincode is not present."] } } // 400 - Location could not be resolved from the pincode { "success": false, "message": "Unable to determine location coordinates", "error_code": "INVALID_LOCATION" }
Create Order
Create a new HLD (Hyperlocal Delivery) order. Delivery guaranteed within 120 minutes. The system will automatically find and assign the nearest available executive.
Duplicate Check: each
customer_order_no books one order. Sending the same number again books nothing and answers 400 DUPLICATE_ORDER with the order already booked under it.
Request Parameters
| Field | Type | Required | Description |
|---|---|---|---|
| Order Information | |||
customer_order_no | string | Required | Your unique order reference number (max 100 chars) |
product_name | string | Required | Name/description of the product (max 255 chars) |
product_value | numeric | Required | Declared product value in INR (min: 1) |
cod_value | numeric | Optional | Cash to collect from the customer. Required and above 0 for COD and Partial; 0 or left out for Prepaid. |
payment_mode | string | Optional | Payment mode: COD, Prepaid, or Partial. Left out, it follows cod_value: above 0 is COD, otherwise Prepaid. A mode that contradicts cod_value is refused (422). |
| Contact Details | |||
contact_person | string | Required | Recipient name (max 100 chars) |
contact_number | string | Required | Recipient phone number (max 15 chars) |
special_instructions | string | Optional | Any special handling instructions (max 500 chars) |
| Pickup Location | |||
pickup_address | string | Required | Full pickup address (max 500 chars) |
pickup_pincode | string | Optional | Pickup area pincode, 6 digits |
pickup_lat | numeric | Required | Pickup latitude (-90 to 90) |
pickup_lng | numeric | Required | Pickup longitude (-180 to 180) |
| Delivery Location | |||
delivery_address | string | Required | Full delivery address (max 500 chars) |
delivery_pincode | string | Optional | Delivery area pincode, 6 digits |
delivery_lat | numeric | Required | Delivery latitude (-90 to 90) |
delivery_lng | numeric | Required | Delivery longitude (-180 to 180) |
| Delivery Preferences | |||
expected_delivery_minutes | integer | Optional | Expected delivery time in minutes (15 - 120) |
priority_level | integer | Optional | 1 = Normal, 2 = High, 3 = Urgent |
requires_signature | boolean | Optional | Whether delivery requires customer signature |
delivery_preferences | json | Optional | Additional delivery preferences as JSON |
| Package Dimensions | |||
length | numeric | Optional | Package length in cm |
width | numeric | Optional | Package width in cm |
height | numeric | Optional | Package height in cm |
weight | numeric | Optional | Package weight in kg |
pickup_otp | string | Optional | Your own pickup OTP, exactly 6 digits: the store gives it to the executive at pickup. Left out: AL makes one. |
delivery_otp | string | Optional | Your own delivery OTP, exactly 6 digits: the customer gives it to the executive at delivery. Left out: AL makes one. |
return_otp | string | Optional | Your own return OTP, exactly 6 digits: the store gives it when the parcel comes back. Left out: AL makes one when a return starts. Any OTP that is not exactly 6 digits answers 422 with the field in errors; OTPs cannot be changed after the order is created. |
Sample Request
{
"customer_order_no": "ORD-2026-00456",
"product_name": "Gold Necklace Set",
"product_value": 45000.00,
"cod_value": 0,
"payment_mode": "Prepaid",
"contact_person": "Priya Sharma",
"contact_number": "9876543210",
"special_instructions": "Handle with care - fragile jewelry box",
"pickup_address": "Zaveri Bazaar, 42 Kalbadevi Road, Mumbai 400002",
"pickup_pincode": "400002",
"pickup_lat": 18.9488,
"pickup_lng": 72.8310,
"delivery_address": "Bandra West, Hill Road, Mumbai 400050",
"delivery_pincode": "400050",
"delivery_lat": 19.0596,
"delivery_lng": 72.8295,
"expected_delivery_minutes": 45,
"priority_level": 2,
"requires_signature": true,
"weight": 0.5,
"length": 20,
"width": 15,
"height": 8
}
Success Response 201 Created
{
"success": true,
"message": "HLD order created successfully",
"data": {
"order": {
"id": "ALHLD2604041030",
"customer_order_no": "ORD-2026-00456",
"service_type": "HLD",
"status": 1,
"status_text": "Order Placed",
"is_hld_order": true,
"pickup_address": "Zaveri Bazaar, 42 Kalbadevi Road, Mumbai 400002",
"pickup_pincode": "400002",
"pickup_lat": 18.9488,
"pickup_lng": 72.8310,
"delivery_address": "Bandra West, Hill Road, Mumbai 400050",
"delivery_pincode": "400050",
"delivery_lat": 19.0596,
"delivery_lng": 72.8295,
"product_name": "Gold Necklace Set",
"product_value": 45000.00,
"cod_value": 0.00,
"contact_person": "Priya Sharma",
"contact_number": "9876543210",
"expected_delivery_minutes": 45,
"priority_level": 2,
"priority_text": "High",
"tracking_token": "aB3dE5fG7hJ9kL2mN4pQ6rS8tU0vW1xY",
"pickup_otp": "452119",
"delivery_otp": "789301",
"created_at": "2026-04-04T10:30:00Z",
"hld_assignment_deadline": "2026-04-04T10:35:00Z"
},
"tracking": {
"token": "aB3dE5fG7hJ9kL2mN4pQ6rS8tU0vW1xY",
"url": "https://delivery.alfast.in/track/hld/aB3dE5fG7hJ9kL2mN4pQ6rS8tU0vW1xY",
"short_url": "https://delivery.alfast.in/track/hld/aB3dE5fG7hJ9kL2mN4pQ6rS8tU0vW1xY",
"live_tracking_enabled": true
},
"assignment": {
"status": "finding_executive",
"executive_id": null,
"assigned_at": null,
"deadline": "2026-04-04T10:35:00Z",
"note": "Searching for available executive"
}
}
}
Error Responses
422 - Validation Error
{
"success": false,
"message": "Validation failed",
"errors": {
"product_value": ["The product value field is required."],
"pickup_lat": ["Invalid latitude coordinate."]
}
}
400 - Service Not Available
{
"success": false,
"message": "HLD service not available",
"error_code": "SERVICE_NOT_AVAILABLE"
}
400 - Duplicate Order
{
"success": false,
"success": false,
"message": "An order with this order number already exists",
"error_code": "DUPLICATE_ORDER",
"data": {
"order_id": "ALHLD2604041030",
"status": 13,
"tracking_token": "aB3dE5fG7hJ9kL2mN4pQ6rS8tU0vW1xY"
}
}
Realtime Tracking
Get the order's live status and the executive's location as JSON, for your systems. This is a public endpoint - no authentication required. Use the tracking_token from the Create Order response.
Example: https://delivery.alfast.in/api/v1/track/aB3dE5fG7hJ9kL2mN4pQ6rS8tU0vW1xY
Share this link with your customer (it is
tracking.url in the Create Order response). It opens a live map page with the Field Executive's position, ETA and order progress. Do not share the API URL with customers.
URL Parameters
| Parameter | Type | Description |
|---|---|---|
trackingToken | string | Unique tracking token received in create order response |
executive (name, mobile, current location) is sent only while the Field Executive has accepted the order and is on the way: statuses 2, 24, 3, 25 and 20. It is left out while the order is only being offered and after the order ends.
Success Response 200 OK
{
"success": true,
"data": {
"order_id": "ALHLD2604041030",
"status": 25,
"status_text": "On the Way",
"pickup_address": "Zaveri Bazaar, 42 Kalbadevi Road, Mumbai",
"delivery_address": "Bandra West, Hill Road, Mumbai",
"expected_delivery_minutes": 45,
"delivery_eta": "2026-04-04T11:15:00Z",
"delivery_time_remaining": "12 minutes",
"created_at": "2026-04-04T10:30:00Z",
"assigned_at": "2026-04-04T10:33:00Z",
"pickup_completed_at": "2026-04-04T10:48:00Z",
"completed_at": null,
"executive": {
"name": "Rajesh Kumar",
"mobile": "9876543212",
"current_location": {
"lat": 19.0176,
"lng": 72.8301
}
},
"real_time_updates": [
{
"status": 1,
"timestamp": "2026-04-04T10:30:00Z",
"location": { "lat": 18.9488, "lng": 72.8310 }
},
{
"status": 2,
"timestamp": "2026-04-04T10:33:00Z",
"location": { "lat": 18.9488, "lng": 72.8310 }
},
{
"status": 3,
"timestamp": "2026-04-04T10:48:00Z",
"location": { "lat": 18.9492, "lng": 72.8315 }
}
]
}
}
Error Response 404 Not Found
{
"success": false,
"message": "Invalid tracking token",
"error_code": "NOT_FOUND"
}
For live tracking, call this endpoint again every 15 seconds while the order is in progress. The executive's location is refreshed about every 30 seconds, so polling more often returns the same data.
Cancel Order
Cancel an HLD order. An order can be cancelled until the Field Executive picks up the parcel.
Request Parameters
| Field | Type | Required | Description |
|---|---|---|---|
order_id | string | Required | HLD order ID to cancel |
reason | string | Required | Cancellation reason (max 500 chars) |
Sample Request
{
"order_id": "ALHLD2604041030",
"reason": "Customer changed delivery address"
}
Success Response 200 OK
{
"success": true,
"message": "Order cancelled successfully",
"data": {
"order_id": "ALHLD2604041030",
"status": 17,
"status_text": "Cancelled",
"cancelled_at": "2026-04-04T10:32:00Z",
"reason": "Customer changed delivery address"
}
}
Error Response 400 - Cancellation Not Allowed
{
"success": false,
"message": "The order is finished and can no longer be cancelled",
"error_code": "CANCELLATION_NOT_ALLOWED",
"data": {
"current_status": 4,
"status_text": "Delivered",
"hint": "Delivered, returned and cancelled orders cannot be cancelled"
}
}
Webhooks
Instead of polling, you can receive every order update on your own server. Each time an HLD order changes, AL Kangaroo sends an HTTP POST with a JSON body to your webhook URL.
Request Headers
| Header | Description |
|---|---|
Content-Type | application/json |
X-Webhook-Event | Event name, same as event in the body |
X-Webhook-Timestamp | Time the webhook was sent (ISO 8601) |
X-Timestamp | Signed webhooks only: Unix time in seconds, the time that is signed |
X-Signature | Signed webhooks only: sha256= followed by the HMAC-SHA256 (hex) of X-Timestamp, a dot, and the raw request body, with your webhook secret as the key |
X-Event-Id | Signed webhooks only: same as event_id in the body |
X-Signature. Compute the same HMAC over X-Timestamp + "." + raw body and compare; reject the webhook when it differs or when X-Timestamp is more than 5 minutes old.
Events
| Event | When it is sent |
|---|---|
order.created | The order was created |
order.executive.accepted | An executive accepted the order |
order.reassigned | The order moved to another executive |
order.status.updated | Any other status change: pickup, out for delivery, delivered, failed attempt, return to store |
order.cancelled | The order was cancelled |
Sample Payload
{
"event": "order.status.updated",
"timestamp": "2026-04-04T10:48:00+05:30",
"event_id": "9f1c2d3e-4b5a-4c6d-8e7f-0a1b2c3d4e5f",
"event_code": "PICKED_UP",
"sequence": 4,
"order": {
"id": "ALHLD2604041030",
"client_order_id": "ORD-2026-00456",
"type": "hld"
},
"status": {
"current": "PickedUp",
"previous": "ExecutiveComing",
"code": 3,
"description": "Picked Up"
},
"executive": {
"id": 156,
"name": "Rajesh Kumar"
}
}
| Field | Description |
|---|---|
order.client_order_id | Your customer_order_no from Create Order |
status.code | New status, see Status Codes |
event_id | Unique id of this update. The same update can arrive more than once (retries): ignore an event_id you have already processed. |
sequence | Increases by 1 for each update of the same order. Use it to put updates in order and to notice a missed one. |
executive | Present once an executive is assigned |
reason_code / reason | Sent with DELIVERY_FAILED, DELIVERY_RESCHEDULED and RTO_INITIATED: why the delivery failed or why the parcel is going back to the store (for example CUSTOMER_NOT_AVAILABLE, DELIVERY_FAILED) |
attempt | Sent with DELIVERY_FAILED and DELIVERY_RESCHEDULED: the delivery attempt number |
return_otp | Sent only with RTO_INITIATED. Share it with your pickup store: the store gives this OTP to the executive when it takes the parcel back. The return completes (status 32) only with this OTP. |
Responding and Retries
To check the latest state at any time, call Realtime Tracking.
Order Status Codes
Main Order Flow
| Code | Status | Description |
|---|---|---|
| 1 | OrderPlaced | Order placed by client |
| 13 | FindingExecutive | Finding nearby executive, pending acceptance |
| 2 | ExecutiveAssigned | Executive accepted the order |
| 24 | ExecutiveComing | Executive on way to pickup |
| 3 | PickedUp | Order picked up |
| 25 | OnTheWay | Executive on the way to the customer |
| 20 | Nearby | Executive near the delivery location |
| 4 | Delivered | Delivered successfully (Prepaid) |
| 9 | PaymentReceived | Delivered + COD payment collected |
| 11 | Completed | Order fully completed (Final) |
Cancellation Statuses
| Code | Status | Description |
|---|---|---|
| 17 | CustomerCancelled | Cancelled, including by your Cancel Order request |
| 30 | MerchantCancelled | Cancelled by the client (merchant) |
| 7 | SystemCancelled | Cancelled by system (timeout) |
Exception Statuses
| Code | Status | Description |
|---|---|---|
| 10 | ExecutiveRejected | Executive rejected, reassigning |
Failed Delivery Attempt
The executive could not deliver and reported a reason with a photo. AL Kangaroo operations then decide: retry the delivery (status goes back to 25) or return the parcel to the store (status 31).
| Code | Status | Description |
|---|---|---|
| 26 | CustomerNotAvailable | Customer not available, not answering, or premises closed |
| 27 | WrongAddress | Wrong or incomplete address |
| 28 | PaymentIssue | Customer not ready to pay the COD amount |
| 29 | Rescheduled | Customer asked for another time |
| 5 | DeliveryAttempted | Customer refused the parcel, did not share the OTP, or another reason |
Return to Store
| Code | Status | Description |
|---|---|---|
| 31 | ReturningToStore | Parcel is being returned to the pickup store |
| 32 | ReturnedToStore | Parcel handed back at the store with the return OTP (final) |
Error Codes Reference
| HTTP Code | Error Code | Description |
|---|---|---|
| 401 | - | Invalid or missing Bearer token. Body: {"error": {"message": "Unauthorized"}} |
| 403 | - | Your account is deactivated. Body: {"error": {"message": "Your account has been deactivated. Please contact AL Kangaroo support."}} |
| 403 | ORDER_ACCESS_DENIED | No order with this id for your account (unknown, or another client's) |
| 404 | NOT_FOUND | Tracking token not found |
| 422 | VALIDATION_FAILED | Request validation failed - check errors object |
| 400 | SERVICE_NOT_AVAILABLE | HLD service not available at given location |
| 400 | DUPLICATE_ORDER | This customer_order_no is already booked; the answer names that order |
| 400 | CANCELLATION_NOT_ALLOWED | The order is delivered, returned or already cancelled |
| 400 | NOT_HLD_ORDER | Order is not an HLD type order |
| 500 | SERVER_ERROR | Internal server error - contact support |