myparcelnl/sdk
Official MyParcel NL PHP SDK for creating shipments, managing labels, tracking, and handling returns via the MyParcel API. Offers a clean, object-oriented interface for integrating Dutch parcel carriers into Laravel or plain PHP applications.
Install and pin. composer require myparcelnl/sdk — there is no Laravel service provider, no config file, and no facade shipped with the package, so treat it as a plain library you wire into the container yourself. Pin an exact minor version in composer.json; the public namespace layout has changed across majors (older releases live under MyParcelNL\Sdk\src\..., newer ones drop the src segment), so an unpinned ^ constraint on a major boundary will silently break every use statement in your app.
Where to look first, in this order:
README.md / examples — the only narrative documentation that exists; it covers the happy path for a single consignment.src/Model/Consignment/ — the consignment classes are the real API surface. Read AbstractConsignment (or the interface it implements) end-to-end; every setter you'll call daily is declared there.src/Helper/MyParcelCollection.php — the batch object that actually talks to the API (setApiKey, createConcepts, getLabels, setLatestData).The minimal flow is always the same four beats: build a consignment → set the API key on the collection → create the concept → fetch the label. There is no single createShipment() call; shipment creation and label retrieval are two separate round-trips, and the label call is where the PDF comes back.
First use case: a Dutch domestic PostNL parcel.
PostNLConsignment, set recipient name, street + number, postcode (1234AB, no space), city, country, and the sender block.setReferenceIdentifier() to your order ID — this is the only thing that makes reconciliation possible later, because you can't search MyParcel by your own identifier otherwise.MyParcelCollection, call setApiKey($key), then createConcepts(). The collection now holds MyParcel's shipment IDs; call getLabels() and write the returned bytes to storage/app/labels/{order}.pdf.createConcepts(), persist the shipment ID and barcode (track_trace) on your own order row. If the process dies between concept creation and label download, that row is how you recover.Put an adapter between your app and the SDK. Because there's no framework glue, the natural move is a MyParcelService bound in a service provider, taking an API key from config('services.myparcel.key'). Every controller, job, and listener talks to your service; only the service imports MyParcelNL\Sdk\*. This costs one class and buys you the ability to mock the boundary and to survive the next namespace reshuffle.
Model the shipment lifecycle as a state machine on your own table. The SDK has nouns (consignment, collection) and no persistence. Give orders columns like myparcel_shipment_id, myparcel_barcode, label_path, and a status enum (pending → concept → labelled → delivered). Every SDK call is then a guarded transition rather than a fire-and-forget call, which is what prevents double-labelling.
Create concepts in a queued job, not in the request. Label generation is the slowest part of the flow and the most likely to time out. A CreateMyParcelShipment job with $tries = 3 and a ShouldBeUnique lock on the order ID is the pattern that works. Keep the label PDF in object storage and store the path — never re-download a label on page render.
Batch through the collection, not a loop. If you're shipping ten orders, add ten consignments to one MyParcelCollection and call createConcepts() once. The collection issues a single multi-shipment request, which is meaningfully faster and gets you consistent behaviour when one row is invalid (the collection reports which consignments failed rather than throwing on the first).
Treat delivery options as a cache-first, checkout-only problem. The pickup-location and delivery-option lookups are a separate API surface from shipment creation, keyed by postcode + house number + carrier. Don't call them on every page load. Resolve them when the user has a complete address, cache by carrier:postcode:number for a day or so, and re-validate the chosen option right before createConcepts() — availability drifts and a stale pickup point will be rejected server-side.
Store the barcode, build the track-and-trace URL yourself. getTrackTraceUrl()-style helpers exist, but the durable artefact is the barcode. Render the consumer-facing link from your own route so you can change wording, add branding, or fall back to a generic carrier URL if MyParcel is down.
Test your adapter, not the SDK. The SDK constructs its own HTTP client internally, so there's no injection point for Http::fake(). The practical approach is a contract test against your MyParcelService interface plus a small set of recorded real responses (one successful concept, one validation rejection, one label PDF) as fixtures. Do one manual smoke run against the sandbox before each release.
createConcepts() is not idempotent. There is no client-supplied dedupe key on the SDK side unless you set a reference identifier and check for existing shipments first. A retried job without a ShouldBeUnique lock will happily create a second real shipment and bill you for a second label. Lock first, then call.getLabels() returns binary/base64 PDF data. It is not JSON, not a URL, and not safe to return response()->json(). Write it straight to disk. The single most common first-day bug is a mojibake response in a controller.1234AB (uppercase, no space) and split the number off the street before setting the address. If you pass 1234 AB or Dam 1-2, the SDK will accept it and MyParcel will reject the concept with a terse message.AbstractConsignment declares a broad set of options but each carrier supports a subset. Setting a Saturday-delivery flag on a carrier that doesn't offer it may not error until the API call. Validate against the carrier subclass's capability list before you offer the option in your UI.config/services.php, assert it in your adapter's constructor, and log which environment each shipment was created against.setLatestData(), track-and-trace, and label re-download are safe to retry with backoff. Concept creation and label generation are side-effecting; retry those only behind your own idempotency guard.NOASSERTION licence — check the actual licence text before shipping commercially). The adapter boundary described above isn't ceremony; it's your exit ramp to raw HTTP calls against the documented MyParcel API if the SDK stalls.How can I help you explore Laravel packages today?