Skip to content

Integrating with Stockly

Stockly is built to be driven from outside. A carrier module, an ERP that owns the weights, a desktop label tool, a program driving a printer the server cannot see, a 3PL that does its own cartonization, a system that books goods onto shelves. None of them needs a change in Stockly to work with it.

There are four ways in. Choosing the right one decides the shape of the whole integration.

MethodUse it whenRuns
Named eventsyour code lives in the same Shopware installationin-process
HTTP endpointsyour code lives somewhere elseover the admin API
DAL entitiesyou are pushing master data, or reading for a reportover the admin API or in-process
Extension pointsyou are adding a capability: a carrier, a split strategyin-process
Your taskStart at
put goods in a bin, take them out, move themPutting stock in a bin
ask what is on the shelvesReading stock
count, seed a warehouse, drain one into anotherCorrections and bulk jobs
be told when something changesEvents
decide something instead of StocklyPolicy hooks
you already own the stock figures elsewhereExternal writes
ship, pack, printthis page, and Packing API

Whatever the task, start with the stock model: almost every integration bug in this module’s history was a misunderstanding of which number is derived from which.

String-named, so a listener never has to import a class of ours, which is the purpose of the naming: a module that breaks when Stockly is absent is a dependency, not an integration.

EventMeaning
p2lab.stockly.parcels.planasks for a proposal; writes nothing
p2lab.stockly.parcels.commitstores a split
p2lab.stockly.parcel.trackingreports a tracking code for one box
p2lab.stockly.parcel.registereda box received a tracking code
p2lab.stockly.parcel.faileda booking failed, with the reason
p2lab.stockly.label.printhands a finished label to the print queue; answers with a jobId
p2lab.stockly.label.preferred_formatasks which format to order from the carrier
p2lab.stockly.print.job.printeda job printed; arguments jobId, printerId
p2lab.stockly.print.job.faileda job failed; arguments jobId, error

The last two are reports rather than questions: they carry their data in the event’s arguments, like the rest of this list, but nothing is read back out of them.

Facts the module publishes afterwards are named the other way, with an underscore after the vendor (p2lab_stockly.…), because they are Shopware business events rather than hooks to be answered. They span demand, backorders, goods, stock, work, shipping, returns and integrity, and have a catalogue of their own.

Most of the facts are also available in the Flow Builder, so a merchant can hang a mail, a tag or a state change on them without anybody writing code.

All under /api/_action/p2lab-stockly/, authenticated like any admin API call and gated by the same ACL privileges as the screens.

Shippingshipping/…

MethodPathWhat it does
POSTshipping/parcels/plana proposal, writes nothing
POSTshipping/parcels/commitstores a split
POSTshipping/parcels/requestasks the carrier for shipments
POSTshipping/parcels/packedreports the packing screen finished
POSTshipping/parcels/of-orderthe stored boxes of an order
POSTshipping/parcels/{parcelId}/trackingreports a tracking code
POSTshipping/parcels/{parcelId}/cancelcancels one box
POSTshipping/parcels/closenothing more is going out for this order
GETshipping/profilesthe carriers and split strategies installed
GETshipping/batches/eligibleboxes waiting to be handed to a driver
POSTshipping/batches/closeseals a hand-over; returns a job id
GETshipping/batches/{batchId}/listthe sheet a driver signs, as a PDF

Packing datapackaging/…

MethodPathWhat it does
GETpackaging/resolvewhat the system will use for these articles, with provenance
GETpackaging/coveragehow complete the catalogue is, and what to fix first
GETpackaging/collisionsarticles two rules of equal rank both claim
GETpackaging/variancewhere prediction and scale disagree

Printingprinting/…

MethodPathWhat it does
POSTprint-jobtakes a finished label from outside Shopware; returns a jobId
POSTprinting/print-jobs/nexthands a station its next job; null when there is none
POSTprinting/print-jobs/{jobId}/ackconfirms a print or reports a failure
POSTprinting/jobs/{jobId}/retryretries a job
POSTprinting/jobs/{jobId}/cancelabandons a job
GETprinting/station-deviceswhether this installation has browser-driven printers
GETprinting/profilesthe printer types known, and their formats

Ordinary Shopware entities, so /api/search/… and /api/… work as they do anywhere else.

EntityHolds
p2lab_stockly_parcelone box: weight, tracking code, station, carton, notes
p2lab_stockly_parcel_linewhat is in the box
p2lab_stockly_parcel_batchone hand-over to a driver
p2lab_stockly_packaging_materialthe box catalogue
p2lab_stockly_packaging_profilethe bulk packing rules
p2lab_stockly_product_packagingper-article overrides
p2lab_stockly_packaging_ruleone rule per shipping method
p2lab_stockly_printerone device: how it connects, its format, its label size
p2lab_stockly_print_jobone print job: payload, status, attempts
p2lab_stockly_print_document_rulewhich Shopware document is caught, and where it goes
p2lab_stockly_print_rulewhich work prints what, for whom, and at what priority
p2lab_stockly_printer_defaultthe device a station, an operator or a warehouse defaults to
p2lab_stockly_print_mediathe stock types a device can be loaded with

The warehouse entities — shelves, bins, movements, lots, license plates — are listed in the stock model. Read them freely; write them through the endpoints.

Interfaces to implement, registered by a service tag.

InterfaceAdds
ParcelSplitStrategyInterfacea way of laying goods across boxes
CarrierProfileInterfacea carrier: its limits, products and validations
CarrierManifestGatewayInterfacetelling a carrier a hand-over closed
ExtractionEngineInterfacea document reading engine for import

The print queue is a path of its own, separate from shipping, and it takes three different kinds of program. The screens are covered in the printing chapter; what follows is what the code has to know.

A label from a plugin in the same installation

Section titled “A label from a plugin in the same installation”

p2lab.stockly.label.print accepts a finished label. The data travels in the event’s arguments, not its subject, because the answer comes back the same way:

$event = new GenericEvent(null, [
'payload' => $labelBytes,
'format' => 'zpl',
'source' => 'AcmeCarrier',
'externalId' => $parcelNumber,
'orderId' => $orderId,
'context' => $context,
]);
$dispatcher->dispatch($event, 'p2lab.stockly.label.print');
// Written back by the queue — for a tracking record, or to cancel the job later.
$jobId = $event->getArgument('jobId');

Before ordering the label from the carrier, ask p2lab.stockly.label.preferred_format which format to ask for: arguments role, format (the caller’s own default) and context, answer under format. It is the format of the device the job would be routed to, so a printer using ZPL receives ZPL instead of a PDF rasterised on the way.

The event never lets an exception escape. The caller is in the middle of creating a shipment at a carrier, where a parcel number has already been handed out; an unreachable printer must not turn a printing problem into a shipping problem.

POST /api/_action/p2lab-stockly/print-job does exactly what the event does, over the API:

{ "source": "acme-desktop", "externalId": "1Z999AA10123456784",
"format": "zpl", "payload": "<base64>", "copies": 1,
"orderId": "0189…", "printerId": "0189…" }

The answer is {"jobId": "…"}. A mediaId may stand in for payload. Without a printerId the queue routes the job itself, exactly as it does for a label from shipping.

A printer connected through the local agent or driven by the browser is not reachable from the server, so the station asks for work instead:

POST /api/_action/p2lab-stockly/printing/print-jobs/next
Authorization: Bearer <token>
Content-Type: application/json
{"stationId": "0189…", "drivers": ["agent"]}

The answer carries job and counts. job is often null, and that is the normal state: a packing station has nothing to print more often than it has something. A job carries format (zpl, epl, pdf), copies, driver, as much about the device as it takes to find it again (host, deviceUid, usbVendorId, usbProductId), and payload base64 encoded: decode it and write the bytes through unchanged.

Afterwards:

POST /api/_action/p2lab-stockly/printing/print-jobs/<jobId>/ack
Content-Type: application/json
{"printed": true}
{"printed": false, "error": "Printer offline"}

Three things cost the most time here:

Claiming is what fetching does. Without an ack the job sits on Sending until the timeout in print settings releases it, five minutes by default.

A reported failure is final. Nobody can take over a job bound to one station, so the queue does not retry it on its own; retrying is a decision for a person or for the calling program.

source together with externalId is the idempotency key. Accepting the same pair twice returns the existing jobId instead of a second label, so retrying after a timeout is safe. The caller cannot tell the difference, which is intended.

Privileges: p2lab_stockly_print_job:create to hand over a label, p2lab_stockly_print_job:update for the agent loop, p2lab_stockly_printer:read for printing/station-devices.

Null is not zero. Everywhere in packing data, an absent number means “ask the next level”, and a zero is a claim. Pushing 0 for a packing weight states that the article ships unpackaged, and nothing further down corrects it.

Provenance travels with numbers. packaging/resolve reports not only what a value is but where it came from: product, parent, profile, global, or none. Integrations that surface that last one to their own users find bad data in days rather than quarters.

The stock ledger is written to, never corrected. A movement row is never edited; a correction is a new row with its own reason. Everything the warehouse records about why a bin holds four is made of rows, which is why nothing may silently change one.