Shipment Packaging
One package table, one rate matrix, one packing grid — read by the listing builder’s margin maths and by the ShipStation rules, so a tare typed once cannot disagree with itself.
One package table, one rate matrix, one packing grid — read by the listing builder’s margin maths and by the ShipStation rules, so a tare typed once cannot disagree with itself.
MR-089. Spec: references/shipstation-package-engine.md §“Screen requirements”.
ShipStation derives a bundle’s weight as Σ(component package weight × qty), so the packaging tare is counted once per unit. Every Merch Riot SKU is a bundle, so that is the whole catalogue, and both UI weight fields are disabled. Fulfillment is fixed by configuration — components carry item weight only, and one automation rule per package adds the tare back. Merchandising is not. At listing-build time there is no order, so the listing builder has to predict package, weight and postage to compute margin (MR-064). That prediction is this screen.
Not the Catalog Product — a formula can supply several branded products and they pack identically. An earlier draft proposed a shared bottle profile; Paul rejected it (“I can never see us assuming that two different source formulas have the same weight or dimensions”) and the formula carries its own profile.
One thing the fixture says out loud, for Paul. In source-formulas-list.html, dimensions repeat across formulas even though weights do not: SF-0019, SF-0029 and SF-0044 are all 2.6×2.6×5.0. Since packing is driven by dimensions and weight is not, several rows will compute the same package — visibly. Per-formula still works and stays the decision; this is only why the grid looks repetitive, and it is the argument a profile would have made.
MR‑348 — where there is no formula, the product is the row. A catalog product’s Sourcing is now Labeled by us (a formula is required) or Arrives finished (the goods come already branded, and there is never a formula). An Arrives-finished product with no formula had no row in either grid, so it could never get a package answer. It now gets its own row, keyed on the catalog product: its CP- id stands where a formula row shows its SF- id, and a no formula tag says which kind of row it is. Nothing else about the row is special — fit, borrowed tests, Basis and the send switch run on it unchanged, because they only ever needed a weight, a size and a box rule.
The formula is still the grain wherever there is one — and a product that arrives finished never has one (MR‑350, RI‑669). CP-0044 Avinol PM was drawn under its two formulas, Sleep Blend and Nighttime Formula; since the formula link is gone, it is one product row (MR‑353). Paul’s tested × 5 → Box (8x4x4) and both formulas’ shipments carry over to it. Its weight is the active version’s, 2.9 oz; v2’s 3.0 oz replaces it when v2 is made active on the product, which is also when ShipStation gets it. A weight or size change on the product sends a Tested row here back to Estimated (RI‑676).
Rejected — a placeholder formula record for such products. Minting an SF- row to hold a finished product’s weight would give the grid a key, but the record would claim a formula that does not exist, show up in the formula list and its counts, and be the one place that product’s weight is typed — while the product screen, per the Sourcing round, is where it is edited. Two homes for one number is the drift this screen exists to end.
Where ships in a box lives for such a product: on the product. For a formula row it is set on the formula and inherited; with no formula there is nothing to inherit from, so the product carries its own flag — the same “settable on a product” route that already existed, used as the only route.
Combos use the same rule per member. A combo key is its set of formulas; a member with no formula enters the key by its CP- id (CP-0151×1 + SF-0051×1), and the row takes the same tag. Brands still never key a combo — a product-keyed member is not a brand standing in for a formula, it is the only physical profile that product has. One example candidate is drawn rather than left open, because the resolver needed no new code to answer it.
The two new fixtures are illustrative. CP-0151 Complexion MD Renewal Cream (5.2 oz, 2.6×2.6×2.4 in) and CP-0152 Zero Air OG Essentials (22 oz, 5.5×5.5×8.0 in) are products Paul named; their weights and sizes, their shipment counts (38 and 9), the Complexion + Marine Collagen combo (14) and Dana’s recorded Zero Air × 1 → Box (8x8x8) pairing are placeholders. That pairing is there because geometry alone picks the 7x7x7 for an 8‑inch-tall carton — volume fits, the lid does not — which is the case recorded tests exist for. Zero Air reads Pending honestly: from × 5 up nothing in the table holds it.
The Basis column says where the answer came from: Tested (someone in the warehouse tried the product against the packaging we stock and chose the best fit — RI‑851) · Estimated (the system worked it out) · Pending (we cannot answer — a missing number upstream, or nothing fits). Only Tested is settable. An Estimated row’s select offers Tested and a Tested row’s offers Not tested; either flip is stamped with who and when, under the select. A Pending row stays disabled and names what blocks it, so a machine conclusion is never typed over and a row nothing can answer is never claimed (MR‑413).
This replaces four tiers with three, and MR‑092 is the record it overturns. The fourth was Implied — a real test of a bigger load in the same box. It earned its own word only because it behaved differently downstream: Implied sent the package where Estimated withheld it. Once the send decision became one switch covering everything that is not Tested, the two behave identically and a second word only split the column. Which test was borrowed is still on the cell, in its tooltip.
And withholding is a setting now, not a rule. MR‑089 made it permanent. Paul, 2026-09-11: “there’s really no reason to ever hold a package from going to ShipStation — the only thing that happens is that the shipping person needs to select the packaging.” Two facts back him. Under a pound all four Commercial tiers carry the same price, so a wrong tare there costs exactly nothing. And the package is not the field that blocks a batch print — weight is; service, package and dimensions already arrive from product defaults on a single-bundle qty-1 order. So withholding does not send anyone to the bench, it just declines to improve on the default.
What it does buy is narrow and real. The package is the weight here: our rule is if package = X, add X’s tare, so naming a box we are unsure of declares a weight we are unsure of. The exposure is entirely at 16.000 oz — USPS re-weighs in the network and Auctane adds 20% of the adjustment, minimum $0.50, and a pattern of short payment can cost Commercial pricing outright. A bad guess can move the total by up to 9 oz.
So it is a switch, off at launch, on the ShipStation Automations tab — because how often the estimate is right is a measurable question and not a permanent one. What earns the flip is on the same tab, and what that measurement still needs is written there rather than assumed.
The resolver never promotes a combo itself. Repeat combos surface as candidates with their shipment counts; a human names the package and that is what makes the row high-confidence. The approval step is the safeguard, and it is precisely what ShipStation’s own SmartFill lacks — SmartFill learns from the last two labels including ones it wrote itself.
USPS Ground Advantage still has four sub-1-lb tiers — up to 4, 8, 12, 15.999 oz, rounding up — and at Commercial they currently carry the same price. The screen renders that as “these cells happen to be equal”, never as “there is one price under a pound”: the structure is permanent, the equality is this year’s data. ShipStation gives Commercial Base plus additional discounts, and below-Commercial cards may still be ounce-priced — so the matrix must be able to show them differing tomorrow.
The only cliff that costs money at Commercial is 16.000 oz, drawn as a rule across the table.
A listing-time estimate has no destination, so it uses a zone blended from our own shipment history, named on the readout. The blend is a volume-weighted mean of the prices, not of the zone numbers — postage is not linear in zone. Until the history pull (S12 ①) exists it shows a fixed representative zone labelled provisional: the rejected option used honestly as a placeholder rather than silently as the design.
MR‑089 drew a Show catalog product names toggle that swapped the row label for a brand, and flagged the two rows that would decide it. They decide it against the toggle. SF-0007 has four products and no single name to borrow. Worse, SF-0012 and SF-0031 both build Avinol PM — the discontinued formula and its replacement, mid-cutover — so switching it on drew two adjacent rows with identical labels, which is the opposite of what the toggle was for.
Two controls replace it, because there were always two questions. What is this row is answered by the i beside the formula name, which lists every product built from it — all four fit in a tooltip and none of them has to become the label. Where is my row is answered by the catalog product filter above the grid. Pick Avinol PM and you get both formulas, named apart as Sleep Blend and Nighttime Formula — the collision the toggle created is the case the filter handles best. The row itself keeps manufacturing language, which is what packing is.
Paul, 2026-09-15, having had the resolver explained: “I don’t really understand why we need it. It’s just giving us transparency into the calculation.” A Tested row’s boxes were chosen by the warehouse, so the only rows with a calculation worth reading are Estimated ones. So the rail and its edge tab are gone, and How this was estimated sits under the product name on Estimated rows only. It opens a drawer with one column per pack size, because the question is about the whole row, not one cell: contents, volume, the lighter boxes it passed over and why, how the box was picked, the box, its empty weight, total weight, tier, postage, and what ShipStation gets.
Passed over is the one line nothing else on the screen shows. It is also what you would check an estimate against: if a box on that list was actually the right one, the estimate is wrong for a reason you can name. Postage uses the blended zone only; the rail’s zone picker is not carried over.
Drawn on trial, and kept. Paul would have cut it if it did not earn its place; once it led with volume → package and compared Tested rows, he locked it in (2026-09-15). It stays on this page rather than a standalone mockup because it reads the page’s own calculation; a standalone copy would be a second resolve() to drift (RI‑854).
The box cells are the shared search dropdown (_picker.js) on both grids, names only — a Mailer / Box tag and the empty weight were drawn first and Paul took them off the same day. An unweighed box is listed and cannot be picked (RI‑853).
The drawer leads with volume → package, because that is the estimate (MR‑415, RI‑855). Paul: “the most important part is not really how we determined the weight … It’s really how we determined the cubic volume to packaging type … Everything else is just calculations.” So the drawer is now a fit table: every package, in the order the estimator tries them (lightest first, unweighed last), against every pack size, each cell saying chosen, fits (with how full), too small, mailer max, no mailers or not weighed. Weight and postage fold away underneath. A line above the table says the method is volume only today, because the table is only worth reading if the reader knows what it cannot see.
The same drawer grades the estimator on a Tested row (MR‑416, RI‑856). Paul’s idea: “if we open it for a tested product, we could see what the estimator suggests versus what we chose.” A Tested row carries Compare with the estimate. The estimator is re-run blind to that product’s own tests — otherwise it would borrow the warehouse’s answer and every column would match — and each pack size says Matched, Diverged (bigger or smaller, and the empty-weight difference) or no recorded choice, with a tally above the table. In the fit table the warehouse’s package is blue, the estimate’s green, and a package both chose says both. Cognitive Complex reads 4 of 6: the warehouse took a bubble mailer at × 2 where volume said poly, and a Box (6x4x4) at × 3 that the estimator can never pick because it is unweighed. Summed across every Tested row, this is the estimator’s accuracy — a better measure for the Send estimated packages switch than the label harvest, whose gaps are written on the Automations tab.
Combo Mapping has the same drawer, one column per combo (MR‑417, RI‑857). The volume row lists each member’s volume and the total; the mailer limit counts the combo’s units; any box-only member rules out every mailer. Two of the three tested combos diverge, both as a mailer estimated against a box chosen — which volume alone cannot see.
It already shows where the estimator is weak. Complexion MD’s single jar lands in a Box (8x4x4) at 15% full, because the two small boxes are unweighed and never tried. Marine Collagen Peptides × 12 was packed by a person into a Box (8x8x8) the volume figures call 121% full — the capacity numbers are low. Both are things to fix in the estimator, and neither was visible before.
This grid shipped on 2026-09-02 with pack sizes 1 / 2 / 3 / 5 / 12. The Shopify builder settled later that the store runs two shapes, 1 / 3 / 5 and 1 / 3 / 6, and the eBay account’s set is 1 / 2 / 3 / 5. Nobody reconciled them, so × 6 was a pack size the store sells and this screen could not map. The grid carries the union now. A pack size missing from a mapping grid is not a cosmetic gap — it is a bundle that ships with no answer.
The default filters are Pending + Estimated, and the tab badges count exactly what they show. That is the work queue: there is no reason to open this screen to look at rows somebody already tested. Paul asked for an alert so the bench knows there is something to test; the queue comes first and the alert follows it, because who gets told and how often needs a recipient model that lives on settings-users.html, and a per-event alert on a job that mints twenty SKUs at once is twenty alerts. Deliberately not invented here.
Paul asked for the catalog product dropdown to be a search with autocomplete, thumbnails and multi-select, the way the listing builder finds products, with a Clear. It is the listing builder’s catalog search — field, filtered list, thumbnail column — with picks as chips inside the field, and the list stays open so picking three is three clicks. Several picks show every row that makes any of them. It also matches the formula id and house name, because the row is the formula. Only Memopryl has a real photo in the sample images; the rest show initials, as the listing builder does for a product with no catalog photo (RI‑845).
It is the third copy of that search — both listing builders carry their own. It is logged as an extraction candidate rather than extracted here, because moving the two builders onto a module is a refactor of screens this round did not touch.
Basis gained an All chip (RI‑846). While All is on, clicking one basis narrows to it; turning the last one off falls back to All rather than an empty grid. The default is still Pending + Estimated. The how-to banner above Bundle Mapping is gone — Paul: a note for the requirements, not the screen. Its text is RI‑847.
Paul found an Estimated row could not be made Tested: the select was disabled, and the Record a tested pairing drawer that might have done it had no caller. He chose the lightest fix — flip the dropdown — on the condition that the flip records who and when. The signed-in user and the time show under the select, on both grids (RI‑848). The drawer is removed rather than wired up: a form per test was the option he turned down (RI‑850).
Pending cannot be marked Tested. Fix what blocks it first — a missing unit weight, a box that predates the weigh-on-add rule, a pack size nothing holds — and it becomes Estimated. The blocker is written under the disabled select so nobody has to hover to find it (RI‑849). The Combo Mapping banner moved to the requirements too (RI‑847).
One screen or three? Packages, rates and the mapping grids have different edit rhythms (rarely / imported / occasionally) but must visibly agree.
What happens to a product-keyed row when a formula is linked later? (MR‑348) Sourcing is set once, but whether an optional formula link can be added after creation is not settled. If it can, the product’s row would move under the formula’s — and its recorded tests, which describe the product’s own carton, may not hold for the formula’s profile. Carry them, re-key them, or drop them is undecided.
A shipment package and its tare are defined here and nowhere else. Everything downstream reads this table: the packing grid, the resolver, the listing builder’s Ship column, and the generated ShipStation rules. listing-builder.html still carries a four-row PKGS fixture of its own from before this screen existed — four packages against thirteen, an invented Box 6×6×6, and a tare wrong on every row (0.6 vs 0.21, 1.1 vs 0.42, 5.0 vs 6.52). MR-084 deleted its visible table; the data is still there and still wrong. It reads from here, or the margin quotes a shipping cost the warehouse’s postage does not support.
Paul, 2026-09-02: “we will never have shipment packaging without weight — if we’re gonna add it to the system we’re just gonna weigh it and it’s gonna be there.” So the empty weight is required on the add form: no draft state, no unknown option, nothing to save around. A package with no tare would silently short-pay every label that used it, and the cheapest place to prevent that is the one door packages come through.
This removes a whole category rather than handling it. The two unweighed boxes on the Packages tab are the last of the ones that predate the rule — a backfill queue that closes and cannot reopen, not a standing state the screen has to keep designing for. What survives as genuine uncertainty is entirely upstream: a formula with no unit weight, or a combo nothing fits.
Contents volume against package capacity, then the lightest package that fits — a mailer beats a box at equal fit, which is what the bench actually does. Two modelled constraints: a package with no weighed tare can never be auto-chosen (an override may still name one, and the missing tare then blocks the row), and a mailer holds two units, three for the large — past that you need a rigid box. That second one is a rule of thumb standing in for real packing, and it is exactly the sort of thing overrides exist to correct.
There is no automation-rules API, and this was tested rather than assumed. /v2/rules, /v2/automation_rules, /v2/shipping_rules and /v2/automations all answer 404 on our own account, and the V1 API’s eleven resources contain nothing like it. ShipEngine’s own docs say a rules endpoint is coming, with no date — the same shape as the package-tare feature request open since 2021. So rule built is a person ticking a box, and it has to be.
The half that is checkable is the half that breaks silently. GET /v2/packages returns all 13 of our custom packages, names matching this table character for character, and carries no weight field at all — which is exactly why the tare lives here and a rule adds it. The name is the rule’s join key, so a rename here stops its rule matching anything with no error anywhere; that check runs automatically and the tab says when it last did.
Material cost per package is empty on purpose — the source sheet’s Cost column was blank, and MR-064 needs it for margin. Editing a package, importing a rate card, and the history pull behind the blend are all stubs. So is the estimate accuracy figure: the harvest is real and measured (14,575 labels), the three things standing between it and a percentage are written on the card, and no number is shown until they are settled.