Skip to content
Gaurav TiwariDocs

Physical Products: Shipping, Stock and Shipments

Version 0.2.0 Updated November 15, 2026

How to sell shipped products beside licenses and downloads: physical products, zones and rates, the address, stock and holds, shipments, and refunds of physical items. You set where you ship and what it costs, Vendwick prices shipping at checkout and holds stock while the buyer pays, and you send shipments with tracking from the order.

Turn it on in Store settings > Modules > Physical Goods and Shipping (it needs Licensing and Payments). While it is off, physical products cannot be saved or sold (the admin and the checkout both say so). Orders paid while it was on are always fulfilled, and their refunds still restock. You can turn it off while paid orders still wait to ship: the Modules page then says how many, and their shipping rows leave Needs attention (they would lead to a page that cannot ship them) until you turn it back on.

Physical products

  • Product type Physical product. It never issues a license key and never renews (its plans are one-time; pay what you want works; bundles cannot contain one).
  • Use plans for variants: a T-shirt has Small, Medium and Large plans.
  • Each plan has a SKU and a weight in grams (product page > Inventory and shipping). Weight-based rates add up the weight of the physical lines only, so a cart with a plugin and a mug ships the mug’s weight.
  • Stock tracking is per plan and optional. An untracked plan always sells.

Zones and rates

Store settings > Shipping > Zones and rates. A zone has a name, the countries (two-letter codes) or regions (US-CA) it covers, a currency and one or more rates.

The buyer’s address picks the zone in this order:

  1. a zone listing the address’s region (US-CA);
  2. a zone listing the country;
  3. a rest-of-world zone (countries *).

No match: checkout stops with NO_SHIPPING_ZONE and the buyer is told which countries you ship to. A zone in another currency than the cart stops with SHIPPING_CURRENCY_MISMATCH. Archived zones are ignored.

Rate types:

TypeCharged byExample
FlatOne priceStandard, 4.99
By order valueThe items subtotal after discountUp to 30.00: 6.00; up to 75.00: 4.00; above: free
By weightTotal grams of the physical linesUp to 500 g: 5.00; up to 2000 g: 9.00

Tier limits are inclusive ("up to 500 g" includes 500 g). Leave the last tier’s limit empty for "and above"; if the last tier has a limit, the rate is not offered for heavier (or larger) orders. Free from makes any rate free when the items subtotal after discount reaches it. When a zone offers several rates the buyer picks one; the cheapest is preselected.

Worked example, zone "Domestic" (GB, USD): Standard is flat 4.99, free from 50.00; Express is by weight, up to 500 g: 9.99, above: 14.99.

  • One T-shirt (25.00, 180 g): Standard 4.99 or Express 9.99. Total with Standard: 29.99.
  • Two T-shirts with a 20% coupon (items 50.00, after discount 40.00): Standard is 4.99, because free shipping looks at the subtotal after discount.
  • Three T-shirts (540 g) by Express: 14.99.

Coupons, tax and the quote

Shipping is its own kind: "shipping" line (id ship) in the price quote, with the zone, rate and weight in quote.shipping.

  • Coupons never discount shipping, and shipping never counts toward a coupon’s minimum order.
  • Tax: a rate marked Charge tax on this rate is taxed with the items (exclusive or inclusive, as your tax settings say); an untaxed rate carries no tax. Every line’s discount, tax and total still add up to the order’s.
  • Merchant-of-record gateways (Polar) sell digital goods only, so they are not offered for a cart with physical items. The checkout says why.
  • A cart with a subscription and a physical item can only be paid on Stripe (see Cart and Checkout). Stripe’s subscription checkout doesn’t receive the shipping address; it is still stored on the checkout and the order.

The address

A cart with a physical item collects a shipping address: name, company (optional), address, line 2 (optional), city, region, postcode, country (from the countries you ship to) and phone (optional). Carts with nothing to ship never see these fields.

The server checks every address: required fields, sensible lengths (too long is refused, not cut), a region where the country always has one (US, CA, AU, IN, BR, MX), postcode shapes for US, CA, GB, DE, FR, ES, IT, NL, IN, AU and JP, and that you ship to the country.

The form asks for the country first, and names the region and postcode fields the way that country writes them ("State" and "ZIP code" in the US, "County" and "Postcode" in the UK); they are marked required where the server requires them. With JavaScript the shipping methods and their prices update as the address changes; without it, "Update shipping" prices the page on the server.

"Billing address is the same" is on by default: tax then follows the shipping address. The address is frozen on the checkout and the order, and saved on the customer for next time (prefilled when they are signed in, editable in their account). The admin can change it on the order until the first shipment leaves. PayPal receives it (shipping_preference: SET_PROVIDED_ADDRESS), Stripe gets it as the payment’s shipping details, Razorpay in the payment notes.

Stock and holds

Each tracked plan has one stock document: units on hand and the holds of open checkouts.

  • When checkout starts, one guarded write checks on hand - active holds >= quantity and records the hold. If not, the buyer sees "There is not enough of … in stock" (or "Only N of … are left" when Show how many are left is on) (OUT_OF_STOCK). Twenty buyers racing for the last five units get at most five holds.
  • A hold lasts 60 minutes, or until the checkout expires if that is sooner. An unpaid checkout no longer keeps stock from other buyers for the whole life of its payment session. Expired holds are ignored at once and removed on the next write. A failed or expired checkout’s hold is given back as soon as its payment session can no longer be paid; a buyer who pays later still gets the order (see Backorders).
  • Holds per visitor are limited: past 5 checkouts that hold stock from one IP address in an hour, a checkout is still checked against the stock but holds nothing, so one visitor cannot lock up a product by starting checkouts.
  • When the payment is confirmed the sale is committed once per checkout, even if fulfillment is retried after a crash.
  • Backorders. A buyer who pays after their hold lapsed and the stock was taken still gets the order (they paid). Stock goes below zero, the order is flagged Backorder, and it shows under Needs attention until you ship it.
  • Adjust a count from Shipping > Stock or the product’s Inventory tab: a number of units to add or remove, with a reason (kept in the activity log).

The storefront shows "Out of stock" with the buy button disabled, and "Only a few left" at or below your low-stock threshold. The exact count ("Only N left", the cart stepper’s cap, stock_left in the storefront API, available in the checkout and quote answers) is shown only when Show how many are left is on in Shipping > Settings. With it off (the default), the checkout and the cart drawer still say when a line asks for more than is in stock ("There is not enough in stock. Lower the quantity to continue.", enough: false) and stop that line’s quantity from going up, without revealing the count. The storefront API adds in_stock, low_stock, sku and weight_grams to physical plans.

  • Turning tracking on for a plan that is already tracked, with a different starting count, is refused ("Stock is already tracked for this plan"): change the count with an adjustment instead.

Shipments

Open the order and use the Shipping section:

  • Create shipment for the units still to ship (all of them, or some), with carrier, tracking number and tracking link, as Shipped (emails the customer) or Packed (not left yet). A shipment can never take more units than were ordered, even when two people ship at once.
  • Move it forward: Mark shipped, Mark delivered, Mark returned.
  • Packing slip opens a printable page with the items, quantities, SKUs and both addresses, no prices.

The order’s shipping status follows its shipments: To ship (unfulfilled), Partly shipped (partial), Shipped, Delivered, Returned. Units refunded before they shipped are no longer waited for.

Emails: "Your order has shipped" (carrier, tracking link, items) when a shipment leaves, and "Delivered" when it is marked delivered (on by default; turn it off in Shipping > Settings). Both are editable in Emails. The events order.shipped and order.delivered reach your outgoing webhooks with the order and the shipment. Delivery is retried if a listener fails, and never sent twice.

The receipt and the invoice list shipping as its own line, and the receipt shows the address. The customer’s account shows the address, the status and tracking links per shipment.

Refunds and returns

  • Refund lines have a restock flag. For an item that has not shipped it defaults to on, whether you refund by item or by amount, and for refunds made in the provider’s dashboard and lost disputes; once it shipped, units go back to stock only when you ask. A refund by amount restocks an item only when it refunds what is left of that item. Each refund line restocks once, even if the refund job is retried or two refunds are sent at once (the second is refused).
  • When the provider later fails a refund that restocked units, those units are taken out of stock again, once. If they were sold meanwhile the count goes below zero and the order is flagged Backorder. The order is back in To ship.
  • Shipping can be refunded on its own (orderItemId: "shipping"), never more than what is left of it. Credit notes, report refunds and the order’s refunded amount still agree.
  • Marking a shipment Returned does not refund anything by itself; it offers the refund form filled with the returned units, restock on.

Not included in 0.2.0

  • Selling physical products through the Core Forms bridge: the bridge has no address field, so a physical product fails with ADDRESS_REQUIRED.
  • Live carrier rates (UPS, DHL, USPS quotes) and shipping labels.
  • Several warehouses or stock locations.
  • Per-item shipping classes or per-product shipping prices.
  • Split shipping addresses in one order.
  • Physical subscriptions (boxes that ship every month).
Share

Was this helpful?

Still stuck? Open a support ticket