Looking to hire Laravel developers? Try LaraJobs

laravel-erli maintained by beziworld

Description
Laravel client for the Erli.pl marketplace Shop API: products, stock and prices, orders, order change polling, tracking, hooks and multiple seller accounts
Last update
2026/11/04 19:54 (dev-master)
License
Downloads
1

Comments
comments powered by Disqus

Laravel Erli

tests Latest Version License

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:poll command with a lock per account
  • hooks: registration and an incoming hook route (orders, product sync) with bearer token verification, plus a BuyabilityChecker contract for the checkBuyability hook
  • 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(), generic request() for any endpoint

Polska wersja dokumentacji

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.

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.