Shipping
The shipping namespace is everything you need before booking a shipment: list available couriers, configure the workspace's default origin, and quote rates. It's intentionally separate from shipments — the latter mutates state, this one is mostly read-only metadata. For HTTP fields, see API: Shipping.
Namespace
fulkruma.shipping — every method:
fulkruma.shipping.couriers()
fulkruma.shipping.origin()
fulkruma.shipping.setOrigin(input)
fulkruma.shipping.rates(input)
Four methods. Three reads, one config mutation (setOrigin).
Methods
shipping.couriers
Signature. fulkruma.shipping.couriers(): Promise<unknown[]>
Returns the list of couriers available in the workspace, with their service codes (e.g. JNE reg, JNE yes, J&T ez). The shape is intentionally loose — it passes through whatever Biteship (or the future direct integrations) returns — so wrap it in your own typed adapter if you depend on specific fields.
const couriers = await fulkruma.shipping.couriers();
for (const c of couriers as Array<{ courierCode: string; courierName: string; services: any[] }>) {
console.log(c.courierCode, c.courierName, c.services.length, 'services');
}
The list is keyed off the merchant's enabled couriers in the portal — if a courier isn't there, enable it in the dashboard first.
shipping.origin and shipping.setOrigin
Signature.
fulkruma.shipping.origin(): Promise<unknown>fulkruma.shipping.setOrigin(input): Promise<unknown>
The workspace's default origin — pickup location, sender contact, area code — used when a shipment doesn't explicitly include an origin. Read it via origin(), write it via setOrigin().
const current = await fulkruma.shipping.origin();
await fulkruma.shipping.setOrigin({
contactName: 'Warehouse Manager',
contactPhone: '+62211234567',
address: 'Jl. Sudirman No. 1',
postalCode: '12190',
areaId: 'IDN-JKT-CTL',
lat: -6.2088,
lng: 106.8456,
});
The default origin is what shipments.create({ origin: { warehouseId } }) resolves against when a warehouse doesn't have its own address on file. If every warehouse you have is fully populated, you can ignore the workspace-level origin.
shipping.rates
Signature. fulkruma.shipping.rates(input): Promise<unknown>
The most-used method on this namespace. Posts a destination + line items + (optional) insurance flag, returns a list of { courierCode, courierServiceCode, courierType, price, etd, ... } — one per (courier, service) combination. Quotes are live (round-tripped through Biteship) and valid for a short window.
const quote = await fulkruma.shipping.rates({
destination: {
areaId: 'IDN-JKT-MTH',
postalCode: '10310',
},
items: [
{ variantId: 'var_01HX...', quantity: 2, weight: 0.5 },
],
insurance: true,
});
// shape: { rates: [{ courierCode, courierServiceCode, price, etd, ... }, ...] }
for (const r of (quote as { rates: any[] }).rates) {
console.log(`${r.courierCode}/${r.courierServiceCode}: Rp${r.price} (ETD ${r.etd})`);
}
The same courierCode/courierServiceCode/price triple is what you pass to shipments.create once the customer picks one.
Types
The shipping namespace returns intentionally-untyped values (unknown) because the upstream courier API (Biteship today) controls the shape. For stability, the only fields we promise across courier providers are:
rates[].courierCode— matches whatshipments.createexpectsrates[].courierServiceCoderates[].courierTyperates[].price(integer rupiah)rates[].etd(estimated delivery, vendor-specific format)
Everything else is passthrough. See API: Shipping for the full per-provider field map.
Common patterns
Quote, render, book
The canonical pattern — user picks from a list of quotes, then you book:
async function checkoutShipping(customerAddress: { areaId: string; postalCode: string }, lineItems: Array<{ variantId: string; quantity: number; weight: number }>) {
const quote = await fulkruma.shipping.rates({
destination: customerAddress,
items: lineItems,
});
const rates = (quote as { rates: any[] }).rates;
// … render rates as radio buttons in your UI, let user pick …
return rates;
}
The book half lives in shipments.create.
Cache courier list per session
shipping.couriers() is workspace-scoped config that rarely changes — safe to memoize for the lifetime of your server process:
let couriersCache: unknown[] | null = null;
async function getCouriers() {
if (!couriersCache) {
couriersCache = await fulkruma.shipping.couriers();
}
return couriersCache;
}
Invalidate on a fulkruma.integration.biteship.updated webhook if you need to be fully reactive; in practice, restart-on-deploy is fine.
Initial setup: configure origin once
For a fresh workspace, you'll typically call setOrigin exactly once at onboarding time:
await fulkruma.shipping.setOrigin({
contactName: 'Fulfillment',
contactPhone: '+62211234567',
address: 'Jl. Sudirman No. 1, Jakarta',
postalCode: '12190',
areaId: 'IDN-JKT-CTL',
});
After that, individual shipments inherit it via origin: { warehouseId } resolution.
Errors
| Code | Status | Cause |
|---|---|---|
validation_error |
400 | Bad areaId, missing weight on items. |
not_found |
404 | Variant in items[] doesn't exist. |
courier_error |
502 | Upstream courier (Biteship) rejected the rate request. |
forbidden |
403 | Key lacks fulkruma:shipping:read / :write. |
courier_error is the retry candidate — transient blips at the courier aggregator are the most common failure mode here.
Next
- Shipments — the other side of the quote-then-book flow.
- Addresses — destinations to quote against.
- API: Shipping — HTTP reference.