HLD API Documentation

Hyperlocal Delivery (HLD) client integration guide: book jewellery deliveries with AL Services from your system, track them live and receive a webhook at every stage.

Overview

Recommended for new integrations

HLD (Hyperlocal Delivery) is AL Services' 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)

https://staging.alfast.in/api/v2/hld

Production (shared when your integration is signed off)

https://delivery.alfast.in/api/v2/hld
Sandbox: 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 Services 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 Services to your server at every stage

Typical Integration

  1. Check serviceability for the store and the customer address.
  2. Create the order with your own order number. You receive the AL order id, the pickup and delivery OTPs and two tracking links.
  3. 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.
  4. Read the order details or the live location whenever you need them, and replay events after an outage on your side.

Conventions

TopicRule
FormatRequests and responses are JSON. Send Content-Type: application/json.
TimesISO 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 pathEither the AL order id (ALHLD...) or your own partner_order_id.
Rate limit600 requests per minute per API token. Above it the API answers 429; wait for the number of seconds in the Retry-After header.
IdempotencyOne AL order per partner_order_id, for ever. Retrying a create after a timeout is safe: you get the same order back.
NamingSome field and event names say rider (for example rider_location, RIDER_ASSIGNED). They refer to the Field Executive.

Moving from v1

Topicv1v2
API tokenThe same token works for both versions. Nothing new to request.
Your order numbercustomer_order_nopartner_order_id (customer_order_no is still accepted)
Repeated order numberRejected for 5 minutesReturns the existing order (200, "duplicate": true)
Cancel after pickupNot allowedStarts the return to the store, with a return OTP
Tracking linksCustomer link onlyCustomer link and a private link for your team
WebhooksGeneric order.* eventsOne 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 Services team to get a token or to replace one.

Authorization: Bearer {your_api_token}
Content-Type:  application/json
Accept:        application/json
Example
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.

HTTPerror_codeWhen
401UNAUTHENTICATEDNo Authorization: Bearer header
401INVALID_TOKENThe token is not known
403CLIENT_DEACTIVATEDYour account is deactivated
403HLD_NOT_ENABLEDHyperlocal delivery is not enabled for your account
// 401
{
  "success": false,
  "message": "Unknown API token",
  "error_code": "INVALID_TOKEN"
}

Serviceability

POST /serviceability Auth Required

Whether AL Services 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.

Full URL: https://delivery.alfast.in/api/v2/hld/serviceability
Same rule as Create Order: if this says serviceable: true, Create Order accepts the order.

Request Parameters

FieldTypeRequiredDescription
pickup_latnumberRequiredStore latitude in decimal degrees, e.g. 18.5362
pickup_lngnumberRequiredStore longitude in decimal degrees
drop_latnumberOptionalCustomer latitude in decimal degrees. Send it together with drop_lng, or leave both out to check the pickup only.
drop_lngnumberOptionalCustomer 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.

FieldDescription
riders_availabletrue 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_minutesMinutes until a Field Executive could be at the store: time to accept the order plus the drive. null when riders_available is false.
eta_atThe 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.

FieldDescription
distance_kmRoad distance from the store to the customer (estimated)
drive_minutesDrive time from the store to the customer
eta_minutesMinutes 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_atThe 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.reasonMeaning
No service zone found for this locationThe point is outside every HLD zone; alternatives lists the nearest zones
Service not available in this zone at this timeThe 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.

POST /api/v2/hld/serviceability
{
  "pickup_lat": 18.5362,
  "pickup_lng": 73.8939
}
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": 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

GET /zones Auth Required

The areas AL Services 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.

Full URL: https://delivery.alfast.in/api/v2/hld/zones
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" }
        }
      }
    ]
  }
}
FieldDescription
geometry.coordinatesOne closed ring of [lng, lat] points (GeoJSON order: longitude first). A point inside the ring is served.
properties.shapepolygon: 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_hoursDaily hours (HH:MM, India time) when orders are taken in the zone

Create Order

POST /orders Auth Required

Books the order. AL Services finds the nearest free Field Executive automatically. The response is the full order plus duplicate.

Full URL: https://delivery.alfast.in/api/v2/hld/orders
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

FieldTypeRequiredDescription
Order
partner_order_idstring, max 100RequiredYour order number. One AL order per number, for ever (also accepted as customer_order_no).
product_namestring, max 255RequiredShort description of the parcel contents
product_valuenumberRequiredDeclared value in INR, minimum 1
payment_modestringOptionalPrepaid, COD or Partial. When omitted: COD if cod_value > 0, otherwise Prepaid.
cod_valuenumberOptionalCash the Field Executive collects from the customer, in INR (minimum 0, default 0)
special_instructionsstring, max 500OptionalHandling notes, shown to the Field Executive
Customer (drop)
contact_personstring, max 100RequiredCustomer name
contact_numberstring, max 15RequiredCustomer mobile number
delivery_addressstring, max 500RequiredFull delivery address
delivery_pincodestring, max 6OptionalDelivery pincode
delivery_latnumberRequiredDelivery latitude in decimal degrees
delivery_lngnumberRequiredDelivery longitude in decimal degrees
Pickup (store)
pickup_addressstring, max 500RequiredFull pickup address
pickup_pincodestring, max 6OptionalPickup pincode
pickup_latnumberRequiredPickup latitude in decimal degrees
pickup_lngnumberRequiredPickup longitude in decimal degrees
pickup_store_codestring, max 50OptionalYour store id
pickup_store_namestring, max 150OptionalYour store name
pickup_contact_namestring, max 100OptionalPerson at the store who hands over the parcel
pickup_contact_numberstring, max 15OptionalThat person's phone number
pickup_window_startdate-timeOptionalWhen 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_enddate-timeOptionalEnd of the pickup window; must be after pickup_window_start
Delivery Preferences
expected_delivery_minutesintegerOptionalPromised delivery time in minutes, 15 to 120 (default 120). Used for the on-time figure in the KPI report.
priority_levelintegerOptional1 Normal (default), 2 High, 3 Urgent
requires_signaturebooleanOptionalCustomer signature needed at delivery (default false)
delivery_preferencesJSON stringOptionalExtra preferences as a JSON-encoded string
Package
package_countintegerOptionalNumber of packages, 1 to 50 (default 1)
shipment_categorystringOptionalOne of jewellery, repair_return, store_transfer, other
awb_numberstring, max 100OptionalThe 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_idstring, max 100OptionalThe bag the parcel travels in, if you use bags. Shown at pickup and returned in package.bag_id.
ready_to_shipbooleanOptionalDefault 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.
weightnumberOptionalWeight in kg
length / width / heightnumberOptionalDimensions in cm
Your own OTPs
pickup_otpstring, exactly 6 digitsOptionalThe OTP the store gives the Field Executive at pickup. Left out: AL makes one.
delivery_otpstring, exactly 6 digitsOptionalThe OTP the customer gives the Field Executive at delivery. Left out: AL makes one.
return_otpstring, exactly 6 digitsOptionalThe 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

POST /api/v2/hld/orders
{
  "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_otp and the customer the otp.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_otp and return_otp; the response then carries those same codes. Each must be exactly 6 digits, otherwise the answer is 422 VALIDATION_FAILED with the field in errors. The OTPs cannot be changed after the order is booked.
  • status.code is often already 13 (a Field Executive is being asked) when the response comes back; the event stays ORDER_CREATED until one accepts.
  • tracking.customer_url is for your customer; tracking.client_url is for your own team only. See Tracking Links. tracking_url is the same as tracking.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

GET /orders/{id} Auth Required

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.

Full URL: https://delivery.alfast.in/api/v2/hld/orders/ALHLD2610171030
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

FieldDescription
statusevent = the stage the order is in (see Status & Event Codes), code = internal status number, text = short description
trackingBoth tracking links, see Tracking Links
pickup / dropStore and customer as booked
package / paymentParcel 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_atfalse 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.
otpPickup and delivery OTP codes (API only, never in webhooks) and whether and when each was verified
riderThe 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_locationThe 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.
etaExpected pickup and delivery times, once known
proofPickup photos after PICKED_UP; delivery photos after DELIVERED
failureAfter a failed delivery attempt, and kept afterwards: reason_code (see failure reasons), reason (text with the Field Executive's note), attempts, failed_at, photos
rtoOnce 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.
cancellationAfter cancellation: cancelled_by (client, customer, al_services, system), reason_code, reason, cancelled_at
timelineEvery 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

PATCH /orders/{id} Auth Required

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.

Full URL: https://delivery.alfast.in/api/v2/hld/orders/{id}
Sandbox: https://staging.alfast.in/api/v2/hld/orders/{id}
Order isResult
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 cancelled409 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).

FieldTypeNotes
Customer
contact_personstring, max 100Customer name
contact_numberstring, max 15Customer phone
special_instructionsstring, max 500Clearable
Drop point
delivery_addressstring, max 500Address text
delivery_pincodestring, 6 digitsClearable
delivery_lat, delivery_lngnumberSend 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_valuenumber, ≥ 0Amount to collect. Sent alone, payment_mode follows it: above 0 → COD, 0 → Prepaid.
payment_modeCOD, Prepaid, PartialPrepaid sent alone sets cod_value to 0. COD / Partial need a cod_value above 0, Prepaid needs 0 (else 422 INVALID_PAYMENT).
Package
product_namestring, max 255What is being sent
product_valuenumber, ≥ 1Declared value
length, width, height, weightnumber, ≥ 0Clearable
package_countinteger, 1 to 50Number of packages
awb_number, bag_idstring, max 100Clearable. Your label and bag references.
Pickup contact and time window
pickup_contact_name, pickup_contact_numberstringClearable
pickup_window_start, pickup_window_enddatetimeClearable. 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_shiptrueFor 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

PATCH /api/v2/hld/orders/ORD-48213
{
  "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_fields lists only what really changed. Sending the values the order already has returns 200 with updated_fields: [], so a retry is safe.
  • An edit is logged as an ORDER_UPDATED event (see Events) and shows on your team's tracking page. No webhook is sent for it, since the change came from you.

Error Responses

409 Conflict: already picked up
{
  "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" }
  }
}
422 Unprocessable: a field that cannot change
{
  "success": false,
  "error_code": "VALIDATION_FAILED",
  "message": "Validation failed",
  "errors": { "pickup_lat": ["pickup_lat cannot be changed"] }
}

Live Location

GET /orders/{id}/location Auth Required

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.

Full URL: https://delivery.alfast.in/api/v2/hld/orders/{id}/location

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
  }
}
FieldDescription
rider_locationThe 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_secondsHow long to wait before the next call
When to stop polling
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 Services 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)

WSS hld.order.{tracking.token} Token in the channel name

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

ProductionSandbox
Hostdelivery.alfast.instaging.alfast.in
Port / TLS443, TLS (wss://)443, TLS (wss://)
App keycfa29167b95250425708vi9qe4bew6eh7whspyv3
URLwss://delivery.alfast.in/app/cfa29167b95250425708?protocol=7wss://staging.alfast.in/app/vi9qe4bew6eh7whspyv3?protocol=7
Channelhld.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)

JavaScript
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

EventWhenData
executive.location_updatedEach new position from the Field Executive's phone, about every 5 seconds while the order is in progressorder_id, location.lat, location.lng, location.accuracy (m), location.speed, location.bearing, location.last_updated, timestamp
hld.order.status_changedEvery status changeorder.id, order.status, order.previous_status (status codes, see Status & Event Codes), order.status_text, timestamp
hld.order.arrivedThe Field Executive came within 200 m of the store (leg: "pickup") or the customer (leg: "drop")leg, at, distance_m, title, message, timestamp
executive.location_updated
{
  "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"
}
hld.order.status_changed
{
  "order": {
    "id": "ALHLD2610171030",
    "tracking_token": "9v72Px9CSnS3tvnhhpXwuYUE9dXsTdq8",
    "status": 25,
    "previous_status": 3,
    "status_text": "Out for Delivery"
  },
  "timestamp": "2026-10-17T05:30:02.711414Z"
}
Good to know
• 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

GET /orders/{id}/events?after_sequence={n} Auth Required

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

ParameterTypeRequiredDescription
after_sequenceintegerOptionalReturn only events with a higher sequence (default 0: all events)

Success Response 200 OK

GET /api/v2/hld/orders/ORD-48213/events?after_sequence=1
{
  "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

POST /orders/{id}/ready-to-ship Auth Required

AL Services supports both ways of manifesting:

You bookSendA Field Executive is sent
After packingCreate Order as usual (ready_to_ship left out or true)At once
At order creation, before packingCreate Order with "ready_to_ship": false, then this call when the parcel is packedWhen you call Ready to Ship
At order creation, with the packing timeCreate 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.

PackingYou sendA Field Executive is looked for
Takes the hour12: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 minutes12:41 Create Order as above; 12:51 Ready to Ship or Edit Order "ready_to_ship": true12:51
Will take longerEdit Order with the new pickup_window_start, e.g. 14:1013:50
Packing time less than 20 minutes away when you bookCreate Order as aboveAt 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.

Full URL: https://delivery.alfast.in/api/v2/hld/orders/{id}/ready-to-ship
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"
  }
}
  • released is true when this call sent the order out. Calling again, or for an order booked as packed, answers 200 with released: false and changes nothing, so retries are safe.
  • Logged as a READY_TO_SHIP event (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

POST /orders/{id}/cancel Auth Required

What cancel does depends on where the parcel is.

Order isResult
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 / returned200 with the order unchanged. Cancelling twice is safe.
Delivered (4, 9, 11)409 CANCELLATION_NOT_ALLOWED
Full URL: https://delivery.alfast.in/api/v2/hld/orders/{id}/cancel

Request Parameters

FieldTypeRequiredDescription
reason_codestringRequiredOne of the codes below
reasonstring, max 500OptionalFree text. Required when reason_code is OTHER.
reason_codeMeaning
CUSTOMER_CANCELLEDCustomer cancelled the order
ORDER_MODIFIEDOrder changed; a new order will be booked
DUPLICATE_ORDERBooked twice by mistake
ADDRESS_ISSUEDelivery address cannot be served
STORE_NOT_READYPickup store cannot hand over the parcel
PAYMENT_ISSUEPayment problem
OTHERAnything else (send reason)

Sample Request

POST /api/v2/hld/orders/ORD-48213/cancel
{
  "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

POST {your_webhook_url} Sent by AL Services

Every event of every order is sent to your server as an HTTP POST with a JSON body, as it happens.

Setup: share your HTTPS webhook URL with the AL Services team and ask for v2 webhooks and a signing secret. The v2 format is switched on per account; until it is, your URL receives the v1 format. A separate URL for hyperlocal orders is supported.

Request Headers

HeaderDescription
Content-Typeapplication/json
X-Webhook-EventThe event name, same as event in the body, e.g. PICKED_UP
X-Webhook-TimestampWhen this attempt was sent (ISO 8601)
X-Event-IdSame as event_id in the body. Sent with the signature.
X-TimestampUnix time in seconds when this attempt was signed. Sent with the signature.
X-Signaturesha256= 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

X-Webhook-Event: PICKED_UP
{
  "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"
  }
}
FieldDescription
event_idUnique id of the event (UUID). The same event can arrive more than once: ignore an event_id you have already processed.
eventThe event name, see Status & Event Codes
sequence1, 2, 3 ... per order. Use it to put events in order and to spot a missed one.
status_code / previous_status_codeInternal status after and before the event
dataThe facts of this event (table below)
orderThe 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

Eventdata fields
Every eventstatus_text
RIDER_ASSIGNED, RIDER_REASSIGNEDrider: id, name, phone of the Field Executive
RIDER_AT_PICKUPdistance_m: how far from the store the Field Executive was when the 200 m mark was crossed
OUT_FOR_DELIVERYattempt: 1 on the first trip, 2 when the Field Executive goes out again after a failed attempt
DELIVERY_FAILED, DELIVERY_RESCHEDULEDreason_code, reason, attempt (attempt number)
RTO_INITIATEDreason_code, reason, return_otp. This is the only webhook that carries the return OTP: pass it to the store.
CANCELLEDcancelled_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.

Node.js (Express)
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
});
PHP
$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);
Python
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

Reply with any 2xx status within 10 seconds, then do the heavy work. Any other reply or a timeout is retried 7 more times: after 10 s, 30 s, 1 min, 5 min, 15 min, 30 min and 1 h (about 2 hours in all).
  • 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_sequence rather than waiting for retries.
  • Current state: Order Details always returns the order as it is now.

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

EventWhenStatus codes
ORDER_CREATEDOrder accepted by AL Services; a Field Executive is being found1
RIDER_ASSIGNEDA Field Executive accepted the order (name, phone and photo now in order.rider)2
RIDER_REASSIGNEDAL Services moved the order to another Field Executive2
RIDER_UNASSIGNEDThe Field Executive was taken off before pickup; a new one is being found1
HEADING_TO_PICKUPThe Field Executive is on the way to the store24
RIDER_AT_PICKUPSent 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_UPPickup OTP verified, pickup photo taken3
OUT_FOR_DELIVERYOn 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_DROPSent automatically when the Field Executive's GPS position comes within 200 m of the customer address20
DELIVERED finalDelivery OTP verified, delivery photo taken; for COD, cash collected (9)4 9
DELIVERY_FAILEDA delivery attempt failed (see Failed attempts)5 26 27 28
DELIVERY_RESCHEDULEDThe customer asked for another time; counts as a failed attempt29
RTO_INITIATEDThe parcel is going back to the store; return OTP in data.return_otp31
RTO_COMPLETED finalParcel handed back at the store against the return OTP, handover photo in order.rto.photos32
CANCELLED finalCancelled before pickup7 17 30
ORDER_UPDATEDYou changed the order with Edit Order; data.updated_fields lists the fields. In the Events API only, no webhook.unchanged
READY_TO_SHIPYou 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

Codestatus.textstatus.event
1 12 15 23Order placedORDER_CREATED
13 10Finding a riderORDER_CREATED
2Rider assignedRIDER_ASSIGNED
24Rider on the way to pickupHEADING_TO_PICKUP
3Picked upPICKED_UP
25Out for deliveryOUT_FOR_DELIVERY
20Rider near the dropNEAR_DROP
4 11DeliveredDELIVERED
9Delivered, cash collectedDELIVERED
5Delivery attempt failedDELIVERY_FAILED
26Customer not availableDELIVERY_FAILED
27Wrong or incomplete addressDELIVERY_FAILED
28Payment issueDELIVERY_FAILED
29Rescheduled by the customerDELIVERY_RESCHEDULED
31Returning to the storeRTO_INITIATED
32Returned to the storeRTO_COMPLETED
7 17 30CancelledCANCELLED
Final statuses: 4, 9, 11 (delivered), 7, 17, 30 (cancelled) and 32 (returned to the store).

Failed Attempts and the Second Attempt

  1. 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 (or DELIVERY_RESCHEDULED) is sent with data.reason_code, data.reason and data.attempt; the photo is in order.failure.photos.
  2. After the first failed attempt, AL Services 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_DELIVERY is sent again (status 25). Meanwhile the Field Executive calls the customer.
  3. If the second attempt also fails, the return to the store starts at once: RTO_INITIATED with rto.reason_code DELIVERY_FAILED and rto.initiated_by system.

Failure reasons

reason_codeMeaningStatus
CUSTOMER_NOT_AVAILABLECustomer not available at the address26
CUSTOMER_UNREACHABLECustomer not answering the phone26
PREMISES_CLOSEDPremises closed26
WRONG_ADDRESSWrong or incomplete address27
CUSTOMER_REFUSEDCustomer refused the parcel5
OTP_NOT_SHAREDCustomer did not share the delivery OTP5
PAYMENT_NOT_READYCustomer not ready to pay the COD amount28
CUSTOMER_RESCHEDULEDCustomer asked for another time (event DELIVERY_RESCHEDULED)29
OTHEROther; reason carries the Field Executive's note5

Return to Store (RTO)

  1. RTO_INITIATED (status 31): the Field Executive is taking the parcel back. A 6-digit return OTP is created for the store: data.return_otp in the webhook, rto.return_otp in Order Details. Give it to the store.
  2. 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.
  3. RTO_COMPLETED (status 32, final): the parcel is back at the store; rto.return_verified is true and rto.photos holds the handover photo.

If the store is closed or cannot take the parcel, the order stays at 31 and AL Services operations arrange the handover.

rto.reason_codeMeaning
DELIVERY_FAILEDDelivery could not be completed (including the automatic return after the second failed attempt)
CLIENT_CANCELLEDYou cancelled the order after pickup
CUSTOMER_REFUSEDCustomer refused the parcel
ADDRESS_NOT_SERVICEABLEAddress cannot be served
OTHEROther

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 CodeError CodeDescription
401UNAUTHENTICATEDNo Authorization: Bearer token
401INVALID_TOKENUnknown API token
403CLIENT_DEACTIVATEDThe account is deactivated
403HLD_NOT_ENABLEDHyperlocal delivery is not enabled for the account
404ORDER_NOT_FOUNDNo order with this id for your account
409CANCELLATION_NOT_ALLOWEDThe order is delivered and can no longer be cancelled
409ORDER_NOT_EDITABLEEdit Order: the order is already picked up or finished
409ORDER_CANCELLEDReady to Ship: the order is cancelled
422VALIDATION_FAILEDRequest validation failed; see errors
422SERVICE_NOT_AVAILABLEPickup or drop not served, or the zone is closed now; details in data
422NOTHING_TO_UPDATEEdit Order: no field to change was sent
422INVALID_PAYMENTEdit Order: payment_mode and cod_value do not match
422INVALID_PICKUP_WINDOWEdit Order: the pickup window would end before it starts
422RANGE_TOO_LONGKPI report: more than 31 days asked for
429-Rate limit reached; retry after Retry-After seconds
500ORDER_CREATION_FAILEDThe order could not be booked; retry with the same partner_order_id
Sandbox simulator only
404NOT_FOUNDThe simulator was called outside the sandbox
409INVALID_TRANSITIONThe requested step is not possible from the order's current status

KPI Report

GET /reports/kpi?from=YYYY-MM-DD&to=YYYY-MM-DD Auth Required

Service figures for your orders placed in the date range. Without parameters: yesterday. At most 31 days per call.

Query Parameters

ParameterTypeRequiredDescription
fromdateOptionalFirst day, YYYY-MM-DD (default yesterday)
todateOptionalLast day, included, on or after from (default: same as from)

Success Response 200 OK

GET /api/v2/hld/reports/kpi?from=2026-10-01&to=2026-10-15
{
  "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 }
  }
}
FieldMeaning
orders, delivered, cancelled, returned_to_store, returning_to_store, openOrder counts
on_time_pctDelivered within the order's expected_delivery_minutes
first_attempt_pctDelivered without a failed attempt
pickup_compliance_pct, pod_compliance_pctPickups / deliveries with the OTP verified and the required photos
cancellation_pct, rto_pctShare of orders cancelled / sent back to the store
avg_accept_minutes, avg_pickup_minutes, avg_delivery_minutes, avg_total_minutesPlaced to accepted, accepted to picked up, picked up to delivered, placed to delivered
failed_attempts, orders_with_failed_attempt, failure_reasons, top_failure_reasonsFailed 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_minutesReturns to the store: started, handed back, started automatically, share of picked-up parcels, average minutes from start to handover
cancellation_reasons, return_reasons, sla_breachesCounts by reason code / SLA stage

Percentages and averages are null when there is nothing to measure (for example no deliveries in the range).

Sandbox

POST /api/v2/sandbox/orders/{id}/simulate Sandbox only

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.

Full URL: https://staging.alfast.in/api/v2/sandbox/orders/{id}/simulate
Not available in production (answers 404 NOT_FOUND).

Request Parameters

FieldTypeRequiredDescription
tostringOptionalRIDER_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

  1. Book an order, simulate to DELIVERED and check every webhook.
  2. Book one and cancel it before pickup.
  3. Book one, simulate to PICKED_UP, cancel it, then simulate RTO_COMPLETED.
  4. Book one, simulate to OUT_FOR_DELIVERY, then DELIVERY_FAILED.
  5. Book the same partner_order_id twice and check the second answer is "duplicate": true.
  6. Send one request with a wrong token and check the 401.