Shipping
litecommerce has two distinct shipping surfaces:
- Checkout shipping — zones and flat rates that a storefront reads while pricing a cart.
- Post-order fulfillment — package planning, carrier selection, label purchase, physical handoff, cancellation/recovery, and warehouse output for an existing order.
Keeping those surfaces separate matters. Reading a checkout rate, package
recommendation, quote, or warehouse row never buys postage or marks goods as
shipped. Examples reuse the api() helper.
Checkout zones and rates
Zones (merchant)
Reads are open to any member; writes require settings:manage. Owner/Admin
retain broad access, and Staff needs that group.
const zone = await api("/merchant/shipping/zones", { method: "POST", token,
body: JSON.stringify({
name: "United States", // required, 1–200
countries: ["US"], // required; uppercase alpha-2; [] is allowed
sortOrder: 0, // optional; defaults to 0
}) });PATCH /merchant/shipping/zones/:id updates a zone; sending countries
replaces the list. Zones archive/restore rather than hard-delete:
await api(`/merchant/shipping/zones/${zone.id}/archive`, { method: "POST", token });
await api(`/merchant/shipping/zones/${zone.id}/restore`, { method: "POST", token });An archived zone disappears from the public read immediately.
countries: [] is valid configuration, but it matches no destination. Neither
that zone's flat-rate configuration nor its live-rate eligibility applies
anywhere until at least one country is added.
Rates (merchant)
Rates hang off a zone. A rate is a flat per-shipment fee with an optional minimum-subtotal gate.
await api(`/merchant/shipping/zones/${zone.id}/rates`, { method: "POST", token,
body: JSON.stringify({
name: "Standard (5–7 days)", // required, 1–200
priceInCents: 599, // required, flat fee
minimumSubtotalInCents: 0, // optional; defaults to 0
estimatedDelivery: "5–7 business days", // optional checkout copy
sortOrder: 0, // optional; defaults to 0
}) });PATCH /merchant/shipping/rates/:rateId updates a rate. Rates archive and
restore through POST …/rates/:rateId/archive and /restore.
Read at the cart (public)
GET /api/v1/public/shipping/zones returns only active zones and rates,
ordered by sortOrder then name:
const zones = await api("/public/shipping/zones");
// → [{ name, countries, liveRatesEnabled, rates: [{ name, priceInCents, … }] }]An empty rates array does not mean the zone cannot ship. Read
liveRatesEnabled with it — that pairing is the whole point of the field:
liveRatesEnabled is zone-level eligibility, not current availability. It
tells you how the zone is configured; it cannot tell you a price will come back.
rates | liveRatesEnabled | What it means |
|---|---|---|
[] | false | No active flat rate is published; live-rate eligibility is paused. |
[] | true | No active flat rate is published; the zone is eligible for live carrier rating. |
| non-empty | false | Active flat-rate configuration only. |
| non-empty | true | Active flat-rate configuration and live-rate eligibility. |
Both flat-rate configuration and live-rate eligibility are scoped by
countries. A zone with countries: [] matches no destination, so both apply
nowhere however the flag reads. For a country no active zone lists, no zone
configuration applies.
true is necessary but not sufficient for a carrier option. Address, cart,
tenant/platform, origin/carrier, and quote conditions that are not represented
on this read still apply. Likewise, each flat rate is subject to its own
minimumSubtotalInCents floor, so a published rate need not apply to the current
cart.
So present carrier shipping as "calculated at checkout" rather than as a
promise. liveRatesEnabled is also a pause control rather than an opt-in: a
zone the merchant has never touched reads true.
This zone read does not resolve a cart-specific option set. For an ordinary
checkout, call
POST /api/v1/public/checkout/sessions/:token/reprice and read
availableShippingOptions from its response. Physical subscription checkout
has no public equivalent for resolving its current shipping options, so do not
infer its checkout outcome from this endpoint.
An empty top-level array means the tenant has no active zones, including when every configured zone is archived. Shipping is therefore unconfigured, not unrestricted. That is a configuration statement; it does not establish a checkout outcome.
Taxes are a separate checkout concern. The shipping schema carries no tax fields. Taxes can be computed through the checkout-session bind flow when a tax provider is enabled; shipping rates remain their own configuration and preview surface.
Processing time and the order cutoff (public)
A shipping-policy page usually needs two facts that are not per-zone or per-rate: how many business days the merchant takes to process an order, and the local time after which an order rolls to the next day. Those are tenant-wide, so they live on the site-config read rather than the zone read:
const { orderProcessing } = await api("/public/storefront/site-config");
// → { mode, timezone, processingDays, cutoffTime }Read mode first. It is the difference between "configured" and "not
configured", and reading past it is the mistake this field exists to prevent:
mode | processingDays | cutoffTime | What to render |
|---|---|---|---|
"description" | null | null | Nothing. The merchant has not configured structured timing, so the only timing the platform publishes is the free-text estimatedDelivery on each rate. |
"structured" | e.g. 2 | "16:00" | "Orders placed before 4:00 PM timezone start processing the same business day; processing takes 2 business days." |
"structured" | e.g. 1 | null | The same sentence without the cutoff clause. null means no cutoff applies, so orders are never rolled to the next day by one. |
"structured" | 0 | null | Same-business-day dispatch with no cutoff. Uncommon — it means the stored cutoff is unusable — but valid, so render it. |
"structured" | null | null | Nothing, and check timezone: it is null, so the stored zone is not one this read publishes and no interpretable timing is available. See the three reasons below — they are not equivalent. |
Three things worth knowing before you wire copy to it:
-
processingDaysis the effective value, not the stored one. A structured estimate configured with no cutoff cannot honestly promise same-business-day dispatch, so the platform floors processing to one business day in that case. A0therefore normally arrives with a cutoff — but0withcutoffTime: nullis reachable when the merchant's stored cutoff is in a form the platform cannot apply, and it means what it says: same-business-day dispatch, no cutoff. Render it rather than treating it as impossible. -
timezonegates the other two. A bareHH:mmwith no zone is not renderable, so whentimezoneisnullbothprocessingDaysandcutoffTimearenullas well. Readtimezonebefore the two timing fields.The value is usually an IANA name, but the gate is what
Intl.DateTimeFormataccepts as atimeZone— a superset of the IANA database. A tenant storing a fixed offset like+05:00has it published verbatim, so treat this as a string you can hand toIntl, not as a name you can look up in the IANA database.nullmeans the stored zone is not one this read will publish, for one of three distinct reasons:- The platform cannot resolve it at all (blank, or not a zone). Checkout computes no dates either.
- It is process-relative (
local,system). The date engine accepts it, but it means "whatever machine evaluates it", so it would resolve to your runtime zone rather than the merchant's. - It is a deterministic offset in a spelling
Intlrejects (UTC+5). This one is unambiguous — the same +05:00 for everybody — and is withheld only because of the spelling. Do not treat it as ambiguous the waylocalis. Note the asymmetry:+05:00is published,UTC+5is not.
Do not read
nullas "checkout shows no dates." In cases 2 and 3 checkout still computes them. Both cases also require a row written outside the platform API, which validates this column with the same rule this read applies — so neither is reachable for a tenant configured normally. -
"Same day" always means same BUSINESS day. Business days are Mon–Fri, and the platform rolls a weekend ship date forward to the next weekday — so a Saturday order with
processingDays: 0ships Monday, not Saturday. Do not render unqualified "ships today" copy from a0. There is also no holiday calendar, so a configured holiday is not skipped.
These are the same inputs that produce shipByDate and the delivery window on
a checkout session, so a policy page wired to this object tracks what checkout
promises the shopper. That is the point: hand-typed processing copy has no way
to notice when the configuration behind it changes.
One documented exception, so you are not surprised by it: for a tenant whose
stored zone is process-relative (local, system) or a deterministic offset in
a spelling Intl rejects (UTC+5), this object publishes no timing while
checkout still computes dates. The page then under-reports rather than
contradicting — it says nothing where checkout says something. Neither case is
reachable through the platform write API, which validates that column with the
same rule this read applies, so both mean a row written around it.
Native post-order fulfillment
The safe physical-shipment sequence is explicit:
select lines → plan/confirm package → quote exact service → buy label (READY)
→ print/pack → confirm carrier custody (SHIPPED)
Provider connection, box presets, package and delivery-promise planning,
quoting, label purchase/reconciliation, and prepared-shipment
cancellation/recovery are Owner/Admin-only. Direct fulfillment, physical
handoff, canonical cancellation through /merchant/commerce when no current or
ambiguous postage exists, and warehouse outputs require orders:operate;
Owner/Admin retain broad access, while Staff needs that delegated group.
Each package owns exact original-order line quantities. READY means postage
and package evidence are prepared; it contributes no handed-off quantity,
does not consume inventory, and does not send a shipped notification. Only the
later handoff command records carrier custody.
Connect a shipping provider
The merchant provider surface uses these exact routes:
GET /merchant/shipping-providerreturns connection status, sandbox/disabled state, a masked key fingerprint, verification timestamps, and the cached carrier list. It never returns the API key.PUT /merchant/shipping-providerconnects or replaces a ShipEngine key after a live verification. The request is a high-risk action with a requiredreason; the key is write-only and encrypted at rest. ATEST_key is identified as sandbox mode.GET /merchant/shipping-provider/serviceslists rateable services from the connected carriers.POST /merchant/shipping-provider/testrechecks the stored key and refreshes the carrier snapshot.POST /merchant/shipping-provider/disconnectis reasoned and removes the stored credential. It fails closed while a label purchase is in flight.
Provider connection and test routes can make live provider reads. They do not buy labels or add funds.
Save boxes and plan a loaded package
/merchant/shipping/box-presets stores revisioned reusable box facts: usable
inside dimensions, carrier-facing outside dimensions, tare weight, and an
optional maximum loaded weight. Presets can be activated/deactivated and one
active preset can be the merchant default. A default is a preference, not an
automatic fit claim.
For selected order lines, call:
const packagePlan = await api(
`/merchant/orders/${orderId}/shipping-package/recommendation`,
{ method: "POST", token, body: JSON.stringify({ lines }) },
);This read-only planner returns RECOMMENDED only when a bounded placement
witness proves every selected unit fits an active preset. Otherwise it returns
NO_RECOMMENDATION with an explicit reason instead of guessing. The merchant
still confirms the actual loaded dimensions and weight used for rating and
purchase.
Quote an exact carrier selection
Before funded label purchase, quote the exact lines, loaded parcel, carrier, and service:
const quote = await api(`/merchant/orders/${orderId}/shipping-label/quote`, {
method: "POST",
token,
body: JSON.stringify({ lines, package: loadedPackage, carrierId, serviceCode }),
});The response includes the cost, currency, selection classification, funding
preflight, any acknowledgement/reason requirements, and an opaque
selectionQuoteFingerprint. The quote can make a non-funded provider rating
request, but it does not allocate lines, buy postage, mutate fulfillment or
inventory, or notify the customer.
When the order carries a delivery promise, POST /merchant/orders/:orderId/delivery-promise-recommendations can compare the
original service with same-or-faster and meets-promise candidates. Missing or
incompatible evidence fails closed; the comparison never selects or purchases
a label.
Buy a label and prepare the shipment
POST /merchant/orders/:orderId/shipping-label is the high-risk, money-moving
step. Send a stable x-idempotency-key, the exact reviewed selection, its
selectionQuoteFingerprint, the required high-risk reason, and any separate
selection overrideReason requested by the quote.
The server recomputes and re-preflights the selection before claiming spend.
It never automatically adds ShipEngine funds. A successful purchase returns a
durable label and a prepared shipment in READY; printing or reprinting that
label still does not mark carrier handoff.
If the provider outcome is uncertain, the API returns a conflict and preserves
the claim for reconciliation. Do not start a second purchase. Read
GET /merchant/orders/:orderId/shipping-label/:purchaseId/reconciliation-status
and, when its server-derived action permits, use POST /merchant/orders/:orderId/shipping-label/reconcile as Owner/Admin with the
exact existing purchaseId, a stable x-idempotency-key, and the required
high-risk reason. Reconciliation adopts only exact provider evidence and is
idempotent; a first provider 404 does not immediately prove that no label was
created.
Record physical carrier handoff
After the carrier has the package:
await api(
`/merchant/commerce/orders/${recordId}/fulfillments/${fulfillmentId}/handoff`,
{
method: "POST",
token,
headers: { "x-idempotency-key": operationKey },
body: JSON.stringify({ notify: true }),
},
);The handoff moves that exact READY shipment to SHIPPED, consumes its line
reservations once, updates fulfillment rollups, and durably records SEND or
SUPPRESS for the shipment notification. Retry with the same idempotency key.
For a direct/manual shipment that does not need native postage, POST /merchant/commerce/orders/:recordId/fulfillments creates an explicit
line-managed SHIPPED fulfillment instead; see
Orders & fulfillment.
Cancel or recover a prepared shipment
Never delete a prepared package or simply rewind its status:
POST /merchant/orders/:orderId/fulfillments/:fulfillmentId/cancel-prepared-shipmentis an Owner/Admin-only, reasoned command with a stablex-idempotency-key. It cancels label-free work immediately. For current paid outbound postage it records one cancellation intent and queues the exact carrier void; the fulfillment staysREADYand handoff remains blocked until authoritative void evidence arrives. A provider-approved void does not promise a carrier credit or a customer refund.POST /merchant/orders/:orderId/fulfillments/:fulfillmentId/cancel-prepared-shipment/retry-carrier-checkis an Owner/Admin-only, reasoned recovery with a stablex-idempotency-key, available after automatic cancellation checks are exhausted. It performs one GET for the frozen label; it never issues another void or buys postage.POST /merchant/commerce/orders/:recordId/fulfillments/:fulfillmentId/acknowledge-external-label-retirementis an Owner/Admin-only, reasoned acknowledgement with a stablex-idempotency-key; its body must setcarrierResponsibilityAccepted: true. It records responsibility for already-retired external/legacy postage, is not provider proof, and cannot bypass current or ambiguous native postage.
Every path is package-local and replay-safe. SHIPPED or DELIVERED goods
move through Returns rather than prepared-shipment cancellation.
Warehouse output without new postage
GET /merchant/shipping/warehouse-outputs lists bounded shipment rows (READY
by default) with packing-slip data and server-derived label eligibility. For
1–50 selected fulfillment IDs:
POST /merchant/shipping/warehouse-outputs/csvexports rows in request order and neutralizes spreadsheet-formula prefixes.POST /merchant/shipping/warehouse-outputs/labelscombines already-purchased PDF artifacts. It makes no authenticated ShipEngine API request and buys no postage; any missing, voided, invalid, oversized, or unavailable artifact fails the whole request instead of silently omitting a label.
Related
- Orders & fulfillment — line allocation, status projections, direct fulfillment, and carrier handoff
- Returns / RMA — goods that have already shipped or arrived
- BYO checkout API — server-authoritative checkout totals
- Pricing rules —
FREE_SHIPPINGdiscounts - API reference — exact request and response schemas