Node.js integration

Shiprocket API integration in Node.js: a step-by-step guide

The whole shipping flow for an online store in plain Node.js: check a pincode, create the order, book a courier, print the label and track the parcel.

Sumit KumarSoftware engineer, Vashi

· 10 min read

Shiprocket's API documentation lists every endpoint, and there are ready-made packages for PHP and Laravel. A clear Node.js walkthrough is harder to find, so this guide covers the whole flow I use when an online store needs shipping: log in, check which couriers deliver to a pincode, create the order, assign a courier, schedule the pickup, print the label and track the parcel.

The examples use plain fetch (built into Node 18 and later), so there is nothing extra to install and they work with Express, Next.js route handlers or any other Node backend.

The short answer

To integrate Shiprocket with Node.js, create an API user in the Shiprocket panel, then call POST /auth/login on https://apiv2.shiprocket.in/v1/external to get a token that lasts 10 days. Send it as a Bearer token to create an order (/orders/create/adhoc), assign a courier (/courier/assign/awb), schedule a pickup (/courier/generate/pickup) and track it by AWB number (/courier/track/awb/{awb}).

Before you start

  1. Create an API user.In the Shiprocket panel, go to Settings → API and add an API user with its own email and password. Don't use your main login: the API user can be rotated or removed without locking you out.
  2. Add a pickup location. Every order names a pickup location, and it must exactly match the nickname of an address saved in your Shiprocket account (for example Primary).
  3. Keep the credentials on the server. They go in environment variables, never in frontend code.
.env
SHIPROCKET_EMAIL=api-user@yourstore.com
SHIPROCKET_PASSWORD=your-api-user-password
SHIPROCKET_PICKUP_LOCATION=Primary

Log in and reuse the token

Every request needs a token from the login endpoint. Shiprocket's tokens are valid for 240 hours, so log in once and reuse the token instead of logging in on every request. This small client caches it, refreshes it a day early, and turns API errors into readable exceptions:

lib/shiprocket.js
const BASE = 'https://apiv2.shiprocket.in/v1/external';
const TOKEN_LIFETIME_MS = 9 * 24 * 60 * 60 * 1000; // tokens last 10 days; refresh after 9

let cached = { token: null, expiresAt: 0 };

async function getToken() {
  if (cached.token && Date.now() < cached.expiresAt) return cached.token;

  const res = await fetch(`${BASE}/auth/login`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      email: process.env.SHIPROCKET_EMAIL,
      password: process.env.SHIPROCKET_PASSWORD,
    }),
  });
  if (!res.ok) throw new Error(`Shiprocket login failed (${res.status})`);

  const { token } = await res.json();
  cached = { token, expiresAt: Date.now() + TOKEN_LIFETIME_MS };
  return token;
}

// Every other call goes through here
async function shiprocket(path, { method = 'GET', body } = {}) {
  const token = await getToken();
  const res = await fetch(`${BASE}${path}`, {
    method,
    headers: {
      'Content-Type': 'application/json',
      Authorization: `Bearer ${token}`,
    },
    body: body ? JSON.stringify(body) : undefined,
  });

  const data = await res.json().catch(() => ({}));
  if (res.status === 401) cached.token = null; // log in again next time
  if (!res.ok) {
    throw new Error(`Shiprocket ${method} ${path} failed (${res.status}): ${data.message ?? 'no message'}`);
  }
  return data;
}

module.exports = { shiprocket };

Check which couriers deliver to a pincode

Before taking an order, check that someone actually delivers to the customer's pincode, and whether cash on delivery is possible there. The same call returns each courier's rate and estimated delivery, which you can show at checkout.

lib/shipping.js
const { shiprocket } = require('./shiprocket');

async function getCouriers({ pickupPincode, deliveryPincode, weightKg, cod }) {
  const query = new URLSearchParams({
    pickup_postcode: pickupPincode,
    delivery_postcode: deliveryPincode,
    weight: String(weightKg), // in kg, e.g. 0.5
    cod: cod ? '1' : '0',
  });
  const result = await shiprocket(`/courier/serviceability/?${query}`);
  return result.data?.available_courier_companies ?? [];
}

// Example (inside an async route handler): the cheapest courier that delivers
const couriers = await getCouriers({ pickupPincode: '400703', deliveryPincode: '560001', weightKg: 0.5, cod: false });
const cheapest = couriers.sort((a, b) => a.rate - b.rate)[0];

An empty list means nobody delivers there with those settings. Tell the customer at checkout, not after they have paid.

Create the order in Shiprocket

The “adhoc” endpoint creates an order without needing your products in Shiprocket's catalogue, which suits most custom stores. Map your own order to Shiprocket's fields:

lib/shipping.js
async function createShipment(order) {
  const result = await shiprocket('/orders/create/adhoc', {
    method: 'POST',
    body: {
      order_id: order.number, // your own order number; must be unique
      order_date: order.createdAt.toISOString().slice(0, 16).replace('T', ' '), // "2026-10-15 14:30"
      pickup_location: process.env.SHIPROCKET_PICKUP_LOCATION,
      billing_customer_name: order.customer.firstName,
      billing_last_name: order.customer.lastName,
      billing_address: order.address.line1,
      billing_city: order.address.city,
      billing_pincode: order.address.pincode,
      billing_state: order.address.state,
      billing_country: 'India',
      billing_email: order.customer.email,
      billing_phone: order.customer.phone, // 10 digits
      shipping_is_billing: true,
      order_items: order.items.map((item) => ({
        name: item.name,
        sku: item.sku,
        units: item.quantity,
        selling_price: item.price, // in rupees, per unit
      })),
      payment_method: order.cod ? 'COD' : 'Prepaid',
      sub_total: order.subtotal,
      length: 30, // package size in cm
      breadth: 20,
      height: 5,
      weight: 0.5, // in kg
    },
  });

  // Save these: every later call needs them
  return { shiprocketOrderId: result.order_id, shipmentId: result.shipment_id };
}

If the customer's shipping address is different from their billing address, set shipping_is_billing to false and send the matching shipping_* fields as well.

Assign a courier and get the AWB number

The AWB (air waybill) number is the courier's tracking number. Assigning one books the courier for this shipment. Pass the courier you chose from the serviceability check, or leave courier_id out and Shiprocket picks one based on your courier priority settings.

lib/shipping.js
async function assignCourier(shipmentId, courierId) {
  const result = await shiprocket('/courier/assign/awb', {
    method: 'POST',
    body: { shipment_id: shipmentId, ...(courierId ? { courier_id: courierId } : {}) },
  });
  const awb = result.response?.data?.awb_code;
  if (!awb) throw new Error('No AWB assigned: ' + JSON.stringify(result));
  return { awb, courier: result.response.data.courier_name };
}

Assigning an AWB uses your Shiprocket wallet balance, so a low balance is a common reason for this call to fail. Check the error message before retrying.

Schedule the pickup and print the label

lib/shipping.js
async function schedulePickup(shipmentId) {
  return shiprocket('/courier/generate/pickup', {
    method: 'POST',
    body: { shipment_id: [shipmentId] },
  });
}

async function getLabel(shipmentId) {
  const result = await shiprocket('/courier/generate/label', {
    method: 'POST',
    body: { shipment_id: [shipmentId] },
  });
  return result.label_url; // a PDF to print and stick on the parcel
}

In an admin panel, these make good buttons: “Request pickup” and “Download label” next to each paid order, so whoever packs the parcels never has to open the Shiprocket panel.

Track the shipment

There are two ways to keep an order's status up to date, and most stores use both.

1. Ask for the status

lib/shipping.js
async function track(awb) {
  const result = await shiprocket(`/courier/track/awb/${awb}`);
  const data = result.tracking_data ?? {};
  return {
    status: data.shipment_track?.[0]?.current_status,
    activities: data.shipment_track_activities ?? [], // newest first: date, activity, location
  };
}

Use this for an “Order details” page, and cache the result for a few minutes rather than calling Shiprocket every time a customer refreshes.

2. Let Shiprocket tell you

Shiprocket can send tracking updates to your server as they happen. Add a webhook URL in the panel's API settings along with a secret token, which Shiprocket sends in the x-api-key header. Check that header before trusting the request, then update the order using the AWB number:

routes/shiprocket-webhook.js
app.post('/api/shipping/webhook', express.json(), async (req, res) => {
  if (req.get('x-api-key') !== process.env.SHIPROCKET_WEBHOOK_TOKEN) {
    return res.sendStatus(401);
  }
  const { awb, current_status } = req.body;
  await Order.updateOne({ awb }, { $set: { shippingStatus: current_status } });
  res.sendStatus(200); // reply quickly; do slow work (emails) afterwards
});

Log a few real webhook bodies in test mode first and check the field names against what you store. They are the easiest part of the integration to get subtly wrong.

Cancel an order

lib/shipping.js
async function cancelShipment(shiprocketOrderId) {
  return shiprocket('/orders/cancel', {
    method: 'POST',
    body: { ids: [shiprocketOrderId] }, // Shiprocket's order id, not your order number
  });
}

Lessons from running it in production

  • Create the shipment only after the payment is confirmed. For prepaid orders, wait until the payment gateway confirms the money arrived (I explain how in verifying Razorpay payments), otherwise you end up shipping unpaid orders.
  • Make every step safe to repeat. Save shipment_idand the AWB number as soon as you get them, and skip a step if its result already exists. Retries and double clicks then can't create two shipments.
  • Validate the address at checkout. A 6-digit pincode and a 10-digit phone number catch most of the orders Shiprocket would reject.
  • Don't block the customer on Shiprocket.If creating the shipment fails, keep the order as paid, show it in the admin panel as “shipping not booked”, and retry. The customer has already paid; the parcel can be booked a few minutes later.
  • Store weights and sizes per product. Shipping rates depend on them, and a wrong default weight quietly costs money on every order.

I built this flow for an online fashion store: orders are booked with Shiprocket only after the payment is confirmed, and the store's admin panel handles the rest. If you need payments and shipping in your store, see what I build or get in touch.

Questions people also ask

Does Shiprocket have an official Node.js SDK?

Shiprocket documents a REST API rather than an official Node.js SDK. You call it with fetch or axios: log in to get a token, then send it as a Bearer token with each request.

How long is a Shiprocket API token valid?

A Shiprocket API token is valid for 240 hours (10 days). Cache it and log in again before it expires, or when a request returns 401.

What is the Shiprocket API base URL?

The base URL is https://apiv2.shiprocket.in/v1/external. For example, login is POST /auth/login and order creation is POST /orders/create/adhoc.

Why does Shiprocket say the pickup location is invalid?

The pickup_location field must exactly match the nickname of a pickup address saved in your Shiprocket account, including capital letters and spaces.

Why does assigning an AWB fail?

The most common reasons are a low Shiprocket wallet balance, a courier that does not serve the pincode, or a shipment that already has an AWB. Read the message in the response before retrying.

Read next