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.
Four ways in
Section titled “Four ways in”| Method | Use it when | Runs |
|---|---|---|
| Named events | your code lives in the same Shopware installation | in-process |
| HTTP endpoints | your code lives somewhere else | over the admin API |
| DAL entities | you are pushing master data, or reading for a report | over the admin API or in-process |
| Extension points | you are adding a capability: a carrier, a split strategy | in-process |
Where to start
Section titled “Where to start”| Your task | Start at |
|---|---|
| put goods in a bin, take them out, move them | Putting stock in a bin |
| ask what is on the shelves | Reading stock |
| count, seed a warehouse, drain one into another | Corrections and bulk jobs |
| be told when something changes | Events |
| decide something instead of Stockly | Policy hooks |
| you already own the stock figures elsewhere | External writes |
| ship, pack, print | this 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.
Named events
Section titled “Named events”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.
| Event | Meaning |
|---|---|
p2lab.stockly.parcels.plan | asks for a proposal; writes nothing |
p2lab.stockly.parcels.commit | stores a split |
p2lab.stockly.parcel.tracking | reports a tracking code for one box |
p2lab.stockly.parcel.registered | a box received a tracking code |
p2lab.stockly.parcel.failed | a booking failed, with the reason |
p2lab.stockly.label.print | hands a finished label to the print queue; answers with a jobId |
p2lab.stockly.label.preferred_format | asks which format to order from the carrier |
p2lab.stockly.print.job.printed | a job printed; arguments jobId, printerId |
p2lab.stockly.print.job.failed | a 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.
HTTP endpoints
Section titled “HTTP endpoints”All under /api/_action/p2lab-stockly/, authenticated like any admin API call and gated by the same
ACL privileges as the screens.
Shipping — shipping/…
| Method | Path | What it does |
|---|---|---|
POST | shipping/parcels/plan | a proposal, writes nothing |
POST | shipping/parcels/commit | stores a split |
POST | shipping/parcels/request | asks the carrier for shipments |
POST | shipping/parcels/packed | reports the packing screen finished |
POST | shipping/parcels/of-order | the stored boxes of an order |
POST | shipping/parcels/{parcelId}/tracking | reports a tracking code |
POST | shipping/parcels/{parcelId}/cancel | cancels one box |
POST | shipping/parcels/close | nothing more is going out for this order |
GET | shipping/profiles | the carriers and split strategies installed |
GET | shipping/batches/eligible | boxes waiting to be handed to a driver |
POST | shipping/batches/close | seals a hand-over; returns a job id |
GET | shipping/batches/{batchId}/list | the sheet a driver signs, as a PDF |
Packing data — packaging/…
| Method | Path | What it does |
|---|---|---|
GET | packaging/resolve | what the system will use for these articles, with provenance |
GET | packaging/coverage | how complete the catalogue is, and what to fix first |
GET | packaging/collisions | articles two rules of equal rank both claim |
GET | packaging/variance | where prediction and scale disagree |
Printing — printing/…
| Method | Path | What it does |
|---|---|---|
POST | print-job | takes a finished label from outside Shopware; returns a jobId |
POST | printing/print-jobs/next | hands a station its next job; null when there is none |
POST | printing/print-jobs/{jobId}/ack | confirms a print or reports a failure |
POST | printing/jobs/{jobId}/retry | retries a job |
POST | printing/jobs/{jobId}/cancel | abandons a job |
GET | printing/station-devices | whether this installation has browser-driven printers |
GET | printing/profiles | the printer types known, and their formats |
DAL entities
Section titled “DAL entities”Ordinary Shopware entities, so /api/search/… and /api/… work as they do anywhere else.
| Entity | Holds |
|---|---|
p2lab_stockly_parcel | one box: weight, tracking code, station, carton, notes |
p2lab_stockly_parcel_line | what is in the box |
p2lab_stockly_parcel_batch | one hand-over to a driver |
p2lab_stockly_packaging_material | the box catalogue |
p2lab_stockly_packaging_profile | the bulk packing rules |
p2lab_stockly_product_packaging | per-article overrides |
p2lab_stockly_packaging_rule | one rule per shipping method |
p2lab_stockly_printer | one device: how it connects, its format, its label size |
p2lab_stockly_print_job | one print job: payload, status, attempts |
p2lab_stockly_print_document_rule | which Shopware document is caught, and where it goes |
p2lab_stockly_print_rule | which work prints what, for whom, and at what priority |
p2lab_stockly_printer_default | the device a station, an operator or a warehouse defaults to |
p2lab_stockly_print_media | the 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.
Extension points
Section titled “Extension points”Interfaces to implement, registered by a service tag.
| Interface | Adds |
|---|---|
ParcelSplitStrategyInterface | a way of laying goods across boxes |
CarrierProfileInterface | a carrier: its limits, products and validations |
CarrierManifestGatewayInterface | telling a carrier a hand-over closed |
ExtractionEngineInterface | a document reading engine for import |
Printing
Section titled “Printing”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.
A label from a program outside Shopware
Section titled “A label from a program outside Shopware”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 program that does the printing
Section titled “A program that does the printing”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/nextAuthorization: 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>/ackContent-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.
Three rules that apply throughout
Section titled “Three rules that apply throughout”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.
- The stock model — what a quantity is attached to
- Putting stock in a bin — the warehouse write path
- Events — the full catalogue
- Packing API in detail — request and response shapes
- Print queue — what becomes of the job you handed over
- Examples — worked integrations, end to end