pashkevich / loyverse-sdk
A simple to use PHP SDK for the Loyverse API
Requires
- php: ^8.4
- guzzlehttp/guzzle: ^8.0
Requires (Dev)
- infection/infection: ^0.35
- laravel/pint: ^1.32
- mockery/mockery: ^1.6
- phpstan/phpstan: ^2.3
- phpunit/phpunit: ^13.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-10-28 06:21:40 UTC
README
To work with this package, firstly you must have a Loyverse account, and secondly you must create an API token through Loyverse itself.
- Documentation
- Installation
- Basic Usage
- Configuration
- OAuth
- Pagination
- Enums
- Data Transfer Objects
- Handling Errors
- Retries
- Webhook Signatures
- Managing Categories
- Managing Customers
- Managing Discounts
- Managing Employees
- Managing Inventory
- Managing Items
- Managing Merchants
- Managing Modifiers
- Managing Payment Types
- Managing Pos Devices
- Managing Receipts
- Managing Shifts
- Managing Stores
- Managing Suppliers
- Managing Taxes
- Managing Variants
- Managing Webhooks
- Fake Client
- Examples
- Upgrading
- Testing
- Contributing
- Security Vulnerabilities
- Credits
- License
Documentation
Installation
PHP 8.4 or newer and Guzzle 8 are required. To install the SDK in your project you need to require the package via composer:
composer require pashkevich/loyverse-sdk:^4.5
Basic Usage
You can create an instance of the SDK like so:
$loyverse = new Pashkevich\Loyverse\Loyverse(PERSONAL_ACCESS_TOKEN_HERE);
Using the Loyverse instance you may perform multiple actions as well as retrieve the different resources Loyverse's
API provides:
$page = $loyverse->categories(); $categories = $page['categories']; $cursor = $page['cursor'];
Every list method returns a page array with the resource list under its API key and a cursor for the next page:
[
'categories' => [/* Category objects */],
'cursor' => 'next-page-cursor', // or null when there are no more pages
]
Each category is represented by an instance of Pashkevich\Loyverse\Resources\Category,
this instance has multiple public properties like $id, $name, $color, and others.
You may also retrieve a single category using:
$category = $loyverse->category(CATEGORY_ID_HERE);
On multiple actions supported by this SDK you may need to pass some parameters, for example when creating a new category:
$category = $loyverse->createCategory([ 'name' => 'Fruits', 'color' => 'ORANGE', ]);
These parameters will be used in the POST request sent to Loyverse servers, you can find more information about the parameters needed for each action on Loyverse's official API documentation.
You can also set the desired timeout value. The timeout is applied to every HTTP request:
$loyverse->setTimeout(120)->createCategory(array $data);
Configuration
The client can be configured for tests, proxies or custom gateways:
$loyverse ->setBaseUri('https://api.loyverse.com/v1.0') ->setConnectTimeout(5) ->setTimeout(30) ->setHeader('X-Request-Id', 'request-id');
The SDK sends a User-Agent header with its version and the current PHP version. You can
override it with setHeader('User-Agent', '...').
OAuth
The SDK supports both personal access tokens and OAuth 2.0. Create an OAuth instance and
redirect the merchant to the authorization URL:
use Pashkevich\Loyverse\{Loyverse, OAuth, Scope}; $oauth = new OAuth(); $state = OAuth::generateState(); $url = $oauth->authorizationUrl( CLIENT_ID, REDIRECT_URI, $state, [Scope::ItemsRead, Scope::ReceiptsRead] );
Store $state in the session and compare it with the state query parameter in the
callback. After that, exchange the authorization code for an access token:
$token = $oauth->exchangeCode(CLIENT_ID, CLIENT_SECRET, $_GET['code'], REDIRECT_URI); $loyverse = new Loyverse($token->accessToken);
Refresh the token when it expires:
if ($token->isExpired(60) && $token->refreshToken !== null) { $token = $oauth->refreshToken(CLIENT_ID, CLIENT_SECRET, $token->refreshToken); }
For automatic refresh, configure a token store:
use Pashkevich\Loyverse\Auth\ArrayTokenStore; $store = new ArrayTokenStore(); $store->set($token); $loyverse = (new Loyverse())->withOAuth($oauth, CLIENT_ID, CLIENT_SECRET, $store);
The client refreshes expired tokens before a request and retries once after a 401
response. You can implement TokenStore to persist tokens in your application.
AccessToken exposes accessToken, tokenType, expiresIn, refreshToken, scope,
idToken, isExpired(), getScopes() and getIdTokenClaims(). ID token claims are decoded
without signature verification.
Pagination
You can iterate over all pages of a list endpoint with paginate(). The callback receives
the current query parameters and must return a page array:
foreach ($loyverse->paginate([$loyverse, 'items'], ['limit' => 250]) as $item) { echo $item->itemName; }
Or using a closure:
foreach ($loyverse->paginate(function (array $parameters) use ($loyverse) { return $loyverse->items($parameters); }, ['limit' => 250]) as $item) { echo $item->itemName; }
paginate() automatically passes the cursor returned by the previous page and stops when
the cursor is empty. It also stops if the API returns the same cursor twice.
If you prefer a page object, use the opt-in page() method:
$page = $loyverse->page('items', ['limit' => 250]); $page->items(); $page->cursor(); $page->hasMorePages(); $page->nextPage(); $page->toArray(); foreach ($page->lazy() as $item) { echo $item->itemName; }
Existing list methods still return arrays, so this is fully backward compatible.
Enums
Common API values are available as enums:
use Pashkevich\Loyverse\Enums\{DiscountType, ItemColor, ReceiptType, TaxType, WebhookEvent}; ReceiptType::Sale->value; TaxType::Included->value; DiscountType::FixedPercent->value; ItemColor::Green->value; WebhookEvent::ItemsUpdate->value;
Case names use PascalCase as required by PER Coding Style 3.1; ->value keeps the exact
string expected by the API. Each enum also provides values(). Scope lives in the root
namespace for backwards compatibility.
Data Transfer Objects
Complex requests can be built with DTOs instead of arrays:
use Pashkevich\Loyverse\Data\{CreateItemData, CreateReceiptData, InventoryUpdate, RefundReceiptData}; $receipt = $loyverse->createReceipt(new CreateReceiptData( storeId: STORE_ID, lineItems: [['variant_id' => VARIANT_ID, 'quantity' => 1]], payments: [['payment_type_id' => PAYMENT_TYPE_ID, 'money_amount' => 10.0]], )); $refund = $loyverse->createReceiptRefund(RECEIPT_NUMBER, new RefundReceiptData( lineItems: [['id' => LINE_ITEM_ID, 'quantity' => 1]], )); $inventory = $loyverse->updateInventory(new InventoryUpdate([ ['variant_id' => VARIANT_ID, 'store_id' => STORE_ID, 'stock_after' => 5], ])); $item = $loyverse->createItem(new CreateItemData('Item name', color: 'GREEN'));
Arrays remain fully supported, so DTOs are opt-in.
Handling Errors
All API errors are represented by exceptions that extend
Pashkevich\Loyverse\Exceptions\ApiException. The exception exposes the HTTP status,
the error code and the details returned by the API:
use Pashkevich\Loyverse\Exceptions\ApiException; try { $loyverse->item('missing-item-id'); } catch (ApiException $exception) { $exception->getStatus(); // 404 $exception->getErrorCode(); // "NOT_FOUND" $exception->getDetails(); // "The resource was not found" $exception->getField(); // null or the field related to the error $exception->getErrors(); // the full list of errors }
Transport errors are wrapped as well:
use Pashkevich\Loyverse\Exceptions\{ConnectionException, HttpTimeoutException, TlsException}; try { $loyverse->items(); } catch (HttpTimeoutException $exception) { // request timed out } catch (TlsException $exception) { // TLS/certificate problem } catch (ConnectionException $exception) { // DNS, connection refused, etc. }
All SDK exceptions extend Pashkevich\Loyverse\Exceptions\LoyverseException.
For rate limited requests you can also read the delay before retrying:
try { $loyverse->items(); } catch (Pashkevich\Loyverse\Exceptions\RateLimitedException $exception) { $exception->getRetryAfter(); // seconds, or null when the header is missing $exception->getRateLimitResetsAt(); // Unix timestamp from x-ratelimit-reset }
Retries
retryOnRateLimit() retries the callback when the API responds with 429. It uses the
Retry-After header when present, otherwise it falls back to exponential backoff:
$page = $loyverse->retryOnRateLimit(function () use ($loyverse) { return $loyverse->items(); });
The default is 3 attempts. You can pass the maximum number of attempts and the base delay in seconds:
$loyverse->retryOnRateLimit($callback, 5, 2);
Webhook Signatures
Webhooks created by OAuth2 applications include an X-Loyverse-Signature header. Verify it
with the application's Client Secret:
use Pashkevich\Loyverse\WebhookSignature; $payload = file_get_contents('php://input'); $signature = $_SERVER['HTTP_X_LOYVERSE_SIGNATURE'] ?? ''; if (!WebhookSignature::verify($payload, $signature, CLIENT_SECRET)) { http_response_code(401); exit; }
The payload must be the raw request body, before JSON decoding.
Managing Categories
$loyverse->categories(array $parameters); $loyverse->category(string $categoryId); $loyverse->createCategory(array $data); $loyverse->deleteCategory(string $categoryId);
On a Category instance you may also call:
$category->update(array $data); $category->delete();
Managing Customers
$loyverse->customers(array $parameters); $loyverse->customer(string $customerId); $loyverse->createCustomer(array $data); $loyverse->deleteCustomer(string $customerId);
On a Customer instance you may also call:
$customer->update(array $data); $customer->delete();
Managing Discounts
$loyverse->discounts(array $parameters); $loyverse->discount(string $discountId); $loyverse->createDiscount(array $data); $loyverse->deleteDiscount(string $discountId);
On a Discount instance you may also call:
$discount->update(array $data); $discount->delete();
Managing Employees
$loyverse->employees(array $parameters); $loyverse->employee(string $employeeId);
Managing Inventory
$page = $loyverse->inventory(array $parameters); $levels = $page['inventory_levels']; $cursor = $page['cursor']; $inventory = $loyverse->updateInventory(array $data);
inventory() returns a page array. updateInventory() returns an Inventory object with
the updated inventoryLevels.
Managing Items
$loyverse->items(array $parameters); $loyverse->item(string $itemId); $loyverse->createItem(array $data); $loyverse->deleteItem(string $itemId); $loyverse->uploadItemImage(string $itemId, string $contents, string $contentType = 'image/png'); $loyverse->deleteItemImage(string $itemId);
On a Item instance you may also call:
$item->update(array $data); $item->delete(); $item->uploadImage(string $contents, string $contentType = 'image/png'); $item->deleteImage();
Upload an image from a file:
$loyverse->uploadItemImage(ITEM_ID, file_get_contents('/path/to/image.png'));
The default content type is image/png. You can pass another supported content type
as the third argument if needed.
Composite item components are available as ItemComponent objects:
foreach ($item->components as $component) { echo $component->variantId; echo $component->quantity; }
Managing Merchants
$loyverse->merchant();
Managing Modifiers
$loyverse->modifiers(array $parameters); $loyverse->modifier(string $modifierId); $loyverse->createModifier(array $data); $loyverse->deleteModifier(string $modifierId);
On a Modifier instance you may also call:
$modifier->update(array $data); $modifier->delete();
Managing Payment Types
$loyverse->paymentTypes(array $parameters); $loyverse->paymentType(string $paymentTypeId);
Managing Pos Devices
$loyverse->posDevices(array $parameters); $loyverse->posDevice(string $posDeviceId); $loyverse->createPosDevice(array $data); $loyverse->deletePosDevice(string $posDeviceId);
On a PosDevice instance you may also call:
$posDevice->update(array $data); $posDevice->delete();
Managing Receipts
$loyverse->receipts(array $parameters); $loyverse->receipt(string $receiptNumber); $loyverse->createReceipt(array $data); $loyverse->createReceiptRefund(string $receiptNumber, array $data);
On a Receipt instance you may also call:
$receipt->refund(array $data);
Receipt line items, discounts, taxes and payments are hydrated into resource objects:
$receipt->lineItems[0]->itemName; $receipt->lineItems[0]->lineTaxes[0]->rate; $receipt->lineItems[0]->lineDiscounts[0]->moneyAmount; $receipt->lineItems[0]->lineModifiers[0]->option; $receipt->totalDiscounts[0]->moneyAmount; $receipt->totalTaxes[0]->moneyAmount; $receipt->payments[0]->moneyAmount;
Managing Shifts
$loyverse->shifts(array $parameters); $loyverse->shift(string $shiftId);
Managing Stores
$loyverse->stores(array $parameters); $loyverse->store(string $storeId);
Managing Suppliers
$loyverse->suppliers(array $parameters); $loyverse->supplier(string $supplierId); $loyverse->createSupplier(array $data); $loyverse->deleteSupplier(string $supplierId);
On a Supplier instance you may also call:
$supplier->update(array $data); $supplier->delete();
Managing Taxes
$loyverse->taxes(array $parameters); $loyverse->tax(string $taxId); $loyverse->createTax(array $data); $loyverse->deleteTax(string $taxId);
On a Tax instance you may also call:
$tax->update(array $data); $tax->delete();
Managing Variants
$loyverse->variants(array $parameters); $loyverse->variant(string $variantId); $loyverse->createVariant(array $data); $loyverse->deleteVariant(string $variantId);
On a Variant instance you may also call:
$variant->update(array $data); $variant->delete();
Managing Webhooks
$loyverse->webhooks(array $parameters); $loyverse->webhook(string $webhookId); $loyverse->createWebhook(array $data); $loyverse->deleteWebhook(string $webhookId);
On a Webhook instance you may also call:
$webhook->update(array $data); $webhook->delete();
Fake Client
Use Loyverse::fake() to test code that depends on the SDK without real HTTP calls:
use GuzzleHttp\Psr7\Response; use Pashkevich\Loyverse\Loyverse; $loyverse = Loyverse::fake([ new Response(200, [], json_encode([ 'categories' => [], 'cursor' => null, ])), ]); $page = $loyverse->categories();
Queued responses are consumed in order. You can also enqueue exceptions to test error handling.
Testing
composer test
composer analyse
composer analyse-tests
composer test-coverage-check
composer format-test
composer audit
Integration tests are opt-in and skipped unless tests/Integration/.env.testing is configured:
cp tests/Integration/.env.testing.example tests/Integration/.env.testing composer test-integration
The suite includes a contract test that checks all endpoints from the committed Loyverse OpenAPI snapshot, an endpoint test for every SDK action method and a method coverage test. A scheduled GitHub Actions workflow compares that snapshot with the live specification and fails when the API changes.
Coverage is enforced at 80% line coverage. Mutation testing runs nightly with Infection
(composer infection).
Contributing
Please see CONTRIBUTING for details.
Security
Please see SECURITY.md for supported versions and reporting instructions.
If you discover any security related issues, please email siarheipashkevich@gmail.com instead of using the issue tracker.
Credits
License
Loyverse SDK is open-sourced software licensed under the MIT license.