laravel-erli maintained by beziworld
Laravel Erli
Erli.pl marketplace Shop API client for Laravel:
- products: create, partial update, stock and price (decimal amounts, sent in grosze), activate, deactivate,
archive, batch updates, search with cursor paging, time-limited discounts, market specific data (
X-ERLI-MARKET) - orders: details, search, cursor-based iteration, seller status, external order id, returns, buyer and invoice addresses, pickup points
- change poller: reads the order and product change queues (the successor of the deprecated inbox), dispatches
Laravel events and marks messages as read only after successful dispatch; scheduler-friendly
erli:pollcommand with a lock per account - hooks: registration and an incoming hook route (orders, product sync) with bearer token verification, plus a
BuyabilityCheckercontract for thecheckBuyabilityhook - shipping: tracking numbers of your own carriers (external parcels), Erli parcels, posting points
- payments and refund operations, dictionaries (price lists, delivery methods, carriers, categories, terms)
- multiple seller accounts, runtime API keys with
using(), genericrequest()for any endpoint
Why this package
The only other Erli package on Packagist (opero-crm/erli-api, Bitbucket, development branch only) generates
untyped methods from the OpenAPI specification. This package is built for Laravel stores and order management:
typed DTOs with decimal money, the recommended change queue flow with events and safe acknowledgement, incoming hooks,
multi-account configuration and documented error handling, all tested against the official OpenAPI schema.
Installation
composer require beziworld/laravel-erli
php artisan vendor:publish --tag=erli-config # optional
ERLI_API_KEY=... # shop panel: Metoda integracji > Własna integracja po API
ERLI_SANDBOX=false # true = sandbox.erli.dev (test account from Erli support)
ERLI_MARKET= # pl or de; empty = base market of the shop
ERLI_USER_AGENT="MyShop/1.0 (laravel-erli)"
ERLI_HOOK_TOKEN= # access token for incoming hooks
ERLI_HOOKS_ENABLED=false
Several shops:
// config/erli.php
'accounts' => [
'default' => ['api_key' => env('ERLI_API_KEY')],
'outlet' => ['api_key' => env('ERLI_OUTLET_API_KEY'), 'hook_token' => env('ERLI_OUTLET_HOOK_TOKEN')],
],
Erli::account('outlet')->orders()->get($id);
Erli::using(['api_key' => $tenant->erli_api_key])->products()->updateStock('SKU-1', 3);
Products
use Beziworld\Erli\Data\NewProduct;
use Beziworld\Erli\Enums\TaxRate;
use Beziworld\Erli\Facades\Erli;
Erli::products()->create(new NewProduct(
externalId: 'SKU-1',
name: 'Krzesło dębowe',
images: ['https://shop.test/img/1.png#v3'], // change the URL when the image changes
price: '499.00',
stock: 12,
dispatchDays: 2,
taxRate: TaxRate::Tax23,
extra: ['deliveryPriceList' => 'standard', 'description' => '<p>Solid oak</p>', 'externalCategories' => [/* ... */]],
));
Erli::products()->updateStock('SKU-1', 5);
Erli::products()->updatePrice('SKU-1', '449.00');
Erli::products()->update('SKU-1', ['name' => 'Krzesło dębowe, olejowane']); // only changed fields
Erli::products()->deactivate('SKU-1'); // products cannot be deleted
$results = Erli::products()->updateStocks(['SKU-1' => 5, 'SKU-2' => 0]); // one batch request
$results[1]->successful(); // per product HTTP status
foreach (Erli::products()->all(['field' => 'stock', 'operator' => '<=', 'value' => 0]) as $product) {
$product->externalId; $product->price; $product->frozen; $product->isBuyable();
}
Erli::inMarket('de')->products()->update('SKU-1', ['name' => 'Eichenstuhl']); // translation for the de market
Creating and updating is asynchronous (HTTP 202); a duplicate create raises ApiException with isConflict().
Fields edited manually in the seller panel are frozen and ignored by the API unless overrideFrozen: true is passed.
Orders and changes
use Beziworld\Erli\Enums\SellerStatus;
$order = Erli::orders()->get('123456x7890');
$order->isPurchased(); $order->totalPrice; $order->items; $order->deliveryAddress; $order->pickupPlace; $order->returns;
foreach (Erli::orders()->since(now()->subDay()) as $order) { /* sorted by update time, cursor paging */ }
Erli::orders()->setStatus($order->id, SellerStatus::InProgress);
Erli::orders()->setExternalId($order->id, 'ZAM/2026/0042');
Erli::shipping()->addTracking($order->id, 'dpd', '0000123456789Q');
Erli::orders()->setStatus($order->id, SellerStatus::Sent);
Polling (recommended)
Schedule::command('erli:poll --all')->everyMinute();
use Beziworld\Erli\Events\{OrderCreated, OrderStatusChanged, OrderSellerStatusChanged, ProductsNeedSync};
Event::listen(OrderStatusChanged::class, function (OrderStatusChanged $event) {
if ($event->order->isPurchased()) { /* fulfil */ }
});
Event::listen(ProductsNeedSync::class, fn (ProductsNeedSync $event) => SyncProducts::dispatch($event->productIds, $event->fields));
Every order change also dispatches OrderChanged. Messages are marked as read up to the last one whose listeners
succeeded, so a failing listener does not lose the remaining messages. Erli does not restore stock after a
cancellation; update it from your listener.
Hooks (optional, faster)
use Beziworld\Erli\Enums\HookName;
Erli::hooks()->register(HookName::OrderCreated, route('erli.hook', ['account' => 'default', 'hook' => 'orderCreated']));
With ERLI_HOOKS_ENABLED=true the route POST /erli/hooks/{account}/{hook} verifies the bearer token against the
account hook_token and dispatches the same events (with a null message). Bind BuyabilityChecker to answer
checkBuyability. Erli does not retry hooks; keep the poller running as the reliable source.
Errors
| Exception | When |
|---|---|
ApiException |
HTTP 4xx/5xx: httpStatus(), errorCode() (for example SAI-12), failureType(), traceId(), isConflict(), isNotFound() |
AuthenticationFailed |
HTTP 401/403: invalid API key or missing scope |
RateLimitExceeded |
HTTP 429 (load dependent), retryAfter() |
InvalidConfiguration |
missing API key or unknown account |
Reads (GET and search requests) are retried once on connection errors and HTTP 5xx; writes are never retried.
Development
composer install
composer ci # pint --test, phpstan (level 8), pest
Author
Created and maintained by Kamil Ćwiertnia, beziworld.eu.
- E-mail: biuro@beziworld.eu
- Issues: GitHub issues
Need an integration? Get in touch through beziworld.eu.
Other packages by the author
laravel-allegro · laravel-baselinker · laravel-shoper · laravel-idosell · laravel-inpost
License
MIT. See LICENSE.md. This package is not affiliated with Erli.