Courier WingsAPI v1

Courier Wings API

Create orders, compare rates, book shipments, fetch labels and tracking, act on NDRs and read your wallet. Base URL https://courier.qrwings.com/api/v1. Machine-readable spec: openapi.yaml.

Authentication & test mode

Create keys in Developers inside your account. Send them as Authorization: Bearer <key>. Keys carry scopes (orders:read, orders:write, shipments:read, shipments:write, rates:read, wallet:read, webhooks:manage) and only see your account's data.

Test keys (cw_test_…) book only on sandbox courier connections — no real parcel moves, but charges still post to your wallet ledger and are refunded on cancel. Live keys (cw_live_…) use production couriers. Rotating a key issues a new one and keeps the old one valid for 24 hours.

Quick start

# 1. Create an order
curl -X POST https://courier.qrwings.com/api/v1/orders -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" -d '{
  "order_number": "WEB-1001", "external_id": "1001", "payment_mode": "cod",
  "customer_name": "Asha Verma", "customer_phone": "9876543210",
  "address1": "12 MG Road", "city": "Gurugram", "state": "Haryana", "pincode": "122001",
  "weight_kg": 0.6, "items": [{"name": "Cotton T-shirt", "sku": "TS-M", "quantity": 2, "unit_price": 499}]
}'

# 2. Compare couriers for it
curl -X POST https://courier.qrwings.com/api/v1/rates -H "Authorization: Bearer $KEY" -d '{"order_id": "<id>"}'

# 3. Book (omit courier_key to let your allocation rules choose)
curl -X POST https://courier.qrwings.com/api/v1/shipments -H "Authorization: Bearer $KEY" \
  -H "Idempotency-Key: WEB-1001-ship" -d '{"order_id": "<id>", "courier_key": "<from rates>"}'

# 4. Label PDF
curl -o label.pdf https://courier.qrwings.com/api/v1/shipments/<shipment id>/label -H "Authorization: Bearer $KEY"

Idempotency

Add an Idempotency-Key header (max 100 chars) to any POST. Repeating the same key with the same body within 24 hours returns the original response with Idempotent-Replayed: true; the same key with a different body returns 422 idempotency_conflict. Orders are also de-duplicated by external_id, and an order can never have two active shipments.

Errors & limits

Errors use one shape: {"error": {"code": "…", "message": "…", "details": {…}}}. Common codes: validation_failed (422), insufficient_balance (402), not_serviceable, duplicate_booking, not_cancellable (422), insufficient_scope (403), not_found (404), rate_limited (429). Default limit: 120 requests per minute per key. List endpoints accept page and per_page (max 100) and return meta.has_more.

Endpoints

GET/pingCheck your key
GET/ordersList orders
POST/ordersCreate order
GET/orders/{id}Order with items
POST/orders/{id}/cancelCancel unbooked order
POST/ratesRates & serviceability
GET/shipmentsList shipments
POST/shipmentsBook shipment
GET/shipments/{id}Shipment + tracking events
GET/shipments/{id}/labelLabel PDF
POST/shipments/{id}/cancelCancel before pickup
POST/shipments/pickupRequest pickup
POST/shipments/{id}/ndrRe-attempt or return
POST/shipments/{id}/returnBook a return pickup
GET/walletBalance
GET/wallet/transactionsLedger
GET/POST/webhooksWebhook endpoints
DELETE/webhooks/{id}Remove endpoint
GET/webhooks/{id}/deliveriesDelivery attempts

Returns: POST /shipments/{id}/return with a reason books a reverse pickup for a delivered shipment on a courier that supports it.

Webhooks

Events: shipment.booked, shipment.status_updated, shipment.delivered, shipment.ndr, shipment.rto, shipment.cancelled, order.created, return.booked, return.delivered. Each POST carries X-CourierWings-Event, X-CourierWings-Event-Id (dedupe on this) and X-CourierWings-Signature: t=<unix>,v1=<hex> where v1 = HMAC-SHA256(secret, "t.body"). Reject signatures older than 5 minutes. Non-2xx responses are retried after 1, 5, 30, 120, 720 and 1440 minutes.

// PHP
$body = file_get_contents('php://input');
preg_match('/t=(\d+),v1=([a-f0-9]{64})/', $_SERVER['HTTP_X_COURIERWINGS_SIGNATURE'] ?? '', $m);
$ok = $m && abs(time() - (int) $m[1]) <= 300
    && hash_equals(hash_hmac('sha256', $m[1].'.'.$body, getenv('CW_WEBHOOK_SECRET')), $m[2]);

// Node.js
const [, t, v1] = header.match(/t=(\d+),v1=([a-f0-9]{64})/) || [];
const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
const ok = v1 && Math.abs(Date.now() / 1000 - t) <= 300 && crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(expected));

Shipment statuses

created → booked → pickup_scheduled → picked_up → in_transit → out_for_delivery → delivered. Exceptions: ndr (failed attempt, action needed), rto_initiated → rto_in_transit → rto_delivered, cancelled, lost, failed (booking did not complete; charge refunded).