@molecule/api-shipping
Core interface · shipping · API (Node) · v1.0.3 · Apache-2.0
Carrier-agnostic shipping labels + tracking.
npm install @molecule/api-shippingHow it works
@molecule/api-shipping is the shipping core interface on the API (Node) side: the API your app calls, with no vendor inside.
Choose the implementation by bonding one of its 2 providers: @molecule/api-shipping-easypost, @molecule/api-shipping-shippo.
import { setProvider, createShipment, createLabel, trackPackage } from '@molecule/api-shipping'
import { provider } from '@molecule/api-shipping-easypost' // or '@molecule/api-shipping-shippo'
// Bond a provider at startup
setProvider(provider)
// 1. Create the shipment: this quotes rates AND returns the shipment id a label
// purchase needs. Keep the returned ShippingRate objects intact (they carry rateId).
const { shipmentId, rates } = await createShipment({
from: { street1: '...', city: '...', postalCode: '...', country: 'US' },
to: { street1: '...', city: '...', postalCode: '...', country: 'US' },
parcels: [{ length: 10, width: 6, height: 4, weight: 2 }],
})
// 2. Purchase a label for a QUOTED rate using the shipment id from step 1. The rate
// MUST be one returned by createShipment — bonds reject a hand-built rate without rateId.
const label = await createLabel(shipmentId, rates[0])
// 3. Track using values from the purchased label.
const status = await trackPackage(label.carrier, label.trackingNumber)
// (getRates(shipment) is a display-only convenience returning just the rates — no
// shipmentId — for flows that quote without purchasing.)Providers (2): @molecule/api-shipping-easypost, @molecule/api-shipping-shippo
Works with: @molecule/api-bond, @molecule/api-i18n
Reference
Shipping core interface for molecule.dev.
Defines the standard interface for shipping/carrier providers (EasyPost, Shippo, etc.). Used for rate quotes, label generation, label voiding, and package tracking.
Auto-generated, AI-first package reference for the molecule.dev ecosystem. It is written to be read by coding agents as much as by people, and is generated from this package's source — edit
src/index.tsJSDoc, not this file.
Quick Start
import { setProvider, createShipment, createLabel, trackPackage } from '@molecule/api-shipping'
import { provider } from '@molecule/api-shipping-easypost' // or '@molecule/api-shipping-shippo'
// Bond a provider at startup
setProvider(provider)
// 1. Create the shipment: this quotes rates AND returns the shipment id a label
// purchase needs. Keep the returned ShippingRate objects intact (they carry rateId).
const { shipmentId, rates } = await createShipment({
from: { street1: '...', city: '...', postalCode: '...', country: 'US' },
to: { street1: '...', city: '...', postalCode: '...', country: 'US' },
parcels: [{ length: 10, width: 6, height: 4, weight: 2 }],
})
// 2. Purchase a label for a QUOTED rate using the shipment id from step 1. The rate
// MUST be one returned by createShipment — bonds reject a hand-built rate without rateId.
const label = await createLabel(shipmentId, rates[0])
// 3. Track using values from the purchased label.
const status = await trackPackage(label.carrier, label.trackingNumber)
// (getRates(shipment) is a display-only convenience returning just the rates — no
// shipmentId — for flows that quote without purchasing.)
Type
core
Installation
npm install @molecule/api-shipping @molecule/api-bond @molecule/api-i18n
API
Interfaces
ShippingAddress
A postal address used as the origin or destination of a shipment.
interface ShippingAddress {
/** Recipient or sender name. */
name?: string
/** Company name, if applicable. */
company?: string
/** Primary street address line. */
street1: string
/** Secondary street address line (apartment, suite, etc.). */
street2?: string
/** City or locality. */
city: string
/** State, province, or region code. */
state?: string
/** Postal or ZIP code. */
postalCode: string
/** ISO 3166-1 alpha-2 country code. */
country: string
/** Contact phone number in E.164 format. */
phone?: string
/** Contact email address. */
email?: string
}
Parcel
Physical parcel dimensions and weight.
interface Parcel {
/** Parcel length. */
length: number
/** Parcel width. */
width: number
/** Parcel height. */
height: number
/** Parcel weight. */
weight: number
/** Linear unit for length, width, and height. Defaults to `'in'` when omitted (all bonds agree). */
distanceUnit?: 'in' | 'cm'
/** Mass unit for weight. Defaults to `'lb'` when omitted (all bonds agree). */
massUnit?: 'lb' | 'oz' | 'kg' | 'g'
}
MonetaryAmount
A monetary amount paired with its currency.
interface MonetaryAmount {
/** Decimal amount represented as a string to avoid float precision loss. */
amount: string
/** ISO 4217 currency code. */
currency: string
}
DeliveryEstimate
Estimated delivery window, if known.
interface DeliveryEstimate {
/** Earliest expected delivery date or datetime. */
earliest?: Date
/** Latest expected delivery date or datetime. */
latest?: Date
/** Number of business days estimated for delivery. */
businessDays?: number
}
Shipment
Description of a shipment used to request rates or create a label.
interface Shipment {
/** Origin address. */
from: ShippingAddress
/** Destination address. */
to: ShippingAddress
/** One or more parcels included in this shipment. */
parcels: Parcel[]
/** Optional service-level filter (e.g., a carrier-specific service code). */
serviceLevel?: string
/** Optional declared value for insurance/customs. */
declaredValue?: MonetaryAmount
}
ShippingRate
A rate quote returned by a carrier for a given shipment.
interface ShippingRate {
/** Carrier identifier (e.g., `usps`, `ups`, `fedex`). */
carrier: string
/** Carrier-specific service code or name. */
service: string
/** Quoted price for the rate. */
amount: MonetaryAmount
/** Estimated delivery window for this rate, if available. */
deliveryEstimate?: DeliveryEstimate
/** Provider-assigned identifier used to purchase this rate. */
rateId?: string
}
ShipmentQuote
The result of creating a shipment: the provider-assigned shipment handle plus the rate quotes returned for it.
A shipment must exist before a label can be purchased — every provider
assigns the shipment an id when it is created, and
ShippingProvider.createLabel needs that id.
ShippingProvider.createShipment returns both the id and the rates in
one round-trip, so the caller never has to reconstruct or re-create the
shipment just to obtain the id needed to buy a label.
interface ShipmentQuote {
/**
* Provider-assigned shipment identifier. Pass this to
* `ShippingProvider.createLabel` to purchase a label for one of `rates`.
*/
shipmentId: string
/** Rate quotes returned by the carrier(s) for this shipment. */
rates: ShippingRate[]
}
ShippingLabel
A purchased shipping label.
interface ShippingLabel {
/** Provider-assigned label or shipment identifier. */
id: string
/** Carrier tracking number assigned to the shipment. */
trackingNumber: string
/** URL where the printable label can be downloaded. */
labelUrl: string
/** Carrier identifier. */
carrier: string
/** Carrier-specific service code or name. */
service: string
/** Total amount paid for the label. */
amount?: MonetaryAmount
}
TrackingEvent
A single event in a package's tracking history.
interface TrackingEvent {
/** When the event was recorded by the carrier. */
timestamp: Date
/** Normalized status at the time of the event. */
status: TrackingStatusCode
/** Human-readable description of the event from the carrier. */
description: string
/** Free-form location string from the carrier, if provided. */
location?: string
}
TrackingStatus
Aggregated tracking status for a single tracking number.
interface TrackingStatus {
/** Carrier identifier. */
carrier: string
/** Tracking number being reported on. */
trackingNumber: string
/** Current normalized status. */
status: TrackingStatusCode
/** Ordered list of tracking events from oldest to newest. */
events: TrackingEvent[]
/** Estimated delivery, if reported by the carrier. */
estimatedDelivery?: DeliveryEstimate
}
ShippingProvider
Shipping provider interface.
All shipping providers must implement this interface to provide rate quoting, label purchasing, label voiding, tracking, and supported-carrier discovery capabilities.
interface ShippingProvider {
/**
* Lists carriers supported by this provider.
*
* @returns Array of carrier identifiers.
*/
listSupportedCarriers(): Promise<string[]>
/**
* Creates a shipment and returns its provider-assigned id together with the
* rate quotes for it. This is the primary quoting path: the returned
* `shipmentId` is the handle `createLabel` needs to purchase a label, so
* callers who intend to buy a label should use this (not `getRates`) and
* persist the id alongside the chosen `ShippingRate`.
*
* Every provider assigns a shipment an id when it is created (EasyPost's
* `POST /shipments`, Shippo's `POST /shipments/`), so both bonds return the id
* and rates natively in a single round-trip — no bond-specific quote helper.
*
* @param shipment - The shipment to create and rate.
* @returns The created shipment's id and its available rates.
*/
createShipment(shipment: Shipment): Promise<ShipmentQuote>
/**
* Requests rate quotes for a shipment, discarding the shipment id.
*
* Convenience over `createShipment` for display-only flows that quote
* rates without (yet) purchasing. To buy a label you also need the
* `shipmentId` — call `createShipment` and keep both.
*
* @param shipment - The shipment to rate.
* @returns Array of available rates.
*/
getRates(shipment: Shipment): Promise<ShippingRate[]>
/**
* Purchases a shipping label for the given rate.
*
* @param shipmentId - Provider-assigned shipment identifier from a prior
* `createShipment` call.
* @param rate - The rate selected for purchase (one of the
* `ShipmentQuote.rates` returned alongside `shipmentId`).
* @returns The purchased label.
*/
createLabel(shipmentId: string, rate: ShippingRate): Promise<ShippingLabel>
/**
* Voids a previously purchased label, if permitted by the carrier.
*
* @param labelId - Provider-assigned label identifier to void.
*/
voidLabel(labelId: string): Promise<void>
/**
* Retrieves the current tracking status for a package.
*
* @param carrier - Carrier identifier.
* @param trackingNumber - Carrier-assigned tracking number.
* @returns The aggregated tracking status.
*/
trackPackage(carrier: string, trackingNumber: string): Promise<TrackingStatus>
}
Types
TrackingStatusCode
Possible high-level tracking statuses, normalized across carriers.
type TrackingStatusCode =
| 'pre_transit'
| 'in_transit'
| 'out_for_delivery'
| 'delivered'
| 'available_for_pickup'
| 'return_to_sender'
| 'failure'
| 'unknown'
Functions
setProvider(provider)
Registers a shipping provider as the active singleton. Called by bond packages during application startup.
function setProvider(provider: ShippingProvider): void
provider— The shipping provider implementation to bond.
getProvider()
Retrieves the bonded shipping provider, throwing if none is configured.
function getProvider(): ShippingProvider
Returns: The bonded shipping provider.
hasProvider()
Checks whether a shipping provider is currently bonded.
function hasProvider(): boolean
Returns: true if a shipping provider is bonded.
listSupportedCarriers()
Lists carriers supported by the bonded provider.
function listSupportedCarriers(): Promise<string[]>
Returns: Array of carrier identifiers.
createShipment(shipment)
Creates a shipment using the bonded provider, returning its provider-assigned
id together with the rate quotes for it. Use this (not getRates) when
you intend to purchase a label — createLabel needs the returned
shipmentId.
function createShipment(shipment: Shipment): Promise<ShipmentQuote>
shipment— The shipment to create and rate.
Returns: The created shipment's id and its available rates.
getRates(shipment)
Requests rate quotes for a shipment using the bonded provider, discarding the
shipment id. Convenience over createShipment for display-only flows;
to buy a label use createShipment and keep the shipmentId too.
function getRates(shipment: Shipment): Promise<ShippingRate[]>
shipment— The shipment to rate.
Returns: Array of available rates.
createLabel(shipmentId, rate)
Purchases a shipping label for the given rate using the bonded provider.
function createLabel(shipmentId: string, rate: ShippingRate): Promise<ShippingLabel>
shipmentId— Provider-assigned shipment identifier from a prior rate quote.rate— The rate selected for purchase.
Returns: The purchased label.
voidLabel(labelId)
Voids a previously purchased label using the bonded provider.
function voidLabel(labelId: string): Promise<void>
labelId— Provider-assigned label identifier to void.
Returns: A promise that resolves when the label has been voided.
trackPackage(carrier, trackingNumber)
Retrieves the current tracking status for a package using the bonded provider.
function trackPackage(carrier: string, trackingNumber: string): Promise<TrackingStatus>
carrier— Carrier identifier.trackingNumber— Carrier-assigned tracking number.
Returns: The aggregated tracking status.
Available Providers
| Provider | Package |
|---|---|
| EasyPost | @molecule/api-shipping-easypost |
| Shippo | @molecule/api-shipping-shippo |
Injection Notes
Requirements
Peer dependencies:
@molecule/api-bond^1.0.1@molecule/api-i18n^1.0.1
Runtime Dependencies
-
@molecule/api-bond -
@molecule/api-i18n -
createShipmentreturns theshipmentIdthatcreateLabelneeds. The shipment id is provider-assigned when the shipment is created, and every bond returns it alongside the rates in one round-trip ({ shipmentId, rates }) — no bond-specific quote helper. Persist the id with the chosen rate between the quote step and the purchase step. -
createLabelconsumes the EXACT rate object returned bycreateShipment— itsrateIdis the provider's purchase handle and every bond rejects a rate without it. Persist the chosenShippingRate(not a reconstruction of carrier/service/amount), not just its fields. -
getRates(shipment)is a display-only convenience returning just the rates (noshipmentId). Do not use it as the first step of a purchase flow — you would have no id to pass tocreateLabel; usecreateShipmentinstead. -
Label purchases cost real money outside the provider's TEST mode — use test API keys in development, and re-quote before purchase (rate quotes expire).
-
Addresses and parcels are user input: validate server-side (
countryis ISO 3166-1 alpha-2; setdistanceUnit/massUnitexplicitly rather than assuming defaults). -
MonetaryAmount.amountis a STRING to avoid float loss — never do arithmetic on it directly; display it or convert via a decimal-safe path.
E2E Tests
Integration checklist — drive the real UI (live preview, no mocks), adapt each item to this app's actual screens/flows, and check every box off one by one. A box you can't check is an integration bug to fix — not a skip. Run the whole flow against the provider's TEST mode (a test API key — e.g. EasyPost/Shippo test keys) so rates and labels are free test artifacts that carry test tracking numbers; never mock the carrier, and never flip to a production key just to "make it real" (a real label purchase costs money):
- Requesting RATES for a real parcel (from + to address, weight, dimensions) renders MULTIPLE carrier/service options in the UI, each with a price AND an ETA — never an empty list, a spinner that never resolves, or a null/$0 amount. Remember amount.amount is a STRING (display it, don't NaN it).
- The rates REFLECT the input: re-quote with a heavier or farther-away parcel and the prices go UP (compare decimal-safe, not as floats) — proving live carrier quotes, not a hardcoded/fixture list.
- Buying a LABEL for a rate the user PICKED from that quote returns a real artifact (a PDF/PNG/ZPL) plus a trackingNumber, both shown in the UI. Pass back the exact ShippingRate object from getRates — its rateId is the purchase handle, so a hand-rebuilt rate is rejected.
- The label artifact is FETCHED and stored on the app's own storage/ uploads and the UI links to that copy — NOT the raw labelUrl, which is an expiring vendor URL that 404s once it lapses.
- TRACKING a purchased shipment (trackPackage with the label's carrier + trackingNumber) returns a real status plus an ordered event history in the UI, and re-tracking as the parcel moves advances the status (pre_transit → in_transit → delivered) instead of a frozen placeholder. If the app wires an inbound carrier tracking webhook, a delivered callback advances the STORED status AND a forged/unsigned callback is rejected.
- A bad/undeliverable address or a provider error surfaces a graceful, readable message in the UI (not a raw stack trace, not a silent empty rate list) — user-supplied addresses/parcels are validated server-side before they reach the carrier.
- SECURITY — the provider API key stays server-side only (this package is server-only and never ships to the client bundle), and a user can only rate, label, void, or track their OWN shipments: guessing another user's label id or tracking number must NOT return that label artifact or tracking status.