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 ascatalog() or inventory(), and each area has a method for each operation. They are the SDK’s methods, with the same arguments and PHPStan types:
get(), post(), put(), patch(), delete() or request():
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:
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:
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 inconfig/stockshift.php to call several organizations:
A token for each tenant
In an app where each tenant connects its own StockShift organization, look the token up before each call:Errors
A failed call throws the SDK’sApiException, which holds the RFC 9457 problem details body. Branch on errorCode, which is stable:
ConnectionException. Both extend StockShiftException.
Retries, idempotency keys and queued calls
The SDK retries a429 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:
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
Setrate_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 aPOST /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:
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]:
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 a2xx, 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
Setwebhooks.path to null to leave the route out and place it yourself. A route for another connection checks that connection’s secret:
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:
* 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.