PHPackages                             blob-solutions/vcr-am-sdk - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [API Development](/categories/api)
4. /
5. blob-solutions/vcr-am-sdk

ActiveLibrary[API Development](/categories/api)

blob-solutions/vcr-am-sdk
=========================

Official PHP SDK for the VCR.AM Virtual Cash Register API — fiscalize sales, refunds, and prepayments through Armenia's State Revenue Committee.

v0.6.0(2w ago)01641ISCPHPPHP ^8.2

Since May 4Pushed 2w agoCompare

[ Source](https://github.com/blob-am/vcr-am-sdk-php)[ Packagist](https://packagist.org/packages/blob-solutions/vcr-am-sdk)[ Docs](https://vcr.am)[ RSS](/packages/blob-solutions-vcr-am-sdk/feed)WikiDiscussions main Synced 1w ago

READMEChangelogDependencies (48)Versions (6)Used By (1)

VCR.AM PHP SDK
==============

[](#vcram-php-sdk)

[![Packagist Version](https://camo.githubusercontent.com/3449fbd5df44c1129a54024bc1023614889dd76b760efc90cba50670bbeb4b7a/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f626c6f622d736f6c7574696f6e732f7663722d616d2d73646b2e737667)](https://packagist.org/packages/blob-solutions/vcr-am-sdk)[![PHP Version Require](https://camo.githubusercontent.com/a62369524149d12142ad7a5ec78a60fa6a8c610329e5a4002fc76e31dc6bbee3/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f646570656e64656e63792d762f626c6f622d736f6c7574696f6e732f7663722d616d2d73646b2f706870)](https://packagist.org/packages/blob-solutions/vcr-am-sdk)[![License](https://camo.githubusercontent.com/8f1800f6dcf7ad71813518c8ccfe78099183776b84a1786fcdd91847871ae648/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f6c2f626c6f622d736f6c7574696f6e732f7663722d616d2d73646b2e737667)](LICENSE)[![CI](https://github.com/blob-am/vcr-am-php/actions/workflows/ci.yml/badge.svg)](https://github.com/blob-am/vcr-am-php/actions/workflows/ci.yml)

Official PHP SDK for the [VCR.AM](https://vcr.am) Virtual Cash Register API. Fiscalize sales, refunds, and prepayments through Armenia's State Revenue Committee — without touching XML, PSR-7, or wire-format quirks.

A native sibling to the [TypeScript SDK](https://github.com/blob-am/vcr-am-sdk). Same endpoints, same error semantics, same response validation philosophy — adapted to idiomatic PHP 8.2+.

Status
------

[](#status)

> **Pre-release.** API is being developed in lockstep with the TypeScript SDK. While the package is on `0.x`, every minor release may introduce breaking changes — pin tightly until `1.0`. See [CHANGELOG.md](CHANGELOG.md) for release notes.

> ⚠️ **Do not use v0.1.0.** The very first release shipped with the wrong auth header and is rejected by every production request. Pin `^0.1.1` or later.

Requirements
------------

[](#requirements)

- PHP **8.2 or newer** (8.1 hit security EOL on 2025-12-31)
- Composer 2.x
- A VCR.AM account and API key — sign up at [vcr.am](https://vcr.am)

Installation
------------

[](#installation)

```
composer require blob-solutions/vcr-am-sdk
```

The package ships with sensible defaults (Guzzle 7 as the PSR-18 HTTP client, `nyholm/psr7` as the PSR-7/PSR-17 implementation). If your application already uses different implementations, the SDK will discover and reuse them via `php-http/discovery`.

Quick start
-----------

[](#quick-start)

Register a sale of two loaves of bread, paid in cash:

```
use BlobSolutions\VcrAm\Input\Buyer;
use BlobSolutions\VcrAm\Input\CashierId;
use BlobSolutions\VcrAm\Input\Department;
use BlobSolutions\VcrAm\Input\Offer;
use BlobSolutions\VcrAm\Input\RegisterSaleInput;
use BlobSolutions\VcrAm\Input\SaleAmount;
use BlobSolutions\VcrAm\Input\SaleItem;
use BlobSolutions\VcrAm\Unit;
use BlobSolutions\VcrAm\VcrClient;

$client = new VcrClient(apiKey: $_ENV['VCR_AM_API_KEY']);

$response = $client->registerSale(new RegisterSaleInput(
    cashier: CashierId::byDeskId('desk-1'),
    items: [
        new SaleItem(
            offer: Offer::existing('sku-bread'),
            department: new Department(5),
            quantity: '2',
            price: '750',
            unit: Unit::Piece,
        ),
    ],
    amount: new SaleAmount(cash: '1500'),
    buyer: Buyer::individual(),
));

echo "Sale {$response->saleId} fiscalised as {$response->fiscal}\n";
echo "Public receipt: https://vcr.am/{$response->urlId}\n";
```

Every input DTO is constructor-validated, so malformed payloads fail at the call site — not after a round-trip to the SRC. All decimal monetary values are passed as strings to preserve precision over the wire (see [Monetary precision](#monetary-precision) below).

Endpoints
---------

[](#endpoints)

### `listCashiers(): list`

[](#listcashiers-listcashierlistitem)

Returns every cashier registered for the account, with a per-language map of names.

```
$cashiers = $client->listCashiers();

foreach ($cashiers as $cashier) {
    $hyName = $cashier->name['hy']->content ?? '(no Armenian name)';
    echo "{$cashier->deskId} (#{$cashier->internalId}): {$hyName}\n";
}
```

### `createCashier(CreateCashierInput $input): CreateCashierResponse`

[](#createcashiercreatecashierinput-input-createcashierresponse)

Provisions a new cashier under the account. The `password` is the terminal PIN (4–8 digits) — held in a private property and redacted from `var_dump` / `print_r`.

```
use BlobSolutions\VcrAm\Input\CreateCashierInput;
use BlobSolutions\VcrAm\Input\LocalizedName;
use BlobSolutions\VcrAm\LocalizationStrategy;

$response = $client->createCashier(new CreateCashierInput(
    name: new LocalizedName(
        value: ['hy' => 'Աննա Գրիգորյան', 'en' => 'Anna Grigoryan'],
        localizationStrategy: LocalizationStrategy::Transliteration,
    ),
    password: '1234',
));

echo "Cashier #{$response->id} provisioned (deskId={$response->deskId})\n";
```

### `createDepartment(CreateDepartmentInput $input): CreateDepartmentResponse`

[](#createdepartmentcreatedepartmentinput-input-createdepartmentresponse)

Creates a department (tax-regime-bound bucket of offers).

```
use BlobSolutions\VcrAm\Input\CreateDepartmentInput;
use BlobSolutions\VcrAm\Input\LocalizedName;
use BlobSolutions\VcrAm\LocalizationStrategy;
use BlobSolutions\VcrAm\TaxRegime;

$response = $client->createDepartment(new CreateDepartmentInput(
    taxRegime: TaxRegime::Vat,
    title: new LocalizedName(
        value: ['hy' => 'Հացաբուլկեղեն', 'ru' => 'Хлебобулочные', 'en' => 'Bakery'],
        localizationStrategy: LocalizationStrategy::Translation,
    ),
    externalId: 'erp-dept-bakery',
));

echo "Department #{$response->department} created — {$response->message}\n";
```

### `createOffer(CreateOfferInput $input): CreateOfferResponse`

[](#createoffercreateofferinput-input-createofferresponse)

Adds a standalone offer (product or service) to the account catalogue.

```
use BlobSolutions\VcrAm\Input\CreateOfferInput;
use BlobSolutions\VcrAm\Input\Department;
use BlobSolutions\VcrAm\Input\LocalizedName;
use BlobSolutions\VcrAm\LocalizationStrategy;
use BlobSolutions\VcrAm\OfferType;
use BlobSolutions\VcrAm\Unit;

$response = $client->createOffer(new CreateOfferInput(
    type: OfferType::Product,
    classifierCode: '01.01.01',
    title: new LocalizedName(
        value: ['hy' => 'Հաց', 'en' => 'Bread'],
        localizationStrategy: LocalizationStrategy::Translation,
    ),
    defaultMeasureUnit: Unit::Piece,
    defaultDepartment: new Department(5),
    externalId: 'sku-bread',
));

echo "Offer #{$response->offerId} created\n";
```

### `listOffers(?string $externalId, ?OfferType $type, bool $includeArchived): list`

[](#listoffersstring-externalid-offertype-type-bool-includearchived-listofferlistitem)

Reads the offer catalogue (up to 500 rows). Handy for checking whether an offer already exists (by `externalId`) before creating it. Archived offers are excluded unless `$includeArchived` is `true`.

```
use BlobSolutions\VcrAm\OfferType;

$existing = $client->listOffers(externalId: 'sku-bread');
if ($existing === []) {
    // ... create it
}

$products = $client->listOffers(type: OfferType::Product);
```

### `getOffer(int $offerId): OfferListItem`

[](#getofferint-offerid-offerlistitem)

Reads a single offer by its internal numeric id.

```
$offer = $client->getOffer(7);
echo "{$offer->id}: {$offer->classifierCode} ({$offer->defaultMeasureUnit})\n";
```

### `updateOffer(int $offerId, OfferTitle $title): OfferListItem`

[](#updateofferint-offerid-offertitle-title-offerlistitem)

Renames an offer's title. Affects only future receipts — already-issued receipts keep the title they were created with, and the SRC fiscal record is unchanged.

```
use BlobSolutions\VcrAm\Input\OfferTitle;
use BlobSolutions\VcrAm\LocalizationStrategy;

$offer = $client->updateOffer(7, OfferTitle::localized(
    ['hy' => 'Լատտե', 'en' => 'Latte'],
    LocalizationStrategy::Translation,
));
```

### `searchClassifier(string $query, OfferType $type, Language $language): list`

[](#searchclassifierstring-query-offertype-type-language-language-listclassifiersearchitem)

Fuzzy-searches the SRC classifier taxonomy. Useful for populating an autocomplete in an offer-creation UI. `Language::Multi` is rejected — pick a concrete language.

```
use BlobSolutions\VcrAm\Language;
use BlobSolutions\VcrAm\OfferType;

$matches = $client->searchClassifier('bread', OfferType::Product, Language::English);

foreach ($matches as $match) {
    echo "{$match->code} — {$match->title}\n";
}
```

### `registerSale(RegisterSaleInput $input): RegisterSaleResponse`

[](#registersaleregistersaleinput-input-registersaleresponse)

Fiscalizes a sale. See [Quick start](#quick-start) above for the basic shape. A more elaborate example with a business buyer, an inline new offer, and an emailed receipt:

```
use BlobSolutions\VcrAm\Input\Buyer;
use BlobSolutions\VcrAm\Input\CashierId;
use BlobSolutions\VcrAm\Input\Department;
use BlobSolutions\VcrAm\Input\Offer;
use BlobSolutions\VcrAm\Input\OfferTitle;
use BlobSolutions\VcrAm\Input\RegisterSaleInput;
use BlobSolutions\VcrAm\Input\SaleAmount;
use BlobSolutions\VcrAm\Input\SaleItem;
use BlobSolutions\VcrAm\Input\SendReceiptToBuyer;
use BlobSolutions\VcrAm\Language;
use BlobSolutions\VcrAm\LocalizationStrategy;
use BlobSolutions\VcrAm\OfferType;
use BlobSolutions\VcrAm\Unit;

$response = $client->registerSale(new RegisterSaleInput(
    cashier: CashierId::byInternalId(42),
    items: [
        new SaleItem(
            offer: Offer::createNew(
                externalId: 'consult-1h',
                title: OfferTitle::localized(
                    content: ['hy' => 'Խորհրդատվություն, 1 ժամ', 'en' => 'Consultation, 1 hour'],
                    localizationStrategy: LocalizationStrategy::Translation,
                ),
                type: OfferType::Service,
                classifierCode: '70.22.11',
                defaultMeasureUnit: Unit::Hour,
                defaultDepartment: new Department(2),
            ),
            department: new Department(2),
            quantity: '1',
            price: '50000',
            unit: Unit::Hour,
        ),
    ],
    amount: new SaleAmount(nonCash: '50000'),
    buyer: Buyer::businessEntity(
        '01234567',
        new SendReceiptToBuyer('billing@example.am', Language::Armenian),
    ),
));
```

**Payment: `amount` or `autoSettle`.** A sale settles by *exactly one* of these (the constructor enforces it):

- `amount: new SaleAmount(...)` — explicit AMD per tender, as above.
- `autoSettle: new AutoSettle(AutoSettleTender::Cash)` — the VCR computes the AMD cart total and charges the whole of it to that one tender. Zero-tap, and the only sane option for a foreign-currency sale (you can't know the AMD total up front). Prefer the `RegisterSaleInput::withAmount()` / `withAutoSettle()` named constructors for a clear call site.

**Foreign-currency sales (HO-234-N).** Set `currency` on each item and price that item in the foreign currency; the VCR converts every line to AMD server-side at the previous-business-day CBA rate. The fiscal receipt is always AMD. All foreign-priced items in one sale must share the same currency, and mixing AMD with foreign lines is rejected. Preview the rate with [`getExchangeRate()`](#getexchangeratestring-currency-exchangerate).

```
use BlobSolutions\VcrAm\AutoSettleTender;
use BlobSolutions\VcrAm\Input\AutoSettle;
use BlobSolutions\VcrAm\Input\Buyer;
use BlobSolutions\VcrAm\Input\CashierId;
use BlobSolutions\VcrAm\Input\Department;
use BlobSolutions\VcrAm\Input\Offer;
use BlobSolutions\VcrAm\Input\RegisterSaleInput;
use BlobSolutions\VcrAm\Input\SaleItem;
use BlobSolutions\VcrAm\Unit;

$response = $client->registerSale(RegisterSaleInput::withAutoSettle(
    cashier: CashierId::byInternalId(42),
    items: [
        new SaleItem(
            offer: Offer::existing('sku-usd'),
            department: new Department(2),
            quantity: '1',
            price: '10',        // priced in USD
            unit: Unit::Piece,
            currency: 'USD',    // VCR converts to AMD at the CBA rate
        ),
    ],
    autoSettle: new AutoSettle(AutoSettleTender::Cash),
    buyer: Buyer::individual(),
));
```

### `getSale(int $saleId): SaleDetail`

[](#getsaleint-saleid-saledetail)

Reads back a previously-registered sale, including all items, refunds, and the cashier snapshot.

```
$sale = $client->getSale(4711);

echo "Sale #{$sale->id} — total cash {$sale->cashAmount}\n";

foreach ($sale->items as $item) {
    echo "  - srcId={$item->srcId} qty={$item->quantity} @ {$item->price}\n";
}

foreach ($sale->refunds as $refund) {
    echo "  refund: cash={$refund->cashAmount}\n";
}
```

### `registerSaleRefund(RegisterSaleRefundInput $input): RegisterSaleRefundResponse`

[](#registersalerefundregistersalerefundinput-input-registersalerefundresponse)

Refunds a previously-registered sale — full or partial.

**Full refund** (omit `items`):

```
use BlobSolutions\VcrAm\Input\CashierId;
use BlobSolutions\VcrAm\Input\RegisterSaleRefundInput;
use BlobSolutions\VcrAm\RefundReason;

$response = $client->registerSaleRefund(new RegisterSaleRefundInput(
    cashier: CashierId::byDeskId('desk-1'),
    saleId: 4711,
    reason: RefundReason::CustomerRequest,
));
```

**Partial refund** by `srcId` of a specific sale item:

```
use BlobSolutions\VcrAm\Input\RefundAmount;
use BlobSolutions\VcrAm\Input\RefundItemInput;

$response = $client->registerSaleRefund(new RegisterSaleRefundInput(
    cashier: CashierId::byDeskId('desk-1'),
    saleId: 4711,
    reason: RefundReason::DefectiveGoods,
    reasonNote: 'Mouldy crust on one loaf',
    refundAmounts: new RefundAmount(cash: '750'),
    items: [
        new RefundItemInput(srcId: 999_001, quantity: '1'),
    ],
));
```

### `registerPrepayment(RegisterPrepaymentInput $input): RegisterPrepaymentResponse`

[](#registerprepaymentregisterprepaymentinput-input-registerprepaymentresponse)

Registers an advance payment from a buyer that will be redeemed against a future sale. Has no items — a prepayment is a single sum, not a basket.

```
use BlobSolutions\VcrAm\Input\Buyer;
use BlobSolutions\VcrAm\Input\CashierId;
use BlobSolutions\VcrAm\Input\PrepaymentAmount;
use BlobSolutions\VcrAm\Input\RegisterPrepaymentInput;

$response = $client->registerPrepayment(new RegisterPrepaymentInput(
    cashier: CashierId::byDeskId('desk-1'),
    amount: new PrepaymentAmount(nonCash: '20000'),
    buyer: Buyer::individual(),
));

echo "Prepayment #{$response->prepaymentId}, fiscal={$response->fiscal}\n";
```

`crn` and `fiscal` may be `null` if SRC fiscal issuance is pending — the prepayment is recorded either way and the SRC handshake retries asynchronously.

### `getPrepayment(int $prepaymentId): PrepaymentDetail`

[](#getprepaymentint-prepaymentid-prepaymentdetail)

Reads back a registered prepayment, including its (at most one) refund.

```
$prepayment = $client->getPrepayment(9001);

echo "Prepayment #{$prepayment->id} — cash={$prepayment->cashAmount}\n";

if ($prepayment->refund !== null) {
    echo "  refunded: cash={$prepayment->refund->cashAmount}\n";
}
```

### `registerPrepaymentRefund(RegisterPrepaymentRefundInput $input): RegisterPrepaymentRefundResponse`

[](#registerprepaymentrefundregisterprepaymentrefundinput-input-registerprepaymentrefundresponse)

Refunds a prepayment in full. There is no partial-refund variant — a prepayment is indivisible.

```
use BlobSolutions\VcrAm\Input\CashierId;
use BlobSolutions\VcrAm\Input\RegisterPrepaymentRefundInput;
use BlobSolutions\VcrAm\RefundReason;

$response = $client->registerPrepaymentRefund(new RegisterPrepaymentRefundInput(
    cashier: CashierId::byDeskId('desk-1'),
    prepaymentId: 9001,
    reason: RefundReason::CustomerRequest,
));
```

### `getExchangeRate(string $currency): ExchangeRate`

[](#getexchangeratestring-currency-exchangerate)

Previews the AMD conversion rate the VCR would apply to a foreign-currency sale registered now — the CBA mid-market rate published on the previous business day (HO-234-N). Read-only; it fiscalizes nothing. `$currency` is a 3-letter ISO 4217 code (case-insensitive); `AMD` is rejected server-side.

```
$rate = $client->getExchangeRate('USD');

$amdEquivalent = (float) $usdPrice * $rate->ratePerUnit;
echo "1 USD = {$rate->ratePerUnit} AMD (rate date {$rate->rateDate})\n";
```

Error handling
--------------

[](#error-handling)

Every call can raise one of three exceptions, all extending the abstract `BlobSolutions\VcrAm\Exception\VcrException`. Catch the base class to handle any SDK-level failure, or branch on the concrete subclass when you care about the cause:

```
use BlobSolutions\VcrAm\Exception\VcrApiException;
use BlobSolutions\VcrAm\Exception\VcrNetworkException;
use BlobSolutions\VcrAm\Exception\VcrValidationException;

try {
    $response = $client->registerSale($input);
} catch (VcrApiException $e) {
    // Server returned a non-2xx HTTP response.
    // $e->statusCode       — HTTP status (int)
    // $e->apiErrorCode     — SRC error code (string|null), e.g. 'INVALID_TIN'
    // $e->apiErrorMessage  — human-readable detail (string|null)
    // $e->rawBody          — full response body, in case envelope parsing failed
    // $e->request          — PSR-7 request (X-API-Key header redacted)
    // $e->response         — PSR-7 response
    if ($e->statusCode === 422 && $e->apiErrorCode === 'INVALID_TIN') {
        // Surface a friendly message to the cashier UI
    }

    throw $e;
} catch (VcrNetworkException $e) {
    // DNS / TLS / TCP / timeout failure before any HTTP response.
    // $e->getPrevious() is the underlying PSR-18 ClientExceptionInterface.
    // $e->request — PSR-7 request (X-API-Key redacted)
    // Safe to retry after a backoff, but be careful with non-idempotent endpoints.
    throw $e;
} catch (VcrValidationException $e) {
    // Server returned 2xx but the body did not match the expected schema.
    // Indicates a server-side wire-format change or upstream proxy corruption.
    // $e->detail   — Valinor mapping error or 'response body is not valid JSON: …'
    // $e->rawBody  — full response body
    // This is never a caller-side bug. File an issue.
    throw $e;
}
```

The `X-API-Key` header is stripped from the request preserved on every exception. APMs that auto-serialize exception state (Sentry, Bugsnag, Laravel's verbose handler) will not surface the API key in their breadcrumbs.

Configuration
-------------

[](#configuration)

```
use BlobSolutions\VcrAm\VcrClient;

$client = new VcrClient(
    apiKey: $apiKey,
    baseUrl: VcrClient::DEFAULT_BASE_URL,  // 'https://vcr.am/api/v1'
    httpClient: $myPsr18Client,            // optional — falls back to discovery
    requestFactory: $myPsr17Factory,       // optional — falls back to discovery
    streamFactory: $myPsr17Factory,        // optional — falls back to discovery
    logger: $myPsr3Logger,                 // optional — defaults to NullLogger
);
```

### PSR-3 logger

[](#psr-3-logger)

The SDK emits structured log lines for outbound requests, network failures, and 4xx/5xx responses. Pass any PSR-3 logger to capture them:

```
use Monolog\Logger;
use Monolog\Handler\StreamHandler;

$logger = new Logger('vcr-am');
$logger->pushHandler(new StreamHandler('php://stderr', Logger::DEBUG));

$client = new VcrClient(apiKey: $apiKey, logger: $logger);
```

Channels emitted:

LevelMessageContext`debug``VCR.AM request``method`, `url``warning``VCR.AM network failure``method`, `url`, `error``warning``VCR.AM API error``method`, `url`, `status`, `errorCode`, `errorMessage`, `rawBodyPreview` (≤500 bytes)The X-API-Key header is never logged.

### PSR-18 HTTP client

[](#psr-18-http-client)

The SDK auto-discovers a PSR-18 client via `php-http/discovery` (the bundled Guzzle 7 by default). To override — most commonly, to set timeouts or attach middleware — construct your own and inject it:

```
use GuzzleHttp\Client as Guzzle;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Middleware;
use GuzzleHttp\RequestOptions;

$stack = HandlerStack::create();
$stack->push(Middleware::retry(
    fn ($retries, $req, $res, $err) => $retries < 2 && $err !== null,
    fn ($retries) => 1000 * (2 ** $retries),
));

$http = new Guzzle([
    RequestOptions::CONNECT_TIMEOUT => 5,
    RequestOptions::TIMEOUT         => 30,
    'handler'                       => $stack,
]);

$client = new VcrClient(apiKey: $apiKey, httpClient: $http);
```

Any PSR-18 client works — Symfony's `symfony/http-client`, Laravel's `Illuminate\Http\Client\Factory` (with a small adapter), `php-http/curl-client`, etc.

### PSR-17 factories

[](#psr-17-factories)

The same `php-http/discovery` mechanism finds a PSR-17 request/stream factory (by default `nyholm/psr7`'s `Psr17Factory`). Override only if you need to keep a single PSR-17 implementation across your application — the SDK does not care which one it is, as long as it conforms to the standard.

Timeouts
--------

[](#timeouts)

PSR-18 has no portable per-request timeout primitive, so the SDK does not expose one. Configure the timeout on the HTTP client you pass in (or on the auto-discovered Guzzle instance via the bundled `guzzlehttp/guzzle` dependency):

```
use GuzzleHttp\Client as Guzzle;
use GuzzleHttp\RequestOptions;

$http = new Guzzle([
    RequestOptions::CONNECT_TIMEOUT => 5,   // seconds
    RequestOptions::TIMEOUT         => 30,  // seconds, total
]);

$client = new VcrClient(apiKey: $apiKey, httpClient: $http);
```

A bounded timeout is strongly recommended for fiscalization endpoints — an indefinite hang on a stalled SRC backend will block the calling thread.

Monetary precision
------------------

[](#monetary-precision)

Decimal fields on **request** types (`SaleAmount::$cash`, `SaleItem::$price`, etc.) are typed as `string` to preserve full precision over the wire. Only digits and at most one decimal point are accepted — no sign, no scientific notation. Constructor-time validation enforces the shape.

Decimal fields on **response** types (`SaleDetail::$cashAmount`, `Receipt::$total`, etc.) are typed as PHP `float` because the API serialises them as JSON numbers (Prisma `Float` columns). This mirrors the TypeScript SDK's `z.number()` shape. **Do not use these floats for reconciliation arithmetic** — re-fetch the receipt or compute against your own ledger of decimal-strings sent in. Equality comparisons on these floats are unsafe.

Idempotency and retries
-----------------------

[](#idempotency-and-retries)

The SDK does **not** retry failed requests automatically. Fiscalization endpoints are not guaranteed to be idempotent on the server side; a silent retry could double-register a sale.

If you need retries, attach a Guzzle (or other PSR-18) middleware that retries only on `VcrNetworkException`-class failures (DNS/TLS/timeout) and never on a `VcrApiException` — the latter means the server has already seen and rejected your request.

Compatibility
-------------

[](#compatibility)

SDK versionPHP versions tested`^0.x`8.2, 8.3, 8.4, 8.5Development
-----------

[](#development)

```
composer install
composer check    # format check + phpstan + tests
composer format   # apply Pint fixes
```

CI enforces `pint --test`, PHPStan max + strict + deprecation rules + phpunit extension, and a 100% line-coverage gate on PHP 8.5.

License
-------

[](#license)

ISC © Alex Kraiz, Blob Solutions

###  Health Score

42

—

FairBetter than 88% of packages

Maintenance97

Actively maintained with recent releases

Popularity14

Limited adoption so far

Community10

Small or concentrated contributor base

Maturity40

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~17 days

Total

5

Last Release

14d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/24254622?v=4)[Alex Kraiz](/maintainers/lobotomoe)[@lobotomoe](https://github.com/lobotomoe)

---

Top Contributors

[![lobotomoe](https://avatars.githubusercontent.com/u/24254622?v=4)](https://github.com/lobotomoe "lobotomoe (1 commits)")

---

Tags

phpsdkfiscalizationsrcvcrfiscalarmeniaarmeniancash-registervcr-amvirtual-cash-registertax-servicee-receiptblob-solutions

###  Code Quality

TestsPest

Static AnalysisPHPStan

Code StyleLaravel Pint

Type Coverage Yes

### Embed Badge

![Health badge](/badges/blob-solutions-vcr-am-sdk/health.svg)

```
[![Health](https://phpackages.com/badges/blob-solutions-vcr-am-sdk/health.svg)](https://phpackages.com/packages/blob-solutions-vcr-am-sdk)
```

###  Alternatives

[tempest/framework

The PHP framework that gets out of your way.

2.2k34.4k16](/packages/tempest-framework)[flow-php/flow

PHP ETL - Extract Transform Load - Data processing framework

85036.3k](/packages/flow-php-flow)[telnyx/telnyx-php

Official Telnyx PHP SDK — APIs for Voice, SMS, MMS, WhatsApp, Fax, SIP Trunking, Wireless IoT, Call Control, and more. Build global communications on Telnyx's private carrier-grade network.

36789.4k2](/packages/telnyx-telnyx-php)[sylius/sylius

E-Commerce platform for PHP, based on Symfony framework.

8.5k5.9M754](/packages/sylius-sylius)[shopware/core

Shopware platform is the core for all Shopware ecommerce products.

585.6M600](/packages/shopware-core)[typo3/cms

TYPO3 CMS is a free open source Content Management Framework initially created by Kasper Skaarhoj and licensed under GNU/GPL.

1.2k1.9M122](/packages/typo3-cms)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
