sashalenz/vchasno-kasa-api
Vchasno.Kasa API — Laravel Package
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.
Related Packages
Laravel notification channel for the TurboSMS REST API
Powerful PHP database abstraction layer (DBAL) with many features for database s...