Skip to content

Examples

Six recurring patterns of integration. Each one names the method it uses, the calls in order, and the most common error.


Context. A shipping provider with an API. The integration needs their labels, their limits and their end-of-day close, without Stockly holding any provider-specific code.

Integration method. Extension points, in-process.

Implementation steps.

  1. Implement CarrierProfileInterface and tag the service. The carrier then appears in the carrier select on every packaging rule, with its own limits and its own validations.
  2. Listen to p2lab.stockly.parcels.commit, or let the merchant’s rule trigger a booking, and call the provider once per parcel.
  3. Report each tracking code back with p2lab.stockly.parcel.tracking. Stockly writes it to the order’s delivery, which is where the shipping confirmation mail reads it from.
  4. If the provider has an end-of-day close, implement CarrierManifestGatewayInterface and declare maxParcelsPerCall(). Stockly chunks the hand-over to that number.

Common error. Booking twice. A parcel that already carries a tracking code must never be re-requested: the shipment exists and the merchant has paid for it. Stockly guards this with isRequestable(), which has to be read before calling out; a retry loop that ignores it produces two labels for one shipment.

A second common error. Declaring limits the contract does not have. Limits are contractual as often as technical: the same carrier is 31.5 kg for one merchant and 20 kg for another whose staff may not lift more. Supply the carrier’s defaults and let the merchant’s rule override them.


Context. The merchant already runs a small program that talks to the carrier and drives the label printer. That program keeps its role, and Stockly is told the outcome.

Integration method. HTTP, polling.

Implementation steps.

  1. Poll /api/search/p2lab-stockly-parcel for boxes with no tracking code:

    { "filter": [
    { "type": "equals", "field": "trackingCode", "value": null },
    { "type": "equalsAny", "field": "status", "value": ["planned", "packed"] }
    ],
    "associations": { "order": {}, "lines": {} } }
  2. Book the label in the external program.

  3. Report it: POST shipping/parcels/{parcelId}/tracking.

  4. At the end of the day, read GET shipping/batches/eligible and POST shipping/batches/close. The close returns a jobId, not a result: it runs in the background, and the PDF the driver signs is at shipping/batches/{batchId}/list.

Common error. Treating batches/close as synchronous. A day’s hand-over is thousands of writes and a carrier API that takes thirty shipments at a time; the endpoint returns a job id immediately and the tool has to poll for the outcome.

Operational note. Filter by station too if the merchant runs more than one packing bench; otherwise the tool’s end-of-day close signs for boxes still standing at another bench. The field is floorElementId on the parcel, and shipping/batches/close takes it as a scope.


Context. Article master data lives in the ERP, and it is better than what is in Shopware.

Integration method. DAL, on a schedule.

Implementation steps.

  1. Push weights and dimensions into the Shopware product itself: weight, length, width, height. Stockly reads the native fields; it keeps no second copy.
  2. Push packaging overhead into p2lab_stockly_product_packaging, keyed by productId.
  3. If the ERP works in packing classes rather than per article, create one p2lab_stockly_packaging_profile per class and let the property match do the work: three hundred articles covered by one row that never has to be synchronised again.
  4. After a sync, read GET packaging/coverage and log summary.missingDimensions. It is the cheapest possible regression test on the export itself.

Common error. Writing zeros. An ERP that exports “0.000” for an unknown packaging weight states that the article ships unpackaged, and no rule underneath corrects it. Omit the field, or send null.

A second common error. Variants. A variant that inherits its weight from its parent stores NULL, and an importer that corrects this by copying the parent’s value onto every variant turns one row of master data into a thousand rows nobody will maintain.


Context. The warehouse belongs to a logistics provider, which decides the boxes and reports them afterwards.

Integration method. HTTP, push.

Implementation steps.

  1. Do not call parcels/plan; this path reports a result rather than asking for one.

  2. POST shipping/parcels/commit with the boxes as they really were:

    { "orderId": "0191…",
    "source": "acme-3pl",
    "parcels": [
    { "sequence": 1,
    "weightKg": 6.42,
    "packagingMaterialId": "0191f2…",
    "volumetricWeightKg": 4.10,
    "note": "outer carton dented, retaped",
    "lines": [{ "lineItemId": "0191…", "quantity": 2 }] }
    ] }
  3. Report tracking codes as in case 2.

Common error. Re-sending a plan after labels exist. commit replaces the split, but boxes that already carry a tracking code are frozen and left exactly as they are, so a re-send does not corrupt booked shipments, but it does not update them either. If the 3PL repacked a booked box, cancel it and send a new one.

Recommended practice. Send packagingMaterialId even if the catalogue was built only for this. It is what later lets the merchant ask whether the 3PL is using sensible box sizes, a question with a direct effect on shipping cost.


Context. Somebody wants to know whether the packing data is decaying, without opening the admin.

Integration method. HTTP, read-only.

What to read, and what it means.

CallAlert when
GET packaging/coveragesummary.hasGlobalProfile is false — the whole feature is inert
GET packaging/coverageready / total falls week over week — new articles arriving unmeasured
GET packaging/collisionsthe list is non-empty — two rules tie and nobody chose
GET packaging/varianceavgVarianceKg above a threshold — an article’s data is wrong

Common error. Alerting on the size of gaps. It lists everything unmeasured and stays permanently long in a shop with a long tail. The number that matters is the top of it: the articles with high shipments and hasDimensions: false.


Context. A screen at the bench that a packer drives with a scanner, in a front end of the integrator’s own.

Integration method. HTTP.

Implementation steps.

  1. Resolve the scanned code: GET /api/_action/p2lab-stockly/packing/scan/resolve?code=…. It tries the order-barcode prefix, then order numbers, then document numbers, and never guesses: an ambiguous code comes back as ambiguous with the candidates.
  2. Ask for a split: POST shipping/parcels/plan.
  3. Show it, let the packer change it, then POST shipping/parcels/commit with floorElementId set to the bench. That stamp is the only moment anybody knows which of three benches handled the order.
  4. POST shipping/parcels/packed when the run finishes, so the merchant’s booking trigger fires.

Common error. Losing the bench. floorElementId is stamped once and never overwritten with null, precisely so a later save from a screen that does not know the bench cannot erase it; but if the kiosk never sends it in the first place, the station reports stay empty, and the omission surfaces only when somebody reads them.

Operational note. Blocked orders come back from the scan resolver with a reason (cancelled, unpaid), not as “not found”. Show the reason, so nobody is sent back to the scanner for an answer that will not change.


  • Read the four ways in before choosing one; most integrations that caused trouble chose the wrong one first.
  • Never write 0 in place of “unknown”.
  • Surface *Source: "none" to the integration’s own users. It is how bad data gets found.
  • Treat batches/close as a job, not a call.
  • If the shop has more than one packing bench, carry floorElementId everywhere possible.