---
title: "Use it in your app"
space: "BWH Shipping"
url: "https://docs.bwh.tech/bwh-shipping/get-started/use-it-in-your-app"
updated: "2026-09-21"
---

This page is for the app that *uses* BWH Shipping, for example your storefront. It covers the four things you wire up: services, checkout quotes, shipments, and status changes.

:::note
Every BWH Shipping DocType is **System Manager** only. Code that runs for shoppers or other staff roles inserts and saves with `ignore_permissions=True`, or you grant roles in **Role Permissions Manager**.
:::

## 1. Set up the Shipping Services

A **Shipping Service** is a delivery option a shopper can pick, such as "Express, 1–2 days". Create one per option, in **Shipping Service → New**:

| Field | Purpose |
|---|---|
| Title | What the shopper sees. It is also the service's name, so it must be unique. |
| Provider | The Shipping Provider Profile that carries it, for example `Shiprocket` |
| Service Code | The carrier's id for this service. Each [carrier page](/bwh-shipping/carriers/shiprocket) says what it means. |
| Markup %, Handling Fee | Added on top of the live carrier rate |
| Shipping Rule | Optional. When one of its bands covers the cart, the band sets the price instead of the live rate. |
| Backup Charge | Used when there is no band and no live rate, so the option still shows |

An enabled service needs a Service Code and an enabled provider.

## 2. Quote at checkout

`quote_services` prices every enabled Shipping Service for a cart. It isn't a public API, so wrap it in a whitelisted method of your own:

```python
# your_app/api.py
import frappe

from bwh_shipping.bwh_shipping.pricing import quote_services
from bwh_shipping.bwh_shipping.utils import get_address_payload
from bwh_shipping.units import normalise_parcels


@frappe.whitelist(methods=["POST"])
def get_delivery_options(address: str) -> list[dict]:
	cart = get_current_cart()  # your code: the shopper's cart
	return quote_services(
		origin=None,  # each carrier ships from its own Pickup Address
		destination=get_address_payload(address),
		parcels=normalise_parcels([{"length": 30, "width": 20, "height": 10, "weight": cart.weight}]),
		cart={
			"declared_value": cart.grand_total,
			"weight": cart.weight,
			"base_net_total": cart.base_net_total,
			"conversion_rate": cart.conversion_rate,
			"currency": cart.currency,
		},
	)
```

Each row it returns looks like this:

```python
{
	"title": "Express",
	"description": "1–2 days",
	"provider": "Shiprocket",
	"service_code": "10",
	"amount": 99.0,
	"is_free": False,
	"is_live_rate": True,
}
```

- **Carrier errors never break checkout.** A carrier that fails is logged as `<provider> checkout rates failed` in the Error Log, and its services fall back to their Shipping Rule or Backup Charge.
- **A service with no price is dropped**, rather than shown as free.
- **Store the chosen row on the server** when the shopper picks it. When the order is placed, bill `get_charge_amount(title, cart, quoted_amount=stored_amount)` from the same module. Never trust an amount the browser sends back. Without a `quoted_amount`, a live-rate service is billed its Backup Charge, or refused if it has none.

:::caution[Carriers need a Pickup Address]
With `origin=None`, a carrier is only asked for rates if its settings have a **Pickup Address**. Without one, its services fall back to their Shipping Rule or Backup Charge.
:::

## 3. Create and book a shipment

**From the desk:** open a submitted **Delivery Note** and click **Create → Shipment**. That creates a draft Shipping Request, which staff book with **Book Shipment**.

The Delivery Note route picks:

- **the carrier and service** from the Sales Order's custom fields `custom_shipping_provider`, `custom_shipping_service_code` and `custom_delivery_option` (the Shipping Service title), when you've added them. Otherwise it uses the most recently created enabled provider, with no service, so staff pick one before **Book Shipment**.
- **the origin** from the carrier's Pickup Address, or the company address.
- **one 30 × 20 × 10 cm parcel**, weighted from the items' total weight. Items with no weight count as 0.5 kg per unit.

:::caution[Weights are read as kilograms]
The Delivery Note route adds up item weights as kilograms, whatever the items' **Weight UOM** is. If your items are weighed in grams or pounds, correct the parcel before booking.
:::

**From code:**

```python
shipping_request = frappe.get_doc(
	{
		"doctype": "Shipping Request",
		"provider": "Shiprocket",
		"service_code": "10",
		"ref_doctype": "Sales Order",
		"ref_docname": sales_order.name,
		"origin_address": warehouse_address,
		"destination_address": sales_order.shipping_address_name,
		"customer_name": sales_order.customer_name,
		"declared_value": sales_order.grand_total,
		"currency": sales_order.currency,
		"parcels": [{"length": 30, "width": 20, "height": 10, "weight": 1.2, "count": 1}],
	}
).insert(ignore_permissions=True)

shipping_request.book()
```

`book()` buys the label and sets the status to **Ready To Ship**. The request then holds the `awb`, `label_url`, `carrier` and `cost_amount`.

## 4. React to status changes

Statuses arrive from carrier webhooks, or when someone clicks **Sync Status** (`shipping_request.sync_status()`). BWH Shipping doesn't poll carriers on a schedule. If you need that, for a carrier without webhooks, run your own scheduled job that calls `sync_status()` on shipments that are still in flight.

To act on a change, for example to email the customer when a parcel is out for delivery, hook `on_update`:

```python
# your_app/hooks.py
doc_events = {
	"Shipping Request": {"on_update": "your_app.shipping.on_shipping_request_update"},
}

# your_app/shipping.py
import frappe


def on_shipping_request_update(doc, method=None):
	if doc.has_value_changed("status") and doc.status == "Out For Delivery":
		frappe.enqueue(send_out_for_delivery_email, shipping_request=doc.name, enqueue_after_commit=True)
```

Check `has_value_changed`, because the hook fires on every save. Enqueue slow work. During a webhook the hook runs inside the carrier's request, as Administrator.

## Next steps

- [Webhooks and security](/bwh-shipping/build/webhooks-and-security): set up tracking updates
- [Shiprocket](/bwh-shipping/carriers/shiprocket) and [AfterShip](/bwh-shipping/carriers/aftership): connect a carrier
