daaner/novaposhta
Laravel wrapper for the Nova Poshta API: look up cities and warehouses, calculate delivery costs, and manage shipments from your PHP app. A clean integration that saves you from raw HTTP calls when building checkout and shipping flows.
Version 1.4 is a small, additive release for the daaner/NovaPoshta wrapper (Nova Poshta API 2.0). Nothing is removed or renamed, so you can upgrade with composer update daaner/novaposhta and keep your existing InternetDocument chains working.
Minimal setup:
composer require daaner/novaposhta
php artisan vendor:publish --tag=novaposhta # config file
NOVAPOSHTA_API_KEY=your_key_here
The wrapper is a thin, fluent layer over the API: you pick an API "model" (InternetDocument, Address, ContactPerson, Common, …), set fields with set* methods, then call the API action (save, update, delete, get*).
Where to look first
src/ — each API model lives in its own class; the set* methods are just field assignments, so if the API supports a field, a setter almost certainly exists (or is trivially added).InternetDocument for the new setAfterpaymentOnGoodsCost() alongside the other COD/settlement setters.First use case — create a ТТН (waybill)
use Daaner\NovaPoshta\NovaPoshta;
$doc = (new NovaPoshta())->internetDocument()
->setPayerType('Sender')
->setPaymentMethod('Cash')
->setDateTime(date('d.m.Y'))
->setCargoType('Cargo')
->setWeight(2)
->setServiceType('WarehouseWarehouse')
->setSeatsAmount(1)
->setDescription('Order #1001')
->setCost(1500)
// 1.4: cash on delivery for the goods value
->setAfterpaymentOnGoodsCost(1500)
->setCitySender($senderCityRef)
->setSender($senderRef)
->setSenderAddress($senderWarehouseRef)
->setContactSender($senderContactRef)
->setSendersPhone('380501112233')
->setCityRecipient($recipientCityRef)
->setRecipient($recipientRef)
->setRecipientAddress($recipientWarehouseRef)
->setContactRecipient($recipientContactRef)
->setRecipientsPhone('380671112233')
->save();
If that call returns a Ref and a IntDocNumber, your integration baseline is done — everything after that is reference-data management and status tracking.
Resolve refs, never hardcode them. Cities, warehouses, and counterparties are UUIDs that differ between the sandbox and production, and change when NP reorganises its network. Build a small lookup service that caches them:
$cities = Cache::remember('np:cities', now()->addDay(), fn () =>
(new NovaPoshta())->address()->getCities()
);
Match by name + area on write, store the Ref on your own model, and only re-resolve on an API errors response.
Wrap the fluent chain behind a service. The chain is expressive but noisy inside controllers. Build one WaybillService::create(Order $order): WaybillData that owns the field mapping (Order → set*) and returns a DTO with Ref / IntDocNumber. This is the single place you touch when NP adds required fields.
New in 1.4 — afterpayment (COD). setAfterpaymentOnGoodsCost() is a InternetDocument setter, so it belongs in the same pre-save() chain. Treat the amount as part of your order snapshot: recompute it server-side from the order total, never from request input, and re-send it via update() if the order total changes before dispatch.
Tracking statuses. 1.4 adds status support (tracking/state handling). API 2.0 has no webhooks for documents, so the reliable pattern is a scheduled poller:
// app/Console/Commands/PollNovaPoshta.php — every 15–30 min
foreach (Waybill::whereNotIn('status_code', [9, 10, 11])->cursor() as $waybill) {
$status = (new NovaPoshta())->internetDocument()
->getStatusDocuments($waybill->ttn);
$waybill->update([
'status_code' => data_get($status, '0.StatusCode'),
'status_raw' => $status, // keep the full payload
]);
}
Persist the raw payload and the numeric StatusCode. Map codes to your own enum/labels in application code rather than relying on the package's constants — NP adds and renames statuses faster than any wrapper releases.
Testing and CI. The wrapper speaks HTTP underneath, so Http::fake() / Guzzle mock handlers give you deterministic tests with canned success: true and success: false payloads. Point the config at the sandbox key in phpunit.xml and never let the production key leak into CI.
Bulk and queued work. Creating dozens of waybills in a request will hit timeouts and NP rate limits. Push each save() into a job, add ->onQueue('novaposhta') with a low concurrency (redis queue, 2–5 workers) so you don't spike the API, and use retryUntil + backoff() because transient NP errors are common.
Printing and lifecycle. Once you have the Ref, use the package's print/PDF methods to render the label and wire delete() to an order-cancellation listener — orphaned ТТН in NP's system are billable and annoying to clean up manually.
AfterpaymentOnGoodsCost has preconditions. NP only accepts afterpayment when the recipient is the payer of the delivery and the payment method is cash. If you set setPayerType('Sender') and afterpayment, expect a success: false response rather than an exception. Set payer/afterpayment together deliberately, and mirror the same rules in your own validation so users get a readable error.{ success: false, errors: [...] } envelope. Every call site needs a success check and a log of the raw response; otherwise failed ТТН silently become null refs in your DB. Wrap the package in one adapter that throws a domain exception on success: false.380XXXXXXXXX, 13 digits, no +, no spaces, no leading 0 short form. Validation failures here are the #1 cause of "recipient not found" errors.InternetDocument, getStatusDocuments, AfterpaymentOnGoodsCost — casing matters and is inconsistent across the API (some PascalCase, some camelCase). Copy names from the official docs; don't guess from the setter name.Ref into a seeder that also runs in production.set* methods are a one-line addition to the field array, and a small PR upstream beats maintaining a divergent copy.save() creates; re-running it on a retry produces duplicate waybills. Guard with an idempotency check on your own Waybill record (Ref already stored → skip) before retrying a job.How can I help you explore Laravel packages today?