Search by

pashkevich / loyverse-sdk

siarheipashkevich

A simple to use PHP SDK for the Loyverse API

Package info

github.com/siarheipashkevich/loyverse-sdk

pkg:composer/pashkevich/loyverse-sdk

Statistics

Installs: 3 747

Dependents: 0

Suggesters: 0

Stars: 23

Open Issues: 0

4.6.0 2026-10-28 06:21 UTC

README

Build Status Total Downloads Latest Stable Version License

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

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.