Shipments
A shipment is the record of a parcel handed off to a courier. It carries the courier code + service code, the price the merchant was quoted, an origin + destination, and the items packed into it. Fulkruma wires this to Biteship (its first courier aggregator) and emits fulkruma.shipment.* webhooks as the carrier sends status updates. For HTTP shapes, see API → Shipments.
Namespace
fulkruma.shipments # ShipmentsResources
Three methods. There's no update and no cancel — once a label is booked with a courier, mutation goes through the courier's portal (or a support ticket). The closest thing to "cancel" is the carrier's own cancellation event, surfaced as a shipment.updated webhook.
Methods
create
fulkruma.shipments.create(body: dict, *, on_behalf_of: str | None = None) -> dict
Books a shipment with the chosen courier. The SDK auto-mints an idempotency key — replay-safe, which matters because creating one twice means paying the courier twice.
result = fulkruma.shipments.create({
"customerId": "cus_01HX...",
"courierCode": "jne",
"courierServiceCode": "reg",
"courierType": "standard",
"price": 18000,
"origin": {"warehouseId": "wh_01HX..."},
"destination": {
"contactName": "Alice Tan",
"contactPhone": "+62812xxxxxxxx",
"address": "Jl. Diponegoro No. 5",
"postalCode": "10310",
"areaId": "IDN-JKT-MTH",
},
"items": [
{"variantId": "var_01HX...", "quantity": 2, "weight": 0.3},
],
})
print(result["shipment"]["id"], result["shipment"].get("trackingNumber"))
origin and destination are intentionally loosely typed — the backend validates the shape per courier (Biteship expects a different envelope than a future direct integration). For warehouse-origin shipments, pass {"warehouseId": "..."} and let the server resolve the address.
The
priceyou pass is what you charge the customer. It must match (or exceed) the quote fromshipping.ratesfor the samecourierCode/courierServiceCode/destination. Fulkruma validates this server-side; if you pass below-quote, you'll get400 validation_error.
get
fulkruma.shipments.get(shipment_id: str, *, on_behalf_of: str | None = None) -> dict
Fetches one shipment by ship_* ID. Includes the latest tracking events embedded in the response.
result = fulkruma.shipments.get("ship_01HX...")
shipment = result["shipment"]
print(shipment["status"]) # "in_transit"
print(len(shipment["events"])) # count of tracking events
list
fulkruma.shipments.list(
*,
status: str | None = None,
on_behalf_of: str | None = None,
) -> dict
Lists shipments in the workspace. Pass status to filter ("pending", "confirmed", "picked_up", "in_transit", "delivered", "returned", "cancelled").
result = fulkruma.shipments.list(status="in_transit")
print(f"{len(result['shipments'])} parcels currently moving")
Status filter is exact-match. There's no date filter — filter the returned rows on createdAt if you need a time-bounded scan.
Types
Each method returns a plain dict. Key shape:
{
"shipment": {
"id": "ship_...",
"accountId": "acc_...",
"customerId": "cus_..." | None,
"courierCode": "jne",
"courierServiceCode": "reg",
"courierType": "standard",
"status": "pending" | "confirmed" | "picked_up" | "in_transit" |
"delivered" | "returned" | "cancelled",
"trackingNumber": "..." | None,
"price": int,
"insurance": int | None,
"insured": bool,
"origin": {...},
"destination": {...},
"items": [{...}, ...],
"events": [{...}, ...],
"externalSource": "..." | None,
"externalRef": "..." | None,
"createdAt": "...",
"updatedAt": "..."
}
}
See API → Shipments for the full courier-code vocabulary and per-courier origin/destination schemas.
Common patterns
Quote-then-book. The robust pattern: quote first via shipping.rates, let the customer pick, then book:
quote = fulkruma.shipping.rates({
"destination": {"areaId": "IDN-JKT-MTH", "postalCode": "10310"},
"items": [{"variantId": "var_01HX...", "quantity": 1, "weight": 0.5}],
})
picked = quote["rates"][0] # user picked one in your UI
shipment = fulkruma.shipments.create({
"customerId": "cus_01HX...",
"courierCode": picked["courierCode"],
"courierServiceCode": picked["courierServiceCode"],
"courierType": picked["courierType"],
"price": picked["price"],
"origin": {"warehouseId": "wh_01HX..."},
"destination": {
"areaId": "IDN-JKT-MTH", "postalCode": "10310",
"contactName": "Alice", "contactPhone": "+62812xxxxxxxx",
"address": "Jl. Diponegoro 5",
},
"items": [{"variantId": "var_01HX...", "quantity": 1, "weight": 0.5}],
})
Track via webhook. Don't poll shipments.get in a loop — subscribe to fulkruma.shipment.updated:
fulkruma.webhooks.create_endpoint({
"url": "https://your-app.example.com/webhooks/fulkruma",
"events": ["fulkruma.shipment.updated", "fulkruma.shipment.delivered"],
})
Then verify + handle in your endpoint. See Webhooks for verify_webhook.
Idempotent re-booking after partial failure. If the courier API timed out and you don't know whether the shipment was created, retry with the same externalRef:
fulkruma.shipments.create({
# ...
"externalRef": "order-2026-118",
"externalSource": "storlaunch",
})
The auto-generated idempotency key covers in-process retries; for cross-process replay use a stable externalRef and check shipments.list() first.
Errors
err.status |
err.code |
Cause |
|---|---|---|
400 |
validation_error |
Missing courier fields, price below quote, bad destination shape. |
404 |
not_found |
Customer, warehouse, or variant ID missing. |
409 |
insufficient_stock |
Stock at the origin warehouse is exhausted. |
502 |
courier_error |
Upstream Biteship rejected the booking. |
403 |
insufficient_scope |
Key lacks fulkruma:shipment:write. |
courier_error is the retry candidate — transient upstream blips are the most common failure mode.
Next
- Shipping — rate quotes + courier listing + origin config.
- Stock — the inventory shipments draw from.
- Webhooks — how to receive status events.
- API → Shipments — HTTP-level reference.