Skip to content

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.

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
}
]
}
FieldNotes
*Sourceproduct, parent, profile, global, none
dimensionsMm[length, width, height], or null when nobody knows
matchedProfileIdswinner first, then the rules it beat
rankCollisiontwo rules of equal rank claimed it; the answer is stable but arbitrary
estimatedat 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.

GET packaging/coverage?page=1&limit=50 ACL: 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.

GET packaging/collisions?limit=50 ACL: 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.

GET packaging/variance?limit=50&minVarianceKg=0.05 ACL: 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.

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
}
  • One row per article in p2lab_stockly_product_packaging. The unique key enforces it, and an upsert keyed on productId is the appropriate write.
  • Leave a field out rather than sending 0 or false unless that value is intended. Omission inherits; a value decides.
  • matchType: null is the global row. Keep exactly one.
  • Equal matchRank values are legal and reported rather than rejected; see collisions. Use distinct ranks.