← All @molecule/* packages · App templates

@molecule/api-shipping-easypost

Provider bond · shipping · API (Node) · v1.0.1 · Apache-2.0

EasyPost multi-carrier shipping provider for molecule.dev.

npm install @molecule/api-shipping-easypost

npm · Source on GitHub · Implements @molecule/api-shipping

How it works

@molecule/api-shipping-easypost is a provider bond on the API (Node) side: it implements the shipping core interface (@molecule/api-shipping) with a concrete vendor or library behind it.

Your code calls the core; you wire this provider once at startup. Swapping vendors later is one line in that wiring, not a rewrite.

import { setProvider } from '@molecule/api-shipping'
import { provider } from '@molecule/api-shipping-easypost'

setProvider(provider)

// Then anywhere in your app:
import { createShipment, createLabel, trackPackage } from '@molecule/api-shipping'
const { shipmentId, rates } = await createShipment({
  from,
  to,
  parcels: [{ length, width, height, weight }],
})
const label = await createLabel(shipmentId, rates[0])

Works with: @molecule/api-bond, @molecule/api-secrets, @molecule/api-shipping

Secrets: EASYPOST_API_KEY

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.

EasyPost shipping provider for molecule.dev.

Implements the @molecule/api-shipping core interface against the EasyPost REST API (https://api.easypost.com/v2). Supports rate quotes, label purchase, label void/refund, and tracker creation across the carriers EasyPost itself supports (USPS, UPS, FedEx, DHL, etc.).

Quick Start

import { setProvider } from '@molecule/api-shipping'
import { provider } from '@molecule/api-shipping-easypost'

setProvider(provider)

// Then anywhere in your app:
import { createShipment, createLabel, trackPackage } from '@molecule/api-shipping'
const { shipmentId, rates } = await createShipment({
  from,
  to,
  parcels: [{ length, width, height, weight }],
})
const label = await createLabel(shipmentId, rates[0])

Type

provider

Installation

npm install @molecule/api-shipping-easypost @molecule/api-bond @molecule/api-secrets @molecule/api-shipping

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
}

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 a previously-quoted rate.

function createLabel(shipmentId: string, rate: ShippingRate): Promise<ShippingLabel>
  • shipmentId — EasyPost shipment ID returned from a prior {@link createShipment} call.
  • rate — The rate selected for purchase. Must include rateId.

Returns: The purchased label normalized to ShippingLabel.

createShipment(shipment)

Creates an EasyPost shipment via POST /shipments and returns its shipment id together with the normalized rates. The shipmentId is the handle {@link createLabel} needs to buy a label (POST /shipments/:id/buy), so use this (not {@link getRates}) when you intend to purchase — one round-trip yields both pieces, matching the core createShipment contract that @molecule/api-shipping-shippo also satisfies natively.

function createShipment(shipment: Shipment): Promise<ShipmentQuote>
  • shipment — Normalized shipment payload.

Returns: The EasyPost shipment id and its normalized rates.

getRates(shipment)

Requests rate quotes for a shipment, discarding the EasyPost shipment id. Convenience over {@link createShipment} for display-only flows; to buy a label you also need the shipmentId, so call {@link createShipment} instead.

function getRates(shipment: Shipment): Promise<ShippingRate[]>
  • shipment — Normalized shipment payload.

Returns: Array of normalized shipping rates.

listSupportedCarriers()

Lists the carriers supported by this EasyPost bond.

function listSupportedCarriers(): Promise<string[]>

Returns: Lowercase carrier identifiers.

trackPackage(carrier, trackingNumber)

Retrieves the current tracking status for a package. Creates a tracker via POST /trackers (idempotent — EasyPost reuses an existing tracker for the same carrier + tracking code) and normalizes the response.

function trackPackage(carrier: string, trackingNumber: string): Promise<TrackingStatus>
  • carrier — Carrier identifier (e.g., usps, ups).
  • trackingNumber — Carrier-assigned tracking number.

Returns: Normalized tracking status.

voidLabel(labelId)

Voids (refunds) a previously purchased label. EasyPost handles void via POST /shipments/:id/refund.

function voidLabel(labelId: string): Promise<void>
  • labelId — EasyPost shipment ID associated with the label.

Constants

provider

The EasyPost shipping provider implementing the ShippingProvider interface.

const provider: ShippingProvider

shippingEasypostSecretDefinitions

Secret definitions required by the EasyPost shipping bond.

const shippingEasypostSecretDefinitions: SecretDefinition[]

Core Interface

Implements @molecule/api-shipping interface.

Bond Wiring

Setup function to register this provider with the core interface:

import { setProvider } from '@molecule/api-shipping'
import { provider } from '@molecule/api-shipping-easypost'

export function setupShippingEasypost(): void {
  setProvider(provider)
}

Injection Notes

Requirements

Peer dependencies:

  • @molecule/api-bond ^1.0.1
  • @molecule/api-secrets ^1.0.1
  • @molecule/api-shipping ^1.0.1

Environment Variables

  • EASYPOST_API_KEY (required) — EasyPost API key

Runtime Dependencies

  • @molecule/api-bond

  • @molecule/api-secrets

  • @molecule/api-shipping

  • Requires EASYPOST_API_KEY in the environment (read per request — fail-fast error if unset). Optionally EASYPOST_API_URL to override the base URL (sandbox / proxy).

  • createLabel(shipmentId, rate) needs the EasyPost shipment id from the SAME quote. Use the core createShipment(shipment){ shipmentId, rates } and persist BOTH between quote and purchase; plain getRates() discards the id.

  • One parcel per shipment. An EasyPost shipment carries exactly one parcel (its API has a single parcel field, not an array), so passing parcels.length > 1 THROWS rather than silently dropping the extras — send each parcel as its own shipment, or use -shippo for multi-piece shipments.

  • Parcel.distanceUnit/massUnit are honored. Dimensions are converted to inches and weight to ounces (EasyPost's only accepted units — its Parcel object has no unit fields) before the call: cm→in, lb/kg/g→oz, so a metric parcel is priced correctly. Units default to 'in'/'lb' when unspecified, matching the -shippo bond so a unit-less parcel is priced the same by either provider.

  • voidLabel(labelId) refunds via POST /shipments/:id/refund — pass ShippingLabel.id (the EasyPost shipment id) returned by createLabel.

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.