Skip to main content
It needs PHP 8.2 or later and Laravel 12 or 13.

Install

stockshift:install publishes config/stockshift.php, adds STOCKSHIFT_TOKEN and STOCKSHIFT_WEBHOOK_SECRET to .env and .env.example and asks for their values. Create an API key in StockShift under Settings > Access > API keys. Then check it:

Quickstart

Making calls

The facade has every area of the API, such as catalog() or inventory(), and each area has a method for each operation. They are the SDK’s methods, with the same arguments and PHPStan types:
You can also call a path directly with get(), post(), put(), patch(), delete() or request():
The facade, an injected StockShift\Laravel\Connection and an injected StockShift\Sdk\StockShift all use the default connection.

Queries

Each list of the API has a query builder. where() sends the API’s own filters, and the results come back as Laravel collections:
lazy() returns a LazyCollection that fetches the next page only when you reach it. With limit() or take(), pages past the limit are never fetched:
The API returns each list in its own fixed order. orderBy() and orderByDesc() sort here instead, so they read the whole list first. Fields may use dot notation. The builders on the facade are items(), stock(), movements(), counts(), transfers(), locations(), suppliers(), purchaseOrders(), customers(), salesOrders(), quotes(), invoices(), returns(), builds(), boms() and workCenters(). Any other list works by its path:
Other methods: Enums are sent as their value and dates as ISO 8601.

Connections

A connection is a token, the webhook secret that goes with it and its settings. Add more in config/stockshift.php to call several organizations:
To inject one, mark the parameter:
Each connection has these settings:

A token for each tenant

In an app where each tenant connects its own StockShift organization, look the token up before each call:
Or make a connection for one token where you need it:
Cached query results and rate limits are kept apart for each token.

Errors

A failed call throws the SDK’s ApiException, which holds the RFC 9457 problem details body. Branch on errorCode, which is stable:
A call that gets no answer throws a ConnectionException. Both extend StockShiftException.

Retries, idempotency keys and queued calls

The SDK retries a 429 after Retry-After. It retries a 5xx or a network failure for GET, PUT and DELETE, and for a POST or PATCH only when it has an idempotency key:
later() sends a call from a queued job. A POST or PATCH gets an idempotency key when the job is made, so the job’s own retries send the same key and the API makes the change once:
The job is StockShift\Laravel\Jobs\SendRequest. It tries five times. A 4xx answer other than 429 fails it at once, because sending it again gets the same answer. A token from withToken() goes with the job. The job is encrypted on the queue.

Rate limits

Set rate_limit on a connection to cap the calls a minute your app makes with one token. Every process that shares the cache store shares the count. A call that would go over waits until it can be sent. Leave it null to rely on the API’s limit, which the client already waits out. Requests go through Laravel’s HTTP client, so Http::fake(), the RequestSending and ResponseReceived events and tools such as Telescope see them.

Webhooks

The package adds a POST /stockshift/webhook route. Give its full URL to StockShift when you add a webhook endpoint, and put the signing secret StockShift shows you in STOCKSHIFT_WEBHOOK_SECRET. Each delivery is checked against the StockShift-Signature header. One that fails gets a 400 and goes no further. One that passes fires two Laravel events: WebhookReceived for every delivery, then the event’s own class, such as InventoryStockMoved:
Every event type in the API reference has a class in StockShift\Laravel\Events, with $data typed for PHPStan. WebhookReceived also fires for types this version of the package has no class for.

Listening with an attribute

Mark a class or a method with #[OnStockShiftWebhook]:
On a class it calls handle() or __invoke(). On a method it calls that method. Give several types, or end one with * to match many, such as sales.order.*. Type the parameter as WebhookEvent when it can get more than one class. * alone listens to WebhookReceived. Classes in app/Listeners are scanned. Change webhooks.discover in the config to scan other folders. A type that matches no event fails at boot with a hint, so a typo does not go unnoticed. A listener that Laravel’s own event discovery already found by its type hint is not added twice. Listeners that implement ShouldQueue are queued as usual. php artisan optimize caches the listeners it finds. stockshift:cache and stockshift:clear do the same on their own.

Retries and queueing

StockShift retries a delivery that does not get a 2xx, and each retry keeps the event’s id. The package remembers ids for a day and fires the events once. If a listener throws, the id is forgotten and the delivery gets a 500, so the retry is handled. Change webhooks.deduplicate_for to keep ids longer, or set it to 0 to keep none. Set STOCKSHIFT_WEBHOOK_QUEUE to answer StockShift at once and fire the events from a queued job. STOCKSHIFT_WEBHOOK_QUEUE_CONNECTION picks the queue connection.

Other routes

Set webhooks.path to null to leave the route out and place it yourself. A route for another connection checks that connection’s secret:
The middleware works on its own too, as stockshift.webhook or stockshift.webhook:eu. It puts the verified event in the stockshift.event request attribute. Webhook routes need no CSRF exception because they are not in the web group. To add middleware, such as throttle:120,1, list it in webhooks.middleware.

Calling the API by hand

Http::stockshift() is a Laravel HTTP request with the connection’s base URL, token and timeouts. It does not retry or throw on errors:

Artisan commands

Each takes --connection where it calls the API. php artisan about shows the default connection and whether listeners are cached.

Testing

StockShift::fake() answers calls from responses you give and records each one. Nothing reaches the API, and no token is needed:
A key is a method and a path after the base URL. * matches anything. A value is a JSON body, a status code, a PSR-7 response or a closure that takes the RecordedRequest and returns one of those. StockShift::page() builds a page of a list with the records under the name the list uses. A call with no response throws UnexpectedRequest. Calling fake() again adds responses to the same fake. RecordedRequest has connection, method, path, query, body, header() and idempotencyKey(). recorded() returns them all, or those matching a route. assertNothingSent() checks that no call was made. To test your webhook listeners, send a signed delivery through the route:
dispatch() fires the events without the HTTP request. payload() and signature() give the body and header if you want to send it yourself. Because calls go through Laravel’s HTTP client, Http::fake() works too, if you would rather fake at the HTTP level.

Versions

The package follows semantic versioning. It supports the Laravel versions that get security fixes and the PHP versions each of them supports. A new major version of the SDK comes with a new major version of the package.