sashalenz / vchasno-kasa-api
Laravel client for Вчасно.Каса Orders API (deferred fiscal receipts / ПРРО)
Requires
- php: ^8.4
- illuminate/contracts: ^11.0|^12.0|^13.0
- illuminate/http: ^11.0|^12.0|^13.0
- spatie/laravel-data: ^4.11
- spatie/laravel-package-tools: ^1.16
Requires (Dev)
- larastan/larastan: ^2.9|^3.0
- laravel/pint: ^1.14
- nunomaduro/collision: ^8.0
- orchestra/testbench: ^9.0||^10.0
- pestphp/pest: ^3.0
- pestphp/pest-plugin-arch: ^3.0
- pestphp/pest-plugin-laravel: ^3.0
- phpstan/extension-installer: ^1.3
This package is auto-updated.
Last update: 2026-04-26 09:43:11 UTC
README
Laravel package for integrating with the Vchasno.Kasa Orders API.
Supports deferred fiscal receipts (ETTN) via Nova Poshta, Ukrposhta and Meest, as well as direct cash and IBAN payments.
Requirements
- PHP 8.4+
- Laravel 11 / 12 / 13
Installation
composer require sashalenz/vchasno-kasa-api
Publish the config file:
php artisan vendor:publish --tag="vchasno-kasa-config"
Add variables to your .env:
VCHASNO_KASA_TOKEN=your-api-token VCHASNO_KASA_RRO_FN=1234567890
Configuration
// config/vchasno-kasa.php return [ 'token' => env('VCHASNO_KASA_TOKEN'), 'rro_fn' => env('VCHASNO_KASA_RRO_FN'), 'base_url' => env('VCHASNO_KASA_URL', 'https://kasa.vchasno.ua/api/v1'), 'timeout' => 30, 'retry' => [ 'times' => 3, 'sleep' => 150, ], ];
| Option | Description |
|---|---|
token |
API token from the Vchasno cabinet: Settings → API |
rro_fn |
Fiscal number of the software cash register (PRRO). Visible in the cabinet or via GET /api/v1/rro |
base_url |
Orders API base URL (change only if necessary) |
Usage
Via Facade
use Sashalenz\VchasnoKasaApi\Facades\VchasnoKasa;
Via DI / app()
use Sashalenz\VchasnoKasaApi\VchasnoKasaApi; $api = app(VchasnoKasaApi::class);
Orders API
1. Deferred receipt — Nova Poshta (COD)
The primary use case: the customer pays upon parcel pickup. The receipt is fiscalized automatically when the customer collects the shipment.
use Sashalenz\VchasnoKasaApi\Facades\VchasnoKasa; use Sashalenz\VchasnoKasaApi\Data\Requests\Orders\CreatePostalOrderData; use Sashalenz\VchasnoKasaApi\Data\Requests\Orders\GoodItemData; use Sashalenz\VchasnoKasaApi\Enums\PostalType; $order = VchasnoKasa::orders()->createPostal( new CreatePostalOrderData( ttn: '59000123456789', // Nova Poshta tracking number rroFn: config('vchasno-kasa.rro_fn'), tag: "order-{$order->id}-{$shipment->id}", // unique idempotency key postalType: PostalType::NovaPoshta, goods: [ new GoodItemData( name: 'BMW E46 front left headlight', quantity: 1.0, price: 350000, // in kopecks (3500.00 UAH) code: 'SKU-12345', ), ], totalSum: 350000, // in kopecks ) ); echo $order->id; // order ID in Vchasno echo $order->status; // OrderStatus::Created
2. Deferred receipt — Ukrposhta / Meest
use Sashalenz\VchasnoKasaApi\Enums\PostalType; $order = VchasnoKasa::orders()->createPostal( new CreatePostalOrderData( ttn: '0312345678901', rroFn: config('vchasno-kasa.rro_fn'), tag: "order-{$order->id}-ukrposhta", postalType: PostalType::UkrPoshta, // or PostalType::Meest goods: [...], totalSum: 120000, ) );
3. Direct cash / card payment
use Sashalenz\VchasnoKasaApi\Data\Requests\Orders\CreateOrderData; use Sashalenz\VchasnoKasaApi\Enums\PaymentType; $order = VchasnoKasa::orders()->create( new CreateOrderData( rroFn: config('vchasno-kasa.rro_fn'), tag: "cash-order-{$order->id}", goods: [ new GoodItemData(name: 'Rear bumper', quantity: 1.0, price: 180000), ], totalSum: 180000, paymentType: PaymentType::Cash, // or PaymentType::Card ) );
4. IBAN bank transfer
use Sashalenz\VchasnoKasaApi\Data\Requests\Orders\CreateIbanOrderData; $order = VchasnoKasa::orders()->createIban( new CreateIbanOrderData( rroFn: config('vchasno-kasa.rro_fn'), tag: "iban-payment-{$payment->id}", goods: [...], totalSum: 250000, iban: 'UA123456789012345678901234567', comment: 'Payment for order #12345', ) );
5. Get order status
$order = VchasnoKasa::orders()->get($fiscalOrderId); if ($order->status->isSuccess()) { $yourOrder->update([ 'fiscal_receipt_url' => $order->receiptUrl, 'fiscal_code' => $order->fiscalCode, 'fiscalized_at' => $order->fiscalizedAt, ]); }
6. List orders (cursor pagination)
use Sashalenz\VchasnoKasaApi\Data\Requests\Orders\ListOrdersData; use Sashalenz\VchasnoKasaApi\Enums\OrderStatus; $result = VchasnoKasa::orders()->list( new ListOrdersData( pageSize: 50, status: OrderStatus::Fiscalized, dateFrom: '2026-04-01T00:00:00Z', dateTo: '2026-04-30T23:59:59Z', ) ); foreach ($result->items as $order) { echo "{$order->id}: {$order->status->label()}"; } // Next page if ($result->hasMore) { $next = VchasnoKasa::orders()->list( new ListOrdersData(cursor: $result->nextCursor, pageSize: 50) ); }
7. Cancel an order
$cancelled = VchasnoKasa::orders()->cancel($fiscalOrderId); echo $cancelled->status->label(); // "Cancelled"
Receipts API
Get a receipt by ID
$receipt = VchasnoKasa::receipts()->get($receiptId); echo $receipt->receiptUrl; // PDF URL echo $receipt->fiscalCode; // fiscal code from the tax authority
Poll for new receipts
Useful for a background job that syncs fiscalization statuses:
use Sashalenz\VchasnoKasaApi\Data\Requests\Receipts\ListReceiptsData; $cursor = cache('vchasno_receipts_cursor'); do { $result = VchasnoKasa::receipts()->list( new ListReceiptsData(cursor: $cursor, pageSize: 100) ); foreach ($result->items as $receipt) { Order::where('fiscal_order_id', $receipt->orderId) ->update([ 'fiscal_receipt_url' => $receipt->receiptUrl, 'fiscal_code' => $receipt->fiscalCode, 'fiscalized_at' => $receipt->createdAt, ]); } $cursor = $result->nextCursor; } while ($result->hasMore); cache()->put('vchasno_receipts_cursor', $cursor);
Enums
OrderStatus
| Case | Value | Description |
|---|---|---|
Created |
0 |
Created, awaiting processing |
WaitingForPickup |
10 |
NP: waiting for customer pickup |
Delivered |
11 |
NP: delivered, fiscalization in progress |
Fiscalized |
101 |
✅ Successfully fiscalized |
Cancelled |
2000 |
Cancelled |
Returned |
2001 |
Returned |
Expired |
2002 |
Expired |
$status->isSuccess(); // true if Fiscalized $status->isFinal(); // true if Fiscalized / Cancelled / Returned / Expired $status->isPending(); // true if Created / WaitingForPickup / Delivered $status->label(); // 'Fiscalized'
PostalType
| Case | Value | paymentType() |
|---|---|---|
NovaPoshta |
nova_poshta |
PaymentType::NovaPoshta (20) |
UkrPoshta |
ukr_poshta |
PaymentType::PostalOther (15) |
Meest |
meest |
PaymentType::PostalOther (15) |
PaymentType
| Case | Value | Description |
|---|---|---|
Cash |
1 |
Cash |
Card |
2 |
Card / terminal |
PostalOther |
15 |
Ukrposhta / Meest COD |
NovaPoshta |
20 |
Nova Poshta COD |
Error handling
use Sashalenz\VchasnoKasaApi\Exceptions\VchasnoKasaRequestException; try { $order = VchasnoKasa::orders()->createPostal($data); } catch (VchasnoKasaRequestException $e) { if ($e->shouldRetry()) { // res_action=1 or 2: request can be retried // res_action=2 (collision): retry with the same tag retry($job); } if ($e->needsDataFix()) { // res_action=3: bad request data, retrying will not help Log::error('Vchasno fiscal error', [ 'message' => $e->getMessage(), 'res_action' => $e->getResAction(), ]); } }
res_action codes
| Value | Method | Action |
|---|---|---|
0 |
— | Success |
1 |
shouldRetry() |
Retry the request |
2 |
isCollision() |
Collision — retry with the same tag |
3 |
needsDataFix() |
Bad data — fix the request payload |
Idempotency (tag)
The tag field is a required unique key for every order. If a request is repeated with the same tag, Vchasno returns the existing order instead of creating a new one.
Recommended format:
// Nova Poshta shipment $tag = "order-{$order->id}-{$shipment->id}"; // Cash payment $tag = "cash-{$order->id}"; // IBAN payment $tag = "iban-{$payment->id}";
Testing
composer test
composer analyse # PHPStan composer format # Pint
License
MIT. See LICENSE for details.