Shipping Provider Functions
A shipping provider answers "what can this cart be shipped with, and for how much?". Providers are registered in memory at bootstrap, attached to zones by the merchant, and asked for methods per (cart, provider, zone) tuple at checkout.
Everything below is reachable from @evershop/evershop/checkout/services.
Import
import {
registerShippingProvider,
getShippingProvider,
getAllShippingProviders,
resolveShippingQuote,
ShippingQuoteError,
buildShippingContext,
resolveZonesForAddress,
getOriginAddress,
serializeItems,
setShippingMethod,
computeFingerprintFromCart,
computeFingerprintFromCtx
} from '@evershop/evershop/checkout/services';
import type {
ShippingProvider,
ShippingContext,
ShippingMethod,
ShippingItem
} from '@evershop/evershop/types/shippingProvider';
ShippingMethodIntent, ResolvedShippingMethod, BuildShippingContextArgs and ZoneAddressFilter are declared in their source files, but the barrel re-exports only the functions from those modules. They are shown below as shapes for reference; you cannot import type them from @evershop/evershop/checkout/services. The provider-facing types (ShippingProvider, ShippingContext, ShippingMethod, ShippingItem) live in @evershop/evershop/types/shippingProvider and are fully importable.
Provider registry
registerShippingProvider
registerShippingProvider(provider: ShippingProvider): void
Register a provider. Call from your module's bootstrap.ts.
The ShippingProvider contract:
| Field | Type | Required | Description |
|---|---|---|---|
code | string | Yes | Unique across the system. Stored on cart.shipping_method_data.provider_code. |
name | string | Yes | Display name in the admin UI. |
getMethods | (ctx: ShippingContext) => Promise<ShippingMethod[]> | Yes | Available methods for this cart + address. Must be safe to call repeatedly with the same inputs. Return [] when the provider cannot serve this address or cart. |
description | string | No | Description for the admin UI. |
zoneConfigFields | ZoneConfigField[] | No | The per-zone config form, declared field by field. Values are saved to shipping_zone_provider.config and handed back as ctx.zoneConfig. |
validateMethod | (ctx, methodCode) => Promise<ShippingMethod | null> | No | Re-validate a previously selected method. Defaults to getMethods(ctx).find(m => m.code === methodCode); override for a cheaper one-method API call. |
quoteTtlSeconds | number | No | Quote validity. A stored snapshot older than this is re-quoted even when the fingerprint matches. Omit for providers whose quotes do not expire by time. |
quoteTimeoutMs | number | No | Per-provider budget for one getMethods call. Defaults to 5000. This is checkout-blocking time — keep it as low as honest. |
Throws:
| Condition | Message |
|---|---|
| Not an object | registerShippingProvider: provider must be an object |
Missing / non-string code | registerShippingProvider: provider.code is required and must be a string |
Missing / non-string name | registerShippingProvider: provider.name is required and must be a string |
Missing getMethods | registerShippingProvider: provider.getMethods is required (provider code: …) |
| Duplicate code | Shipping provider "<code>" is already registered. Each provider must have a unique code across the system. |
| Called after bootstrap | Registry is locked. … — registration routes through addProcessor. |
Duplicate-code detection is synchronous, so a second registration of the same code blows up at the call, not at the first getAllShippingProviders().
import { registerShippingProvider } from '@evershop/evershop/checkout/services';
export default () => {
registerShippingProvider({
code: 'shippo',
name: 'Shippo',
description: 'Live rates from every connected carrier',
quoteTtlSeconds: 900,
quoteTimeoutMs: 8000,
async getMethods(ctx) {
if (!ctx.destination.country) return [];
const rates = await fetchRates(ctx);
return rates.map((r) => ({
code: r.servicelevel.token,
name: r.servicelevel.name,
cost: Number(r.amount),
carrier: r.provider
}));
}
});
};
getShippingProvider
getShippingProvider(code: string): Promise<ShippingProvider | undefined>
One provider by code, or undefined when no such provider is registered (or its module is not installed). Async because it reads the registry through getValue.
getAllShippingProviders
getAllShippingProviders(): Promise<ShippingProvider[]>
Every registered provider, in registration order (which follows the alphabetical module load order).
There is no global shipping_provider.is_enabled toggle — the in-memory registry is the source of truth. Merchant intent is expressed by attaching (or not attaching) the provider to a zone.
Quoting
resolveShippingQuote
resolveShippingQuote(
cart: Cart,
intent: ShippingMethodIntent
): Promise<ResolvedShippingMethod>
Fully resolve a bare { provider_code, method_code } intent against the cart's current state into an enriched snapshot.
interface ShippingMethodIntent {
provider_code: string;
method_code: string;
}
interface ResolvedShippingMethod {
provider_code: string;
method_code: string;
snapshot: {
code: string;
name: string;
cost: number;
carrier?: string;
delivery?: unknown;
};
fingerprint: string;
quotedAt: string;
}
It resolves every zone matching the cart's shipping address in order and takes the first zone where the provider is attached, enabled, and the method still validates. A provider error inside one zone is logged and the loop continues to the next zone.
Throws ShippingQuoteError on:
| Condition | Message |
|---|---|
| Missing intent fields | Missing provider_code or method_code |
| Provider not registered | Shipping provider "<code>" is not registered |
| Cart has no shipping country | Shipping address is required |
| No zone covers the address | We do not ship to this address |
| Method no longer validates in any zone | Selected shipping method is no longer available |
Other errors propagate unchanged. API handlers translate ShippingQuoteError into user-facing responses.
ShippingQuoteError
class ShippingQuoteError extends Error {}
error.name is 'ShippingQuoteError'. Use instanceof to separate "tell the customer" failures from genuine bugs.
import {
resolveShippingQuote,
ShippingQuoteError
} from '@evershop/evershop/checkout/services';
try {
const quote = await resolveShippingQuote(cart, intent);
} catch (e) {
if (e instanceof ShippingQuoteError) {
response.status(400).json({ error: { message: e.message } });
return;
}
throw e;
}
setShippingMethod
setShippingMethod(cart: Cart, intent: ShippingMethodIntent): Promise<void>
Set the cart's shipping method from a bare intent. Pre-resolves the quote and writes the enriched value onto shipping_method_data.
shipping_method_data directlycart.setData('shipping_method_data', intent) stores a value with no snapshot and no fingerprint. That breaks the dependent fields (shipping fee, taxes) and the cache-by-fingerprint path on rebuild. Always go through setShippingMethod.
Throws whatever resolveShippingQuote throws.
Context helpers
These are the pieces the checkout orchestrator composes. Providers rarely call them; they are exported so extensions can build the same context outside the normal flow (an admin re-quote tool, a test harness).
buildShippingContext
buildShippingContext(args: BuildShippingContextArgs): Promise<ShippingContext>
Compose a ShippingContext for a single (cart, provider, zone) tuple.
| Field | Type | Required | Description |
|---|---|---|---|
cart | Cart | Yes | The cart. Only getData / getItems are used. |
provider | ShippingProvider | Yes | The provider being asked. |
zone | ShippingZoneRow | Yes | The resolved zone, placed on ctx.zone. |
attachment | { config } | null | No | Pre-loaded shipping_zone_provider row. Its config becomes ctx.zoneConfig. Pre-loading avoids a round trip per zone/provider combination. |
destinationOverride | Address | null | No | Use a tentative address instead of the cart's. |
ctx.currency comes from the cart, falling back to getStoreCurrency(). ctx.providerConfig is always {} — global provider config was removed before release; secrets belong in process.env, per-zone state in zoneConfig.
resolveZonesForAddress
resolveZonesForAddress(filter: ZoneAddressFilter): Promise<ShippingZoneRow[]>
Zones covering a destination. A zone matches when the country is in its shipping_zone_country rows and either the zone has no province rows for that country (whole country covered) or the destination province matches one.
interface ZoneAddressFilter {
country: string;
province?: string | null;
postcode?: string | null; // reserved; not used for matching yet
}
Multiple zones may match one address — overlapping coverage is allowed and the orchestrator fans out per zone. Returns [] when country is empty.
getOriginAddress
getOriginAddress(): Promise<Address>
The shop's origin address, composed from the existing store settings (storeCountry, storeProvince, storeCity, storeAddress, storePostalCode) rather than a separate origin setting.
Returns a defined-but-possibly-incomplete Address. Providers that need specific fields — USPS needs country + postcode — must validate and return [] when they are missing.
serializeItems
serializeItems(cart: Cart): ShippingItem[]
Turn a cart's items into plain ShippingItem DTOs. Providers never see Cart or CartItem instances.
interface ShippingItem {
productId: number;
sku: string;
name: string;
qty: number;
weight: number; // per-unit goods weight, no packaging tare
unitPrice: number; // tax-exclusive
lineTotal: number;
noShippingRequired: boolean;
dimensions?: { length: number; width: number; height: number; unit: 'cm' | 'mm' | 'in' };
}
Numeric fields are coerced from PostgreSQL's numeric (which arrives as a string) and default to 0. dimensions is present only when the item's package has both a length and a width; the unit follows the store's dimension setting, normalised to cm | mm | in.
Fingerprint helpers
computeFingerprintFromCart(cart: Cart): string
computeFingerprintFromCtx(ctx: ShippingContext): string
A SHA-1 over the shipping-relevant cart state: destination (country, province, postcode, city only), totalWeight, totalValue, and a sorted [productId, qty] list. Fields like full_name and telephone are deliberately excluded, so two copies of the same address hash identically.
computeFingerprintFromCartruns inside theshipping_method_dataresolver and stampsResolvedShippingMethod.fingerprint.computeFingerprintFromCtxbacks the per-request memoization ofprovider.getMethods.
Both must produce the same hash for the same logical cart state — they are two entry points to one algorithm. Origin is intentionally not in the fingerprint: it changes when an admin reconfigures the store address, which is far too rare to invalidate every cached quote.
Not reachable through the exports map
These exist in the source but are not re-exported from modules/checkout/services/index.ts, so they cannot be imported from @evershop/evershop/checkout/services (or from any other entry in the package exports map). Treat them as internal:
| Function | Source | What to use instead |
|---|---|---|
buildDefaultParcels | modules/checkout/services/cart/packing.ts | The cart already exposes the computed parcels as a cart field; carriers receive parcel dimensions through ShippingItem.dimensions and CreateLabelInput. |
createPackage | modules/checkout/services/package/packageManager.ts | The admin Package REST API. The service functions are hookable, so extensions can still hookBefore/hookAfter them by name without importing them. |
updatePackage | ||
deletePackage |
The core shipping-rate CRUD (createCoreShippingRate, updateCoreShippingRate, deleteCoreShippingRate) is exported, along with its hook helpers and the CoreShippingRateData / CreateCoreShippingRateInput types.
See Also
- Shipping Provider Development — Building a provider end to end
- Checkout Settings — Guest checkout, price rounding
- Shipping Zone API — Zone and attachment endpoints
- Carrier Development — Labels and tracking, which are a separate concern from quoting