← All @molecule/* packages · App templates

@molecule/api-shipping

Core interface · shipping · API (Node) · v1.0.1 · Apache-2.0

Carrier-agnostic shipping labels + tracking.

npm install @molecule/api-shipping

npm · Source on GitHub

How 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

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.ts JSDoc, not this file.

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.

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

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
}

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
}

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'
}

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
}

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 {@link ShippingProvider.createLabel} needs that id. {@link 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
   * {@link ShippingProvider.createLabel} to purchase a label for one of `rates`.
   */
  shipmentId: string

  /** Rate quotes returned by the carrier(s) for this shipment. */
  rates: ShippingRate[]
}

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
}

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
}

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 {@link createLabel} needs to purchase a label, so
   * callers who intend to buy a label should use this (not {@link getRates}) and
   * persist the id alongside the chosen {@link 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 {@link createShipment} for display-only flows that quote
   * rates without (yet) purchasing. To buy a label you also need the
   * `shipmentId` — call {@link 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
   *   {@link createShipment} call.
   * @param rate - The rate selected for purchase (one of the
   *   {@link 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>
}

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
}

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
}

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

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.

createShipment(shipment)

Creates a shipment using the bonded provider, returning its provider-assigned id together with the rate quotes for it. Use this (not {@link getRates}) when you intend to purchase a label — {@link 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.

getProvider()

Retrieves the bonded shipping provider, throwing if none is configured.

function getProvider(): ShippingProvider

Returns: The bonded shipping provider.

getRates(shipment)

Requests rate quotes for a shipment using the bonded provider, discarding the shipment id. Convenience over {@link createShipment} for display-only flows; to buy a label use {@link createShipment} and keep the shipmentId too.

function getRates(shipment: Shipment): Promise<ShippingRate[]>
  • shipment — The shipment to rate.

Returns: Array of available rates.

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.

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.

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.

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.

Available Providers

ProviderPackage
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

  • createShipment returns the shipmentId that createLabel needs. 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.

  • createLabel consumes the EXACT rate object returned by createShipment — its rateId is the provider's purchase handle and every bond rejects a rate without it. Persist the chosen {@link ShippingRate} (not a reconstruction of carrier/service/amount), not just its fields.

  • getRates(shipment) is a display-only convenience returning just the rates (no shipmentId). Do not use it as the first step of a purchase flow — you would have no id to pass to createLabel; use createShipment instead.

  • 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 (country is ISO 3166-1 alpha-2; set distanceUnit/massUnit explicitly rather than assuming defaults).

  • {@link MonetaryAmount.amount} is 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.