---
title: "Carrier reference"
space: "BWH Shipping"
url: "https://docs.bwh.tech/bwh-shipping/build/carrier-reference"
updated: "2026-09-21"
---

Every carrier is a Single DocType whose controller extends both `Document` and `ShippingProviderBase`:

```python
from frappe.model.document import Document

from bwh_shipping.base_class import ShippingProviderBase


class AcmeCourierShippingSettings(Document, ShippingProviderBase): ...
```

| Method | Required | Called when |
|---|---|---|
| `get_rates` | ✅ | Checkout asks for delivery options |
| `create_shipment` | ✅ | A Shipping Request is booked |
| `cancel_shipment` | ✅ | Staff click **Cancel With Carrier** |
| `get_tracking` | ✅ | Someone clicks **Sync Status** |
| `handle_webhook` | ✅ | The carrier posts to the webhook endpoint |
| `schedule_pickup` | | Staff click **Schedule Pickup** |
| `generate_manifest` | | Staff click **Generate Manifest** |
| `resume_booking` | | Staff click **Resume Booking** after a partial booking |
| `get_service_choices` | | Your code lists the carrier's services |

## Rules for every method

- **Weights are in kilograms, dimensions in centimetres, and money in major units** of the currency named alongside it. Convert to whatever the carrier wants with `bwh_shipping.units`.
- **Statuses are ladder statuses**, already mapped from the carrier's wording: `Draft`, `Ready To Ship`, `Pickup Scheduled`, `In Transit`, `Out For Delivery`, `Undelivered`, `RTO`, `Delivered`, `Cancelled`, `Lost`.
- **To report failure, throw** with `frappe.throw`. `get_rates` is the exception: return `[]` there.

---

## get_rates

```python
def get_rates(
	self,
	origin: dict,
	destination: dict,
	parcels: list[dict],
	cod: bool = False,
	declared_value: float = 0.0,
) -> list[dict]
```

Quotes every service that can carry the consignment. It's called once per carrier at checkout, and only when the carrier has an origin (its **Pickup Address**, unless the checkout passes one).

**Parameters**

| Name | Type | Description |
|---|---|---|
| `origin`, `destination` | `dict` | See Address below |
| `parcels` | `list[dict]` | See Parcel below. Passed **as the checkout gave them**, so normalise them first. |
| `cod` | `bool` | Whether the shopper chose cash on delivery |
| `declared_value` | `float` | The cart's value, in major units |

**Returns** a list of:

| Key | Description |
|---|---|
| `service_code` | The carrier's id for the service, as a string. It must match a Shipping Service's **Service Code**, and it's passed back unchanged at booking. |
| `amount` | The price, in major units |
| `currency` | The price's currency |
| `service_name`, `carrier`, `transit_days`, `cod_available` | For display |

**Return `[]`** when you can't quote, for example when the carrier doesn't serve the route. Checkout then prices the service from its Shipping Rule or Backup Charge. If you raise instead, the error is logged and checkout still renders.

---

## create_shipment

```python
def create_shipment(self, shipment: dict) -> dict
```

Books one consignment and buys its label. Called by `ShippingRequest.book()`.

**The `shipment` dict**

| Key | Description |
|---|---|
| `reference` | The Shipping Request name, for example `SHIP-26-00012` |
| `order_reference` | The document the shipment is for (`ref_docname`) |
| `service_code` | From the Shipping Service |
| `origin`, `destination` | Address dicts |
| `parcels` | Parcel dicts |
| `declared_value`, `currency` | Declared value, in major units |
| `cod`, `cod_amount` | Cash on delivery |
| `customer` | `{name, phone, email}` |

**Returns**

| Key | Required | Description |
|---|---|---|
| `awb` | ✅ | The tracking number. Webhooks find the shipment by it. |
| `order_ref`, `shipment_ref` | ✅ | Your handles for cancel, pickup and manifest. They're stored as they are. |
| `label_url` | | Opened by **Print Label** |
| `carrier` | | The actual courier's name |
| `cost_amount`, `cost_currency` | | What the carrier charged you |
| `status` | | The carrier's own status, stored as `provider_status` |
| `tracking_ref` | | A separate tracking handle, if the carrier has one |

After it returns, the request's status becomes **Ready To Ship**.

**Throw when** the carrier refuses the booking. If part of the booking already exists at the carrier, raise `PartialBookingError` instead (see Helpers).

---

## cancel_shipment

```python
def cancel_shipment(self, order_ref: str, shipment_ref: str | None = None, awb: str | None = None) -> dict
```

Cancels a booking. It's only offered while the request is **Draft**, **Ready To Ship** or **Pickup Scheduled**.

**Returns** `{"status", "message"}`. `status` is the carrier's own wording. The request becomes **Cancelled** either way, so **throw** if the carrier refuses.

---

## get_tracking

```python
def get_tracking(self, awb: str, shipment_ref: str | None = None, tracking_ref: str | None = None) -> dict
```

Reads the current status from the carrier. Called by `ShippingRequest.sync_status()`.

**Returns**

| Key | Description |
|---|---|
| `status` | A ladder status, or `None` to leave it unchanged (scans are still added) |
| `provider_status` | The carrier's own wording |
| `events` | Scans: `{timestamp, status, location, message}`, plus an optional `event_id` |

The status only changes if the new one is higher on the ladder. Scans are added whether or not the status moves. Repeats are skipped by `event_id`, or by timestamp, status and message.

---

## handle_webhook

```python
def handle_webhook(self, payload: bytes, headers: dict) -> dict
```

Verifies a delivery to the webhook endpoint and says what it means.

**Returns**

| Return | Meaning | HTTP |
|---|---|---|
| *(throws)* | Verification failed | 400 |
| `{}` | Genuine, but nothing to act on | 200 |
| `{"awb", "status", "event_id", "events", "provider_status"}` | Apply to the Shipping Request with this AWB | 200 |

See [Webhooks and security](/bwh-shipping/build/webhooks-and-security).

---

## Optional methods

Implement only what the carrier really supports. `supports(capability)` returns `True` when your class overrides the method. Desk actions for a capability you don't support fail with a clear message.

| Method | Capability | Returns |
|---|---|---|
| `schedule_pickup(shipment_ref, pickup_date=None)` | `pickup` | `{pickup_ref, scheduled_date, message}`. The status moves to **Pickup Scheduled**. |
| `generate_manifest(shipment_refs: list[str])` | `manifest` | `{manifest_url}` |
| `resume_booking(order_ref, shipment_ref, service_code=None)` | `resume` | The same shape as `create_shipment` |
| `get_service_choices()` | `service_choices` | `{provider, accounts: [{carrier, description, services: [{service_code, service_name}]}]}` |

`get_provider_name()` is optional too. It returns a display name, and defaults to the class name.

---

## Address

```python
{
	"contact_name": "Ada Lovelace",
	"company_name": "Lovelace Labs",  # the Address title
	"line1": "1 MG Road",
	"line2": None,
	"city": "Bengaluru",
	"state": "Karnataka",
	"pincode": "560001",
	"country": "India",  # the Frappe Country name, not an ISO code
	"phone": "+919800000000",
	"email": "ada@example.com",
}
```

Convert `country` to an ISO code yourself if the carrier needs one. Build this dict from an ERPNext Address with `get_address_payload(address_name)` from `bwh_shipping.bwh_shipping.utils`.

## Parcel

```python
{"length": 30, "width": 20, "height": 10, "weight": 1.2, "count": 1}  # cm, cm, cm, kg
```

## Helpers

**`bwh_shipping.units`**

| Helper | Example |
|---|---|
| `normalise_parcels(parcels)` | Rounds and validates a list of parcels. Throws on an empty list. |
| `billable_weight(parcels, divisor=5000)` | A 50 × 40 × 30 cm, 2 kg parcel → `12.0` |
| `volumetric_weight(parcel, divisor=5000)` | The same parcel → `12.0` |
| `total_weight(parcels)` | The actual weight of every parcel × count |
| `enclosing_dimensions(parcels)` | One box that holds them all: lengths added, widest width and height |
| `to_kg(value, unit)`, `to_cm(value, unit)` | `to_kg(500, "g")` → `0.5` |
| `to_system_datetime(value)` | Converts a timestamp with a timezone offset for storage |

**`bwh_shipping.status`**: `SHIPMENT_STATUSES`, `can_advance(current, new)`, `is_terminal(status)`.

**`bwh_shipping.exceptions.PartialBookingError(message, booking=None)`**: raise it when a staged booking fails part-way, with `booking={"order_ref": ..., "shipment_ref": ...}`. BWH Shipping saves those references, and the **Book Shipment** button becomes **Resume Booking**, which calls your `resume_booking`.
