Developers

Skyfleet API

REST API for custom integrations — book, track, reconcile, and receive real-time events.

Downloads

Import the Postman collection, set the api_key variable to your key, and every request is ready to run. The OpenAPI 3.1 spec (/openapi.json) is the machine-readable source of truth — import it into Postman/Insomnia/Swagger UI or generate a client SDK from it.

Overview

The Skyfleet API is a JSON REST API. All endpoints are relative to the base URL below and require authentication (except the public tracking endpoint). Use it to check rates, create and book shipments, track parcels, reconcile COD and wallet, handle NDR/returns, and receive real-time webhook events.

Base URL:  https://api.skyfleetnow.com/api/v1
Content-Type: application/json

All requests and responses are UTF-8 JSON. Timestamps are ISO-8601 (UTC). Monetary values are in INR (₹) unless a field name ends in Paise. Weights are in grams, dimensions in centimetres.

Authentication

Authenticate every request with an API key. Generate one in the dashboard under Settings → Developer → API Keys. The full key (sf_live_…) is shown once at creation — store it securely; you can revoke and re-create at any time. A key is scoped to your organization.

Send it in the X-API-Key header:

curl https://api.skyfleetnow.com/api/v1/wallet \
  -H "X-API-Key: sf_live_xxxxxxxxxxxxxxxxxxxxxxxx"

Keep keys server-side only — never ship them in a browser, mobile app, or public repo. Rotate immediately if exposed (revoke the old key, create a new one).

What a key can reach

Everything documented on this page: shipments, rates and serviceability, tracking, sales-channel orders, COD, wallet balance and transactions, NDR, returns, webhooks, and listing or revoking keys. Account administration is dashboard-only — team members, settings, KYC, bank details, creating new keys, wallet top-ups and anything under /admin. A key that calls one of those gets 403 with "This endpoint is not available to API keys — use the dashboard".

Troubleshooting: {"message":"Access token required"}

The same key works on every endpoint — there is no separate token. This error means the X-API-Key header didn't reach the request. Check, in order:

  • Use HTTPS, not HTTP. http://api.skyfleetnow.com returns a 301 redirect to https://, and most HTTP clients drop custom headers on redirect — so the key is lost. Always call https://api.skyfleetnow.com directly.
  • No trailing slash. Use /channels/orders, not /channels/orders/.
  • Send the header on every request. If it's set on one call but not another, set it as a default header on your HTTP client so all requests include it.
  • Exact header name: X-API-Key (not X_API_Key or X-Api_Key).

Other messages: "Invalid, revoked, or expired API key" = the key was received but doesn't match (regenerate it); 401 with a key = same.

Responses & errors

Every response uses a consistent envelope.

Success

{ "success": true, "data": { ... }, "message": "optional" }

Paginated list

{
  "success": true,
  "data": [ ... ],
  "meta": { "total": 120, "page": 1, "limit": 20, "totalPages": 6 }
}

List endpoints accept page (default 1) and limit (default 20).

Error

{ "success": false, "message": "Human-readable reason", "errors": { "field": ["..."] } }
StatusMeaning
400Bad request / validation failed — including an unserviceable pincode:"Drop pincode not serviceable: 999999 is not in our serviceable pincode list…" (also pickup). Returned by create, book and rate quotes; fix the pincode or ask support to add it.
401Missing / invalid / revoked API key
402Wallet not configured or insufficient balance (booking)
403Forbidden — KYC not approved, or an endpoint that is not available to API keys
404Not found
409Duplicate order reference (DUPLICATE_ORDER_REF); a booking already in progress for that shipment (retry after a moment); or every courier declined the booking

Error codes

Every order and booking error has the form "<Title>: <reason>. <what to do>" and a code: in data.booking.code when you create with book: true, otherwise in the code field of the error response.

CodeHTTPMeaning / what to do
INVALID_ORDER_DATA400A value no courier would accept: a phone number typed into invoiceValue or codAmount, a phone that isn't 10 digits, a pincode that isn't 6 digits, no weight, a missing name or address. The message lists every problem. Correct the order and book again.
PICKUP_PINCODE_NOT_SERVICEABLE400 / 409The pickup pincode is not in our serviceable list (400), or the chosen courier doesn't serve it (409). Fix the pincode or choose another courier.
DROP_PINCODE_NOT_SERVICEABLE400 / 409The same, for the drop pincode.
ROUTE_NOT_SERVICEABLE400 / 409Both pincodes are unknown, or the courier doesn't serve this pickup → drop route. Fix the pincodes or choose another courier.
PRICING_NOT_AVAILABLE409That courier has no price for this route and weight on your account. Check the weight and dimensions, or choose another courier.
ALLOCATION_NOT_SET400 / 409No courier is allocated for this order under your allocation rules, or the chosen courier isn't enabled on your account. Choose a courier, or ask support to set an allocation rule.
COURIER_REJECTED409The courier refused the order (a weight, COD or invoice limit, a restricted item…); the message gives its reason. Fix it or choose another courier.
COURIER_TEMPORARY_ERROR409The courier's booking system had a temporary error. Nothing is wrong with the order: retry in a few minutes or choose another courier.
INSUFFICIENT_WALLET_BALANCE402The wallet (or credit limit) can't cover the freight. Recharge and book again.
ACCOUNT_NOT_READY402 / 403KYC not approved, or the wallet is not set up. Complete KYC or contact support.
BOOKING_FAILED409Several couriers were tried and each failed for a different reason; the message lists each one.
DUPLICATE_ORDER_REF409A live shipment already uses this orderReferenceNumber. The response's data.shipment is that order and data.bookable is true when it has no AWB yet — to retry a failed booking, call POST /shipments/{data.shipment.id}/book instead of re-pushing. allowDuplicate: true creates a second, separate shipment.

Enums

  • ShipmentStatus: CREATED, LABEL_GENERATED, PICKUP_SCHEDULED, PICKUP_FAILED, PICKED_UP, IN_TRANSIT, OUT_FOR_DELIVERY, DELIVERED, DELIVERY_FAILED, RTO_INITIATED, RTO_IN_TRANSIT, RTO_DELIVERED, CANCELLED, LOST, DAMAGED
  • PaymentMode: PREPAID, COD
  • ShipmentType: FORWARD, REVERSE, EXCHANGE
  • NdrAction: REATTEMPT, RTO, CHANGE_ADDRESS, CONTACT_CUSTOMER

Booking prerequisites (KYC + wallet)

POST /shipments creates a draft (status CREATED, no AWB, no charge). Booking a draft (assigning a courier + AWB) is gated:

  • Your organization's KYC must be APPROVED (else 403).
  • A wallet must be configured (else 402); prepaid wallets must cover the freight.

Complete KYC and top up your wallet in the dashboard before booking via API.

Rates & serviceability

POSThttps://api.skyfleetnow.com/api/v1/rates/calculate
Get all available courier quotes for a lane.
curl https://api.skyfleetnow.com/api/v1/rates/calculate -X POST \
  -H "X-API-Key: sf_live_…" -H "Content-Type: application/json" \
  -d '{
    "pickupPincode": "110011",
    "dropPincode": "400001",
    "weightGrams": 500,
    "paymentMode": "PREPAID",
    "lengthCm": 20, "breadthCm": 15, "heightCm": 10,
    "invoiceValue": 1499
  }'

// 200 → data: [
  {
    "courier": "Delhivery Surface", "carrierId": "delhivery-surface",
    "zone": "METRO", "chargeableWeightGrams": 500,
    "baseRate": 46.61, "codCharge": 0, "gstAmount": 12.05,
    "totalCharge": 78.95, "eddDays": 3, "isAvailable": true
  }
]
GEThttps://api.skyfleetnow.com/api/v1/rates/serviceability/detailed?pickup=110011&drop=400001&serviceType=FORWARD
Per-courier serviceability (prepaid/COD/reverse support) for a lane.
GEThttps://api.skyfleetnow.com/api/v1/rates/classify?pickup=110011&drop=400001
Zone classification only.

Shipments

POSThttps://api.skyfleetnow.com/api/v1/shipments
Create a shipment draft.
curl https://api.skyfleetnow.com/api/v1/shipments -X POST \
  -H "X-API-Key: sf_live_…" -H "Content-Type: application/json" \
  -d '{
    "shipmentType": "FORWARD",
    "paymentMode": "COD",
    "orderReferenceNumber": "ORD-1042",
    "invoiceValue": 1499,
    "codAmount": 1499,
    "pickup": {
      "name": "Acme Warehouse", "phone": "9876543210",
      "address": "Plot 4, Industrial Area", "city": "New Delhi",
      "state": "Delhi", "pincode": "110011"
    },
    "drop": {
      "name": "Ravi Kumar", "phone": "9812345678",
      "address": "12 MG Road", "city": "Mumbai",
      "state": "Maharashtra", "pincode": "400001"
    },
    "packages": [{
      "weightGrams": 500, "lengthCm": 20, "breadthCm": 15, "heightCm": 10,
      "items": [{ "name": "Cotton T-Shirt", "sku": "TS-01", "quantity": 1, "unitPrice": 1499 }]
    }]
  }'

// 201 → data: { "shipment": { "id": "...", "status": "CREATED", ... } }

Required: shipmentType, paymentMode, invoiceValue, pickup, drop, packages (≥1, each with weight + dimensions + items). codAmount is required for COD. orderReferenceNumber must be unique among live orders — a duplicate returns 409 unless you pass "allowDuplicate": true. You can reference a saved pickup with pickupAddressId instead of an inline pickup object.

One call instead of two: create and book together

Add "book": true to the same payload and the draft is booked in the same request — the response carries the AWB, courier and label URL. The two-step flow (create, then POST /shipments/:id/book) keeps working exactly as before. There are three ways to choose the courier:

// Option 1 — a specific courier, by carrierId from a rate quote (recommended)
// POST /rates/calculate returns one option per serviceable courier; copy its carrierId.
// It is the exact service code, so it fixes Air vs Surface as well.
{ ...order fields..., "book": true, "carrierId": "delhivery-surface" }

// Option 2 — by brand, no quote needed (serviceType optional: SURFACE | AIR)
// Brands: DELHIVERY, BLUEDART, XPRESSBEES, EKART, DTDC, ECOM_EXPRESS, SHADOWFAX,
//         SMARTR, FEDEX, PROFESSIONAL_COURIER, AMAZON_SHIPPING (those enabled on your account)
{ ...order fields..., "book": true, "carrierBrand": "XPRESSBEES", "serviceType": "AIR" }

// Option 3 — auto-allocate: cheapest serviceable courier under your allocation rules, with fail-over
{ ...order fields..., "book": true }

// Rules: send carrierId OR carrierBrand. If both are present, carrierBrand wins.
// A carrierId that matches no active courier on your account falls back to auto-allocation.
curl https://api.skyfleetnow.com/api/v1/shipments -X POST \
  -H "X-API-Key: sf_live_…" -H "Content-Type: application/json" \
  -d '{ ...same fields as above..., "book": true, "carrierId": "delhivery-surface" }'

// 201 → data: { "shipment": { "id": "...", "status": "LABEL_GENERATED", "awbNumber": "77812345678",
//                             "sortCode": "DEL/OKH",   // courier sort code, null if none
//                             "labelUrl": "https://…", "courierName": "Delhivery Surface", ... },
//               "booking": { "status": "booked" } }

// If booking fails, the draft still exists — still 201:
// data: { "shipment": { "id": "...", "status": "CREATED", ... },
//         "booking": { "status": "failed", "error": "Insufficient wallet balance", "code": null, "httpStatus": 402 } }
// → fix the cause, then POST /shipments/{id}/book (with another carrierId if the courier could not serve the lane)

Pincodes must be serviceable. Both pincodes are checked when the order is created: if either is not in Skyfleet's pincode list the request is refused with 400 and a message naming the incorrect pincode, so the order never becomes a draft that fails at booking. Surrounding whitespace is stripped automatically. If a real pincode is rejected, contact support to have it added and resubmit.

POSThttps://api.skyfleetnow.com/api/v1/shipments/:id/book
Book a draft — assigns courier + AWB (KYC + wallet gate applies).
curl https://api.skyfleetnow.com/api/v1/shipments/SHIP_ID/book -X POST \
  -H "X-API-Key: sf_live_…" -H "Content-Type: application/json" \
  -d '{ "carrierId": "bluedart-air" }'          // exact rate code (recommended)
  // — OR —  -d '{ "carrierBrand": "DELHIVERY" }' // courier brand
  // — OR —  omit the body entirely to auto-allocate

// 200 → data: { "id": "...", "awbNumber": "...", "carrierId": "bluedart-air", "status": "LABEL_GENERATED",
//               "labelUrl": "https://api.skyfleetnow.com/api/v1/shipments/SHIP_ID/label.pdf", ... }
// labelUrl is a Skyfleet-branded label link — fetch it with your X-API-Key header (follows a redirect to the PDF).

Both fields are accepted (this resolves the earlier docs-vs-Postman ambiguity):

  • carrierId — the exact rate code incl. service (e.g. bluedart-air / bluedart-surface). Recommended — take it from /rates/calculate, which preserves the Air vs Surface pick.
  • carrierBrand — the courier brand (e.g. DELHIVERY, BLUEDART_AIR). Uppercase provider-account form.

Send either one. If both are present, carrierBrand selects the brand and carrierId refines the service. Omit both to auto-allocate by your allocation rules.

All carrierId values

CourierAirSurface
Blue Dartbluedart-airbluedart-surface
Delhiverydelhivery-airdelhivery-surface
DTDCdtdc-airdtdc-surface
Xpressbeesxpressbees-airxpressbees-surface
Ekart—ekart-surface
Amazon—amazon-surface
Professional—professional-surface
Shadowfax—shadowfax-surface

Uppercase forms (BLUEDART_AIR, …) also work. If a carrierId isn't serviceable/priced for that lane + merchant, the booking returns a clear error rather than silently switching couriers.

GEThttps://api.skyfleetnow.com/api/v1/shipments?status=IN_TRANSIT&page=1&limit=20
List shipments. Filters: status, statusGroup, paymentMode, carrier, search, from, to, awb, orderId, hasAwb, unprocessed.

status takes a single ShipmentStatus (see Enums). For broad buckets use statusGroup:

statusGroupIncludes
pickupsAwaiting/scheduled pickup
inTransitPicked up → out for delivery
deliveryFailedFailed delivery attempts (NDR)
deliveredDelivered
rtoRTO initiated / in transit / delivered
pickupBooked, awaiting pickup, not yet on a pickup manifest
manifestAwaiting pickup and listed on a pickup manifest
transitPicked up / in transit (before out-for-delivery)
ofdOut for delivery
lostLost or damaged
cancelledCancelled

Also: hasAwb=yes|no (booked vs draft), unprocessed=yes (drafts needing action).

Note: statusGroup only groups results for filtering — it never collapses the data. Every shipment always returns its exact status (e.g. OUT_FOR_DELIVERY, RTO_IN_TRANSIT). To filter to one precise state use ?status=OUT_FOR_DELIVERY, and there's a dedicated shipment.out_for_delivery webhook. You never lose granularity.
GEThttps://api.skyfleetnow.com/api/v1/shipments/:id
Full shipment detail incl. packages, items, and tracking events.
PATCHhttps://api.skyfleetnow.com/api/v1/shipments/:id
Edit a draft (only while CREATED, no AWB). After booking this returns 400; cancel and create a new shipment instead.
  • Editable: paymentMode, invoiceValue, codAmount, dropName, dropPhone, dropEmail, dropAddress, dropCity, dropState, dropPincode, dropLandmark, pickupAddressId, totalWeight (kg), totalLength, totalBreadth, totalHeight (cm), isFragile, isInsured, isOtpDelivery, isQcRequired, remarks, internalNotes, items. Other fields are ignored without an error, including packages and the pickup address fields.
  • Changing weight or dimensions: send them together with items. Rates and the courier booking read the package, and the package is only rebuilt when items is sent, so without items these fields do not change the price or what the courier receives. The book call cannot change them either, so edit first and then book.
  • Sending items replaces the whole list and saves the shipment as a single package. For a multi-package order, create a new shipment instead. Any weight or dimension you leave out keeps its saved value.
  • Each item needs itemName (or name, as in POST /shipments), and a row with neither is dropped. If no usable row is left and the shipment already has items, the call returns 400 and nothing changes.
  • An item that matches an existing one (by id, then sku, then name) keeps any details you don't send, such as hsnCode or category. Send null to clear one.
{
  "totalWeight": 2.5,
  "totalLength": 30,
  "totalBreadth": 20,
  "totalHeight": 15,
  "items": [
    { "itemName": "Cotton T-Shirt", "sku": "TS-001", "quantity": 1, "unitPrice": 500 }
  ]
}
POSThttps://api.skyfleetnow.com/api/v1/shipments/:id/cancel
Cancel a shipment. After pickup, a cancellation becomes a return to origin (RTO). Body: { "reason": "..." } (optional).
  • :id is the internal id returned as data.shipment.id on create — not the shipment number (SKF…), order reference or AWB. Have only one of those? Look it up with GET https://api.skyfleetnow.com/api/v1/shipments?search=<number | AWB | order ref> and use data[0].id.
  • Before pickup (CREATED, LABEL_GENERATED, PICKUP_SCHEDULED, PICKUP_FAILED) the shipment is cancelled. After pickup (PICKED_UP, IN_TRANSIT, OUT_FOR_DELIVERY, DELIVERY_FAILED) the parcel is already with the courier, so the cancellation becomes a return: status RTO_INITIATED, rtoInitiated: true in the response, a shipment.rto_initiated webhook, RTO charges and no freight refund. The same rule applies to cancellations from the dashboard, a connected store or a courier scan. Already on RTO: success with alreadyReturning: true, nothing changed. Delivered or already cancelled: 400.
  • Response data: courierCancelled, courierError (the courier's reason if it rejected — the shipment is still cancelled on Skyfleet) and refunded (freight credited back; PREPAID wallets only, otherwise 0).
  • Store orders (Shopify / WooCommerce): the store order goes back to unfulfilled and a fresh draft is created for it, so it can be booked again.
GEThttps://api.skyfleetnow.com/api/v1/shipments/:id/label.pdf
Stable Skyfleet-branded label link — 302-redirects to the PDF. This is the URL returned as labelUrl in booking / shipment responses.
GEThttps://api.skyfleetnow.com/api/v1/shipments/:id/label-url
Returns a 1-hour presigned label PDF URL as JSON ({ url, ours }).
GEThttps://api.skyfleetnow.com/api/v1/shipments/:id/invoice-url
1-hour presigned commercial-invoice PDF URL.
GEThttps://api.skyfleetnow.com/api/v1/shipments/:id/pod
Proof of delivery for a delivered / RTO-delivered shipment: { recipientName, recipientRelation, recipientContact, documentUrl, documentUrls, fetchedAt }. documentUrl is a downloadable signed-POD document (open or download directly). Where the courier exposes POD on demand, the first call fetches and caches it, so allow a moment for the initial request.
Labels are Skyfleet-branded. The labelUrl in booking and shipment responses points at /shipments/:id/label.pdf — fetch it with your X-API-Key header and follow the redirect (any standard HTTP client does this automatically). It always serves the current Skyfleet label, so don't cache the redirected URL — re-fetch the endpoint when you need the file. (Amazon-carrier shipments return Amazon's own label.)
POSThttps://api.skyfleetnow.com/api/v1/shipments/bulk-download
Merge labels or zip invoices. Body: { "shipmentIds": [...], "type": "labels" | "invoices" }.

Sales Channel orders

Orders synced from a connected store (Shopify, Amazon, WooCommerce, etc.) are a separate resource from shipments. They live under /channels/orders. A channel order only appears under /shipments after it's imported — so if you're looking for freshly-synced store orders, use the endpoints here, not /shipments.

GEThttps://api.skyfleetnow.com/api/v1/channels/orders?status=NEW&page=1&limit=20
List synced channel orders. Filters: status, integrationId, errorType.
curl "https://api.skyfleetnow.com/api/v1/channels/orders?status=NEW" -H "X-API-Key: sf_live_…"

// 200 → data: [
  {
    "id": "co_abc", "externalOrderId": "5521", "externalOrderNumber": "#1042",
    "customerName": "Ravi Kumar", "destinationCity": "Mumbai", "destinationPincode": "400001",
    "paymentMode": "COD", "totalValue": 1499, "status": "NEW",
    "integration": { "name": "My Shopify Store", "type": "SHOPIFY" },
    "shipmentId": null, "shipmentStatus": null, "shipmentAwb": null
  }
], "meta": { "total": 12, "page": 1, "limit": 20, "totalPages": 1 }

status values: NEW (synced, not yet imported), IMPORTED, SKIPPED, FAILED, CANCELLED_ON_CHANNEL.

GEThttps://api.skyfleetnow.com/api/v1/channels
List the merchant's connected sales channels.
GEThttps://api.skyfleetnow.com/api/v1/channels/orders/stats
Counts by status / error type.
POSThttps://api.skyfleetnow.com/api/v1/channels/orders/:id/book
One call: import + book a single order — the simplest way to ship one synced order. Returns the AWB.
curl "https://api.skyfleetnow.com/api/v1/channels/orders/co_abc/book" -X POST \
  -H "X-API-Key: sf_live_…" -H "Content-Type: application/json" \
  -d '{ "carrierId": "bluedart-air" }'   // optional; omit to auto-allocate

// 200 → data: { "channelOrderId":"co_abc", "shipmentId":"ship_x", "awbNumber":"77812345678", "shipment": { ... } }
POSThttps://api.skyfleetnow.com/api/v1/channels/orders/:id/import
Import only — turn one channel order into a shipment draft (book later). Returns the created shipment.
POSThttps://api.skyfleetnow.com/api/v1/channels/orders/bulk-import
Import many at once. Body: { "orderIds": [...], "mode": "draft" | "allocate" | "book" }.
POSThttps://api.skyfleetnow.com/api/v1/channels/orders/:id/skip
Mark a channel order as skipped (ignore it).

Typical flow: pull store orders → ship

GET  /channels/orders?status=NEW          → un-imported synced orders
POST /channels/orders/{id}/import         → creates a draft shipment (returns shipmentId)
POST /shipments/{id}/book                 → book it (returns AWB)

// One-shot alternative:
POST /channels/orders/bulk-import   { "orderIds": ["co_abc"], "mode": "book" }

Orders sync but don't auto-import? Turn on auto-create

By default, synced orders sit as NEW in the channel-order feed until you import them. To have Skyfleet auto-create a draft shipment for every incoming order, enable autoCreateShipments on the integration (and set a default pickup, since a shipment needs one):

PATCHhttps://api.skyfleetnow.com/api/v1/channels/:id
Update an integration's sync settings.
curl "https://api.skyfleetnow.com/api/v1/channels/INTEGRATION_ID" -X PATCH \
  -H "X-API-Key: sf_live_…" -H "Content-Type: application/json" \
  -d '{
    "autoCreateShipments": true,
    "defaultPickupAddressId": "addr_123",   // required for auto-create
    "defaultPaymentMode": "COD",            // optional fallback
    "defaultWeightGrams": 500,              // optional fallback
    "initialSyncDays": 30
  }'

Get integration IDs from GET /channels, and pickup-address IDs from GET /shipments/pickup-addresses. With autoCreateShipments off, orders stay in the feed and you import them yourself (above). Either way, auto-create produces drafts — call /shipments/:id/book to book, or use bulk-import with mode:"book".

Mental model: /channels/orders = raw orders from the store · /shipments = orders in Skyfleet's shipping pipeline (drafts + booked). Import bridges the two.

Tracking

GEThttps://api.skyfleetnow.com/api/v1/shipments/:awb/track
Authenticated tracking — returns { shipment, tracking } with the live courier timeline. Every status is a Skyfleet status (the same values webhooks send): tracking.current_status and each events[].status. The courier's own codes are kept alongside as raw_status / raw_status_code. Courier codes differ by connection: couriers booked through our aggregator (Delhivery, Bluedart, Xpressbees…) send numeric codes (50 = pickup failed), Shadowfax direct sends words (pickup_not_attempted). Both become PICKUP_FAILED, so drive your logic from status / current_status and treat status_code and the raw_* fields as display information. A courier code with no Skyfleet equivalent (a delay notice) keeps the previous status. Responses are served from a short cache (up to 45 s as-is, refreshed in the background up to 5 min), so polling the same AWB is cheap; add ?fresh=1 to force a live courier pull. Prefer webhooks to polling.
GEThttps://api.skyfleetnow.com/api/v1/public/track/:awb
Public (no auth) — safe to expose to buyers. Sanitized (no full address/phone/COD).
curl https://api.skyfleetnow.com/api/v1/public/track/DL1234567890

// 200 → data: {
  "awbNumber": "DL1234567890", "status": "OUT_FOR_DELIVERY",
  "courier": "Delhivery", "customerName": "Ravi K.",
  "timeline": { "orderedAt": "...", "pickedUpAt": "...", "deliveredAt": null, "expectedDeliveryDate": "..." },
  "events": [ { "status": "OUT_FOR_DELIVERY", "description": "...", "location": "Mumbai", "eventTime": "..." } ]
}

For most integrations, prefer webhooks over polling — you get pushed a status event the moment it changes.

COD

GEThttps://api.skyfleetnow.com/api/v1/cod/remittances?status=PROCESSING&page=1
Your COD remittances (payouts).
GEThttps://api.skyfleetnow.com/api/v1/cod/remittance-schedule
Per-cycle COD / freight / VAS statement.
GEThttps://api.skyfleetnow.com/api/v1/cod/eligible
COD past its cycle window, ready to remit.
GEThttps://api.skyfleetnow.com/api/v1/cod/upcoming
Forward-looking COD with expected remittance dates.
GEThttps://api.skyfleetnow.com/api/v1/cod/transactions?from=2026-07-01&to=2026-07-31
Per-parcel COD ledger (paginated). CSV at /cod/transactions.csv.

Wallet

GEThttps://api.skyfleetnow.com/api/v1/wallet
Current balance and wallet record.
GEThttps://api.skyfleetnow.com/api/v1/wallet/config
Wallet type, COD cycle config, and bank details.
GEThttps://api.skyfleetnow.com/api/v1/wallet/transactions?category=orders&page=1
Wallet ledger (paginated). category: recharges | orders. CSV at /wallet/transactions.csv.

NDR (failed deliveries)

GEThttps://api.skyfleetnow.com/api/v1/ndr?status=PENDING&page=1
Open NDRs. Stats at /ndr/stats.

Filters: status, reason, page, limit, plus awb and orderReferenceNumber(substring match) to fetch the NDR event(s) for a specific shipment — e.g. /ndr?orderReferenceNumber=ORD-1042 or /ndr?awb=DL1234567890.

Where ndrEventId comes from: Skyfleet creates one NDR event per failed delivery attempt. Call GET https://api.skyfleetnow.com/api/v1/ndr?awb=<AWB> and use the id of the newest event that is still open (PENDING, ACTION_TAKEN or REATTEMPT_SCHEDULED). It is not the AWB, shipment id or order reference. A typical flow: the shipment.delivery_failed webhook arrives, you fetch the NDR for that AWB, then act on it.

curl "https://api.skyfleetnow.com/api/v1/ndr?awb=50845018205256" -H "X-API-Key: sf_live_…"

// 200 → { "success": true, "data": [ {
//   "id": "cmw1ndr8x0012ab34cd56ef78",     // ← this is the ndrEventId
//   "awbNumber": "50845018205256", "attemptNumber": 1,
//   "ndrReason": "CUSTOMER_NOT_AVAILABLE", "status": "PENDING", "location": "Mumbai_Andheri_DC", ...
// } ], "meta": { "total": 1, "page": 1, "limit": 20 } }
POSThttps://api.skyfleetnow.com/api/v1/ndr/action
Act on an NDR. Returns 404 for an unknown id and 400 if the NDR is already resolved.
curl https://api.skyfleetnow.com/api/v1/ndr/action -X POST \
  -H "X-API-Key: sf_live_…" -H "Content-Type: application/json" \
  -d '{
    "ndrEventId": "NDR_ID",
    "action": "REATTEMPT",          // or RTO | CHANGE_ADDRESS | CONTACT_CUSTOMER
    "preferredDate": "2026-07-12",
    "notes": "Customer will be home after 5pm"
  }'

For action: "CHANGE_ADDRESS", also send the corrected consignee details:

  -d '{
    "ndrEventId": "NDR_ID",
    "action": "CHANGE_ADDRESS",
    "newAddress": "12 MG Road, Indiranagar",   // REQUIRED
    "newPincode": "560038",                     // REQUIRED (6-digit)
    "newCity": "Bengaluru",                     // optional
    "newState": "Karnataka",                    // optional
    "newPhone": "9876543210"                    // optional — corrected contact
  }'
// To change ONLY the phone: resend the current address in newAddress/newPincode with the new newPhone.
POSThttps://api.skyfleetnow.com/api/v1/ndr/bulk-action
Body: { "ndrEventIds": [...], "action": "REATTEMPT", "notes": "..." }. Actions: REATTEMPT, RTO or CONTACT_CUSTOMER (address changes go through /ndr/action). Results are reported per NDR.

Returns (Reverse pickup)

POSThttps://api.skyfleetnow.com/api/v1/rvp/from-forward
Create a reverse pickup from a delivered forward shipment.
curl https://api.skyfleetnow.com/api/v1/rvp/from-forward -X POST \
  -H "X-API-Key: sf_live_…" -H "Content-Type: application/json" \
  -d '{
    "forwardAwb": "DL1234567890",              // or "forwardShipmentId"
    "orderReferenceNumber": "RET-1042",        // optional — YOUR ref, echoed in webhooks
    "isQcRequired": false,                       // default (defaults to "<forward-ref>-RVP")
    "reasonForReturn": "Size mismatch"
  }'

// → returns a REVERSE shipment draft; book it to schedule the pickup.

The addresses are swapped automatically (customer → pickup, your warehouse → drop). Pass orderReferenceNumber to set your own reference — it's echoed back on every webhook for the reverse shipment, exactly like forward orders.

POSThttps://api.skyfleetnow.com/api/v1/rvp/create
Create a standalone reverse pickup — no originating forward order needed (e.g. returns for orders placed on another platform). You supply both addresses.
curl https://api.skyfleetnow.com/api/v1/rvp/create -X POST \
  -H "X-API-Key: sf_live_…" -H "Content-Type: application/json" \
  -d '{
    "orderReferenceNumber": "RET-9001",         // optional — YOUR ref, echoed in webhooks
    "pickup": {                                  // where the courier collects (the customer)
      "name": "Asha R", "phone": "9876543210",
      "address": "12 MG Road", "city": "Bengaluru", "state": "Karnataka", "pincode": "560038"
    },
    "drop": {                                    // where it returns to (your warehouse)
      "name": "Acme Returns", "phone": "9000000000",
      "address": "Plot 4, Sector 8", "city": "Gurugram", "state": "Haryana", "pincode": "122001"
    },
    "items": [{ "itemName": "Blue Kurta", "sku": "KUR-BLU-M", "quantity": 1, "unitPrice": 899 }],
    "isQcRequired": false,
    "weightKg": 0.5                              // optional (default 0.5); dimensionsCm optional
  }'

// → returns a REVERSE shipment draft; book it with POST /shipments/:id/book.
GEThttps://api.skyfleetnow.com/api/v1/rvp?status=PICKED_UP&page=1
List reverse pickups.
GEThttps://api.skyfleetnow.com/api/v1/rvp/stats
Counts for the RVP dashboard: { total, qcRequired, byStatus }.

Doorstep QC (quality check)

Set isQcRequired: true on either create call to have the pickup agent inspect the item at the doorstep before accepting it. To give the agent something to check against, pass each item's imageUrl — it's forwarded to the courier as the reference photo:

"items": [
  {
    "itemName": "Blue Kurta", "sku": "KUR-BLU-M",
    "quantity": 1, "unitPrice": 899,
    "imageUrl": "https://cdn.yourstore.com/kur-blu-m.jpg",   // QC reference photo
    "color": "Blue", "brand": "Acme", "hsnCode": "6104"      // all optional
  }
]

When the item fails QC, the reverse shipment carries the result — readable on GET /shipments/:id and pushed on shipment.delivery_failed:

{
  "qcStatus": "fail",                       // "passed" | "fail" | "fail_hub" | null
  "qcRemarks": "Brand tag missing; colour mismatch",
  "qcImages": ["https://…/qc-1.jpg", "https://…/qc-2.jpg"],   // photos taken at the door
  "qcCheckedAt": "2026-08-18T10:22:05.000Z"
}

Booking a reverse shipment (POST /shipments/:id/book) rate-shops only the reverse (RVP) courier lane — forward couriers are never offered for a return.

Webhooks (real-time events)

Instead of polling, subscribe to events and Skyfleet will POST them to your URL the moment they happen. Create and manage endpoints in the dashboard under Settings → Webhooks (set a URL, pick events, get a signing secret).

Events

shipment.created            shipment.out_for_delivery
shipment.label_generated    shipment.delivered
shipment.pickup_scheduled   shipment.delivery_failed
shipment.pickup_failed      shipment.rto_initiated
shipment.picked_up          shipment.rto_in_transit
shipment.in_transit         shipment.rto_delivered
shipment.cancelled          shipment.lost
shipment.damaged

One event per shipment status. Every payload echoes your orderReferenceNumber (and awbNumber), so you can reconcile against your own order — including reverse shipments. Note: shipment.created is subscribable but not currently emitted (a shipment first surfaces on shipment.label_generated, i.e. once it's booked). shipment.pickup_failed fires when the courier attempts and fails a pickup; the courier re-attempts it.

Replay a real shipment (testing)

POST https://api.skyfleetnow.com/api/v1/webhooks/replay with { "awbNumber": "..." } re-sends that shipment's current status to every active subscription, as a normal signed webhook with the full data.tracking history. awbNumber also accepts the shipment number or your order reference. Earlier statuses arrive inside data.tracking.events, not as separate webhooks. To check your endpoint is reachable first, use POST https://api.skyfleetnow.com/api/v1/webhooks/:id/test (a test.ping).

curl -X POST https://api.skyfleetnow.com/api/v1/webhooks/replay \
  -H "X-API-Key: sf_live_..." -H "Content-Type: application/json" \
  -d '{ "awbNumber": "50845018127954" }'

// 200 → { "success": true, "data": { "awbNumber": "50845018127954", "status": "PICKUP_FAILED" } }

Payload

POST <your-url>
Headers:
  X-Skyfleet-Event: shipment.delivered
  X-Skyfleet-Signature: <hmac-sha256 hex of the raw body>
  X-Skyfleet-Delivery-Attempt: 1

Body:
{
  "success": true,
  "event": "shipment.delivered",
  "timestamp": "2026-07-08T09:15:00.000Z",
  "data": {
    "shipment": {                          // fields are nested under data.shipment
      "id": "ckv…",
      "shipmentNumber": "SKY100234",
      "awbNumber": "DL1234567890",
      "sortCode": "DEL/OKH",                // courier sort code, null if none
      "orderReferenceNumber": "ORD-1042",
      "shipmentType": "FORWARD",
      "status": "DELIVERED",
      "paymentMode": "COD",
      "courier": "Delhivery",
      "codAmount": 1499,
      "deliveredAt": "2026-07-08T09:14:55.000Z",
      "lastStatusUpdate": "2026-07-08T09:14:55.000Z",
      "expectedDeliveryDate": "2026-07-09T00:00:00.000Z",
      "revisedDeliveryDate": null,
      "chargedWeight": 0.5,                // kg, at booking
      "billedWeight": null,                // kg, after courier re-weigh
      "..." : "..."
    },
    "tracking": {                          // same shape as GET /shipments/{awb}/track
      "awb_number": "DL1234567890",
      "courier": "Delhivery",
      "current_status": "DELIVERED",
      "events": [                          // oldest first
        {
          "status": "PICKED_UP",            // Skyfleet status
          "status_code": "60",
          "description": "Shipment picked up",
          "remarks": "Shipment picked up",   // same text as description
          "location": "Faridabad_MathuraRoad_GW (Haryana)",
          "timestamp": "2026-07-06T11:02:10.000Z",
          "raw_status": "Picked Up",        // courier's own wording
          "raw_status_code": "60"
        },
        { "status": "DELIVERED", "status_code": "100", "description": "Delivered to consignee", "...": "..." }
      ]
    }
  }
}

data.tracking is the shipment's full recorded history at the time the webhook is sent, so you don't need a follow-up call to the tracking API. Each event carries a Skyfleet status plus the courier's raw_status / raw_status_code, description (also sent as remarks), location and timestamp. A webhook fires once per status change, not on every courier scan, so scans between two status changes arrive in the next webhook's history. data.tracking is null only if the history couldn't be read; the status webhook is still sent.

Verify the signature

Compute an HMAC-SHA256 of the raw request body using your endpoint's secret and compare (constant-time) to the X-Skyfleet-Signature header. Reject if they differ.

// Node.js (Express)
import crypto from "crypto";

app.post("/skyfleet-webhook", express.raw({ type: "*/*" }), (req, res) => {
  const expected = crypto.createHmac("sha256", process.env.SKYFLEET_WEBHOOK_SECRET)
    .update(req.body)                    // raw Buffer, not parsed JSON
    .digest("hex");
  const got = req.headers["x-skyfleet-signature"];
  if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(String(got)))) {
    return res.status(401).end();
  }
  const event = JSON.parse(req.body.toString());
  // ... handle event.event / event.data ...
  res.sendStatus(200);                   // 2xx = acknowledged
});

Respond 2xx to acknowledge. Non-2xx, timeouts, and network errors are retried with backoff (4xx is treated as a caller bug and not retried). Every attempt is logged in Settings → Webhooks → Deliveries, where you can inspect and manually retry.

Questions or need a sandbox key? Email support@skyfleetnow.com. See also our Security and Privacy pages.