storehugs / integration
PHP client for the Store Hugs Integration API: the Storefront API and the Admin API.
Package info
github.com/innoverse-solutions/storehugs-php-integration-sdk
pkg:composer/storehugs/integration
Requires
- php: ^8.2
- ext-json: *
- guzzlehttp/guzzle: ^7.15.2 || ^8.0.3
Requires (Dev)
- laravel/pint: ^1.18.1
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^11.5
- symfony/yaml: ^7.1
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-11-09 10:47:41 UTC
README
A PHP client for the Store Hugs Integration API. It runs on PHP 8.2+ and sends requests with Guzzle 7.15.2+ or 8.0.3+, or with any PSR-18 HTTP client you pass it.
- Storefront API: the catalog, carts and checkout links, and signed-in shoppers on the shop's host. On a server, use a secret key (
sk_live_…) and pass the shopper's IP. - Admin API: the merchant's own systems and connected apps. Use an Admin key (
sa_live_…).
The full reference is at https://developers.storehugs.com (/openapi.yaml). Report issues and send changes at https://github.com/innoverse-solutions/storehugs-php-integration-sdk. A TypeScript client with the same API is at https://github.com/innoverse-solutions/storehugs-ts-integration-sdk.
Install
composer require storehugs/integration
Guzzle comes with it (7.15.2+ or 8.0.3+, whichever your app already uses), so nothing else is needed. Laravel apps already have it. Earlier Guzzle releases have security advisories, and Composer refuses them by default.
Storefront
use StoreHugs\Integration\StorefrontClient; $storefront = new StorefrontClient( host: 'acme.storehugs.com', key: getenv('STOREHUGS_SECRET_KEY'), shopperIp: $request->ip(), ); $products = $storefront->products->list(['search' => 'mug', 'perPage' => 12])['data']; $cart = $storefront->createCart(); $session = $storefront->cart($cart['cartToken']); $session->addItem($products[0]['id'], 2); $link = $session->checkout(['returnPath' => '/thanks']); // Redirect the shopper to $link['checkoutUrl'].
Shopper sign-in uses OAuth with PKCE on the shop's own page:
- Call
Pkce::create()andPkce::state(), and keep the verifier and state in the session. - Send the shopper to
$storefront->signIn->authorizeUrl(…). - Check the state, then exchange the code with
$storefront->signIn->exchangeCode(…). - Call
$storefront->withAccessToken($tokens['access_token'])to act as the shopper.
Admin
use StoreHugs\Integration\AdminClient; $admin = new AdminClient(getenv('STOREHUGS_ADMIN_KEY')); foreach ($admin->orders->all(['updatedSince' => '2026-11-01T00:00:00Z']) as $order) { // Every page is fetched as you read. } $admin->setInventory([['sku' => 'MUG-01', 'stockQuantity' => 40]]); $product = $admin->products->get(12); $admin->products->update(12, ['price' => 19.5], unmodifiedSince: $product['updatedAt']); // 409 edit_conflict if it changed
Catalog setup: besides products, the client reaches what products are built from.
$admin->units: the units products are sold in (a product'sunitId).$admin->taxonomies: the shop's taxonomies, andterms($taxonomyId)for one taxonomy's terms (a product'staxonomyTermIds).$admin->variantOptions: the options variable products are sold in, such as Size, and their values. Reuse these names so the shop keeps one of each.$admin->variableProducts: products sold in those options, created with their variants or given more.$admin->specTemplates: the specification templates a product can use, with the properties itsspecsfill.
Apps get an Admin key, not an OAuth token:
- Send the merchant to
AdminClient::authorizeAppUrl(clientId: …, redirectUri: …, scopes: […], state: …, codeChallenge: …). - On your server, call
AdminClient::exchangeAppCode(…)within 10 minutes. It returnsapi_key,scopesandshop, and the key is shown only that once.
Webhooks
use StoreHugs\Integration\Webhooks; $genuine = Webhooks::verifySignature( $request->getContent(), $request->header(Webhooks::SIGNATURE_HEADER), getenv('STOREHUGS_WEBHOOK_SECRET'), );
This checks the HMAC and that the delivery is recent (5 minutes by default). An empty secret throws InvalidArgumentException instead of answering, so a missing setting can't let forged deliveries through. Pass the raw body exactly as received. Skip any Storehugs-Event-Id you have already handled, because retries reuse it.
Behaviour
- Answers are arrays, decoded from the API's JSON. Their shapes are PHPStan types generated from the OpenAPI file, in
StoreHugs\Integration\Schemas. Import one with@phpstan-import-type Cart from Schemas. - Writes are safe to retry. Every POST, PUT, PATCH and DELETE gets an
Idempotency-Key. PassidempotencyKey: '…'to keep one across processes, oridempotencyKey: falseto send none. - Retries: answers 429 and 503, and network errors, are retried up to
maxRetriestimes (default 2), followingRetry-After, with the same key. If a retry reaches the server while the first attempt is still running (409idempotency_in_progress), the client waits for it under the same key for up toinProgressTimeoutseconds (default 30) and returns its answer. - Errors: a refusal throws
IntegrationApiException, withstatus, the API'serrorCode(insufficient_scope,edit_conflict, …) and, on a 422,errorsper field. Network errors that outlast the retries are thrown as the HTTP client's own PSR-18 exceptions (with Guzzle,GuzzleHttp\Exception\ConnectException).
HTTP
Every client takes an HttpOptions. By default it makes its own Guzzle client, which gives up on a request after 30 seconds (timeout) and on connecting after 10 (connectTimeout):
use StoreHugs\Integration\AdminClient; use StoreHugs\Integration\Http\HttpOptions; $admin = new AdminClient(getenv('STOREHUGS_ADMIN_KEY'), http: new HttpOptions( timeout: 60, maxRetries: 3, inProgressTimeout: 60, ));
To use your own HTTP client, pass any PSR-18 client as client, for example a Guzzle client with your middleware or Symfony's Psr18Client. Its own timeouts then apply. requestFactory and streamFactory take PSR-17 factories, Guzzle's by default.
Development
composer install
composer format:check
composer types:check
composer test
src/Schemas.php is generated from the Integration API's OpenAPI file. Don't edit it by hand: run composer generate, which reads the published spec at https://developers.storehugs.com/openapi.yaml. To read another copy, pass a URL or a file path in SPEC, for example SPEC=../matajer/documentation/openapi/integration-api.yaml composer generate.
A weekly workflow regenerates the types from the published spec and fails when they change, which signals that the client needs a release.
Releasing
-
Update
CHANGELOG.mdand commit tomaster. -
Tag the commit and push the tag:
git tag v0.1.0 && git push origin v0.1.0Packagist reads the version from the tag through the repository's GitHub hook. No version goes in
composer.json.
CI on pushes and pull requests checks types and tests on PHP 8.2 to 8.5 with Guzzle 8, on PHP 8.5 with the newest Guzzle 7, and on PHP 8.2 with the lowest dependency versions (Guzzle 7.15.2). Formatting is checked once, on PHP 8.5 with the newest Pint.
Contributing and security
See CONTRIBUTING.md. Report vulnerabilities privately, as SECURITY.md describes.
License
MIT