Packing API
Everything here lives under /api/_action/p2lab-stockly/packaging/ and authenticates like any other
admin API call. Examples assume a bearer token in Authorization.
Resolve
Section titled “Resolve”
GET packaging/resolve?productIds=<comma separated>ACL:p2lab_stockly_packaging_profile:read· at most 100 ids per call
The one endpoint most integrations need. It answers which values will be used for an article and which level supplied them.
{ "products": [ { "productId": "0191f1…", "packagingWeightKg": 0.06, "packagingWeightSource": "profile", "ownBox": false, "ownBoxSource": "global", "dimensionsMm": [300, 200, 100], "dimensionsSource": "product", "dunnageFactor": 1.15, "preferredMaterialId": "0191f2…", "winningProfileId": "0191f3…", "matchedProfileIds": ["0191f3…", "0191f4…"], "rankCollision": false, "estimated": true } ]}| Field | Notes |
|---|---|
*Source | product, parent, profile, global, none |
dimensionsMm | [length, width, height], or null when nobody knows |
matchedProfileIds | winner first, then the rules it beat |
rankCollision | two rules of equal rank claimed it; the answer is stable but arbitrary |
estimated | at least one value was inherited or assumed rather than measured |
Common error. packagingWeightKg: null with packagingWeightSource: "none" means nobody recorded one. Do
not render that as 0.00 kg; the distinction is the reason the field exists.
Coverage
Section titled “Coverage”
GET packaging/coverage?page=1&limit=50ACL:p2lab_stockly_packaging_profile:read
{ "summary": { "total": 4812, "missingDimensions": 3140, "missingWeight": 210, "ready": 1672, "materials": 8, "profiles": 5, "hasGlobalProfile": true }, "gaps": [ { "productId": "0191f1…", "productNumber": "SW10001", "name": "Wall paint, white, 5 l", "shipments": 412, "hasWeight": true, "hasDimensions": false, "hasOwnRow": false } ]}gaps is ordered by shipments over the last six months, descending. That ordering is the useful
part: it turns the task of fixing five thousand articles into fixing the forty that ship every day.
hasGlobalProfile: false is worth alerting on. Without a catch-all rule, articles outside every
profile get no packing data at all.
Collisions
Section titled “Collisions”
GET packaging/collisions?limit=50ACL:p2lab_stockly_packaging_profile:read
{ "collisions": [ { "productId": "0191f1…", "productNumber": "SW10001", "name": "Wall paint, white, 5 l", "matchRank": 100, "profiles": [ { "id": "0191f3…", "name": "Paints 5 l" }, { "id": "0191f4…", "name": "Fragile goods" } ] } ]}Normally empty. A non-empty list means two rules of the same rank claim the same article, so the winner is decided by an internal comparison rather than by the merchant.
Variance
Section titled “Variance”
GET packaging/variance?limit=50&minVarianceKg=0.05ACL:p2lab_stockly_parcel:read
{ "variance": [ { "productId": "0191f1…", "productNumber": "SW10001", "name": "Wall paint, white, 5 l", "parcels": 37, "avgVarianceKg": 0.42, "maxVarianceKg": 0.61, "avgEstimatedKg": 5.9 } ]}Only parcels containing a single kind of article are counted; in a mixed box a miss cannot be attributed to any one of them. Positive means the box turned out heavier than predicted.
Writing packing data
Section titled “Writing packing data”There is no write endpoint here on purpose: packing data is ordinary DAL, so the standard admin API already covers it with less to learn and less to break.
POST /api/p2lab-stockly-packaging-material{ "code": "K30", "label": "Carton 300x200x150", "type": "box", "innerLengthMm": 300, "innerWidthMm": 200, "innerHeightMm": 150, "tareKg": 0.28, "maxPayloadKg": 20, "active": true}POST /api/p2lab-stockly-packaging-profile{ "name": "Paints 5 l", "matchType": "property", "matchId": "<property_group_option id>", "matchRank": 100, "packagingWeightKg": 0.06}POST /api/p2lab-stockly-product-packaging{ "productId": "0191f1…", "packagingWeightKg": 0.12, "ownBox": true}Traps when writing
Section titled “Traps when writing”- One row per article in
p2lab_stockly_product_packaging. The unique key enforces it, and an upsert keyed onproductIdis the appropriate write. - Leave a field out rather than sending
0orfalseunless that value is intended. Omission inherits; a value decides. matchType: nullis the global row. Keep exactly one.- Equal
matchRankvalues are legal and reported rather than rejected; see collisions. Use distinct ranks.