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 | /ping | Check your key |
| GET | /orders | List orders |
| POST | /orders | Create order |
| GET | /orders/{id} | Order with items |
| POST | /orders/{id}/cancel | Cancel unbooked order |
| POST | /rates | Rates & serviceability |
| GET | /shipments | List shipments |
| POST | /shipments | Book shipment |
| GET | /shipments/{id} | Shipment + tracking events |
| GET | /shipments/{id}/label | Label PDF |
| POST | /shipments/{id}/cancel | Cancel before pickup |
| POST | /shipments/pickup | Request pickup |
| POST | /shipments/{id}/ndr | Re-attempt or return |
| POST | /shipments/{id}/return | Book a return pickup |
| GET | /wallet | Balance |
| GET | /wallet/transactions | Ledger |
| GET/POST | /webhooks | Webhook endpoints |
| DELETE | /webhooks/{id} | Remove endpoint |
| GET | /webhooks/{id}/deliveries | Delivery 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).