kolirt/laravel-ukrposhta
Laravel wrapper for the Ukrposhta API that simplifies integrating Ukrainian postal services into your app. It provides a configured client with ready-made request handling and easy access to endpoints for tracking shipments, addresses, and delivery data, cutting HTTP boilerplate.
The package is a thin Laravel wrapper around the Ukrposhta (Ukrainian national postal operator) HTTP API, so the fastest path to a working call is: install, publish config, set a token, make one read-only request.
composer require kolirt/laravel-ukrposhta
php artisan vendor:publish --provider="Kolirt\Ukrposhta\UkrposhtaServiceProvider"
Then in .env:
UKRPOSHTA_TOKEN=your-bearer-token
Laravel auto-discovery registers the service provider and facade alias, so a first smoke test is a read-only lookup rather than a write:
use Kolirt\Ukrposhta\Facades\Ukrposhta;
$status = Ukrposhta::tracking()->status('UA123456789UA');
Where to look first, in order:
config/ukrposhta.php — base URL, token key, timeout, and any sandbox/production switch. This is the single most important file; almost every "it doesn't work" question traces back here.First realistic use case: order-status polling. Read-only tracking is idempotent, needs no waybill lifecycle knowledge, and validates your token, endpoint, and error handling in one call before you attempt anything that creates a shipment.
Wrap the facade in your own service. Do not call Ukrposhta:: from controllers or Blade. Introduce App\Services\Ukrposhta\ShipmentService that depends on the package client via constructor injection. The package is at 1.x with an external API behind it; a single seam means a future breaking release touches one class instead of forty call sites.
Bind the client in a service provider, not by facade resolution. If the underlying class is bound as a singleton, resolve it through the container so tests can $this->app->instance(...) a fake. This is the difference between a testable integration and one you mock at the HTTP layer for every unit test.
Test at the HTTP layer, not the class layer. The client ultimately uses Laravel's HTTP client, so Http::fake(['api.ukrposhta.ua/*' => Http::response($fixture)]) lets you assert both the outgoing payload shape and your response mapping. Capture a handful of real responses (success, validation failure, auth failure) into tests/Fixtures/ on day one — they become the contract you pin against when 1.0.x bumps arrive.
Cache the reference data aggressively. City, street, and post-office (department) dictionaries change on the order of weeks, not seconds. A Cache::remember('ukrposhta:departments:'.$cityId, now()->addDay(), ...) wrapper cuts latency and, more importantly, keeps you clear of any per-key rate limits during address autocomplete. Never cache live tracking status or waybill creation results.
Push write operations onto the queue. Waybill creation, label PDF retrieval, and status changes are network-bound and failure-prone. A job with $tries = 3, backoff() of a minute or two, and failed() writing to a shipment_errors table gives you visibility the synchronous path never will. Keep the idempotency key (your order/shipment UUID) in the payload so a retry after a timeout doesn't create a duplicate waybill.
Model the shipment lifecycle explicitly. Store the returned waybill identifier (barcode) alongside your own order record, plus a status column and last_synced_at. Then a scheduled command polls only shipments in non-terminal states on a staggered interval. Do not poll everything on every tick — tracking endpoints are the ones most likely to throttle you.
Keep a translation layer for statuses. The API returns status codes/strings in Ukrainian or as opaque numeric codes depending on the endpoint. Map them to your own enum (Created, InTransit, Arrived, Delivered, Returned) at the boundary, and keep the raw payload in a JSON column for forensics.
Validate before you call. Form requests for the shipment payload (recipient name length, phone format +380XXXXXXXXX, weight > 0, declared value ≥ 0) save you a round trip and produce far better user errors than the API's field messages.
Log the correlation, not the payload. Log request ID, waybill barcode, endpoint, and duration at info; log the full request/response at debug behind a dedicated channel. Shipment payloads contain names, phones, and addresses — treat them as PII and keep them out of your default log stack.
config/ukrposhta.php is read at boot. Changing UKRPOSHTA_TOKEN in .env on a server that has run config:cache does nothing until you re-cache. Add php artisan config:cache to your deploy script and stop debugging the token.200 with an error body, or a business-level error code inside a successful envelope. Always inspect the decoded body for an error indicator before treating a call as done — a bare $response->successful() check will silently produce waybills that were never created.connect_timeout to a couple of seconds and an overall timeout of 5–10s in config, and catch ConnectionException distinctly from a validation rejection so you can retry the former and surface the latter.JSON_UNESCAPED_UNICODE is optional but handy when diffing logs) — Cyrillic names mangled into \uXXXX are a common "why is the recipient's name wrong" report.+380 prefix, and trim trailing whitespace on street names. Most rejected waybills are rejected on address formatting, not on anything the API documents well.local-only guard, or default the base URL to the test host when APP_ENV !== 'production'.Ukrposhta::macro('...') or a small subclass for one-off endpoints (e.g. an internal reporting call) over forking. If it is not macroable, bind your own decorator around it — never patch vendor/.Http::globalMiddleware or the package's config option to dump the raw request/response once, or hit the endpoint with curl and the same bearer token. If curl works and the package fails, the difference is almost always the base URL, a missing Content-Type: application/json, or an un-normalised token (leading Bearer pasted into the .env value is a classic).^1.0 in composer.json, run your recorded-fixture tests after every bump, and read the diff of the facade/client class rather than the changelog when moving between minor versions.How can I help you explore Laravel packages today?