Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Shopify Api Laravel Package

shopify/shopify-api

Official Shopify API library for PHP. Build Shopify apps and integrations with REST and GraphQL Admin API support, OAuth, webhooks, session storage, and typed request/response helpers for modern PHP applications.

View on GitHub
Deep Wiki
Context7

Getting Started

Install with Composer (the package name is shopify/shopify-api, not the repo name):

composer require shopify/shopify-api

Read these classes first, in this order — they are the whole library:

  1. Shopify\Context — global config (Context::initialize(...)), API version, embedded-app flag, default session storage.
  2. Shopify\Auth\Auth — begin() / callback() for the OAuth install flow.
  3. Shopify\Clients\Graphql — the client you will use 95% of the time.
  4. Shopify\Webhooks\Registry — addHandler() / process() for webhooks.
  5. Shopify\Auth\SessionStorage\* — FileSessionStorage, CookieSessionStorage, and the SessionStorageInterface you'll almost certainly implement yourself in Laravel.

One-time setup in config/shopify.php + AppServiceProvider::boot():

use Shopify\Context;

Context::initialize(
    apiKey:         config('shopify.api_key'),
    apiSecretKey:   config('shopify.api_secret'),
    scopes:         config('shopify.scopes'),      // array or comma string depending on version
    shopifyHostName: config('shopify.app_url'),
    apiVersion:     config('shopify.api_version'), // e.g. '2025-01', pinned
    isEmbeddedApp:  true,
    sessionStorage: app(\Shopify\Auth\SessionStorage\SessionStorageInterface::class),
);

First use case to get working end-to-end: the OAuth handshake, then a single GraphQL call.

// routes/web.php
Route::get('/auth',      [ShopifyAuthController::class, 'begin']);
Route::get('/auth/callback', [ShopifyAuthController::class, 'callback']);
public function begin(Request $request)
{
    $shop = $request->string('shop');           // validate against *.myshopify.com first
    $response = Auth::begin($shop, route('shopify.callback'), /* isOnline */ false);

    return redirect($response->getHeaderLine('Location'))
        ->withCookies($this->psrCookies($response)); // don't drop Set-Cookie headers
}

public function callback(Request $request)
{
    $session = Auth::callback($this->toPsrRequest($request), new \GuzzleHttp\Psr7\Response());
    // persist $session->getAccessToken() keyed by shop

    $client = new \Shopify\Clients\Graphql($session);
    $result = $client->query([
        'query' => 'query { shop { name myshopifyDomain } }',
    ]);

    return $result->getDecodedBody();
}

Once that works, the library stops being mysterious: everything else is "load a session, construct a client, run a query."

Implementation Patterns

Bind config and session storage as singletons, but never store a Session globally. Context::initialize() is process-wide and only needs to run once (fine in a service provider). Sessions are per-shop and per-request, so pass them explicitly to clients:

$session = $this->sessions->loadByShop($shop);      // your repo, cached
$client  = new Graphql($session);                   // explicit — safe in queues, jobs, commands

This constructor-injection style is what makes the library usable in Laravel's queue workers, where there is no HTTP request for Context to inspect. Prefer new Graphql($session) over any request-context helper in jobs and commands.

Wrap the client in one domain service per resource. Don't sprinkle raw GraphQL strings across controllers. One class per resource (Orders, Products, Fulfillments) that exposes typed methods, builds variables, unwraps getDecodedBody(), and throws domain exceptions on userErrors. This is also your only realistic testing seam — the library has its own HTTP layer, so Http::fake() will not intercept it, but a mocked repository interface will.

final class ShopifyOrders
{
    public function __construct(private Session $session) {}

    public function find(string $gid): array
    {
        $res = (new Graphql($this->session))->query([
            'query' => 'query($id: ID!) { order(id: $id) { id name totalPriceSet { shopMoney { amount currencyCode } } } }',
            'variables' => ['id' => $gid],
        ]);

        return $res->getDecodedBody()['data']['order'] ?? throw new ShopifyException('Order not found');
    }
}

Middleware for embedded apps. A ResolveShopifySession middleware that (a) validates the shop param or App Bridge session token, (b) loads the offline session, (c) binds it into the container, and (d) aborts 401 when absent. Keep JWT/HMAC verification in the middleware, not in every controller.

Webhooks: register in a provider, process in one controller, dispatch to queue.

// AppServiceProvider::boot()
Registry::addHandler('PRODUCTS_UPDATE', function ($topic, $shop, $body, $webhookId, $apiVersion) {
    SyncProductJob::dispatch($shop, json_decode($body, true), $webhookId);
});
Registry::addHandler('APP_UNINSTALLED', fn ($topic, $shop) => $this->forgetShop($shop));
public function webhook(Request $request)
{
    Registry::process($request);   // validates HMAC + topic, runs handlers
    return response('', 200);
}

Always return 200 fast; Shopify retries aggressively on non-2xx. Use X-Shopify-Webhook-Id for idempotency at the job level.

Pagination. For small sets, loop pageInfo { hasNextPage endCursor }. For anything bulk (catalog sync, order exports), use bulkOperationRunQuery + poll the operation + download the JSONL from url — it is dramatically faster and avoids cost-limit whack-a-mole.

Billing is just GraphQL: read currentAppInstallation { activeSubscriptions { name status } }, and create plans with appSubscriptionCreate. Keep the plan definitions in config so test and prod shops can differ.

Gotchas and Tips

  • The package is archived. No upstream fixes, no new API versions guaranteed. Pin the exact version in composer.json, and put a calendar reminder to evaluate the successor path the README points to before your pinned API version is sunset (Shopify supports versions ~12 months).
  • API version is a hardcoded string in Context::initialize. Nothing warns you when it's retired — requests just start failing. Make it a config value with a test asserting it matches a supported version.
  • REST is gone/deprecated. Recent majors removed the Rest client because Shopify moved the REST Admin API to legacy status. If your codebase is on an older version and uses Rest, plan the GraphQL migration before you upgrade a major — this is the single largest upgrade cost.
  • PSR-7 bridging is the #1 Laravel friction point. Auth::begin(), Auth::callback(), and session loading take PSR-7 Request/Response objects, not Laravel's. Convert with GuzzleHttp\Psr7\ServerRequest::fromGlobals() (or build manually) and, critically, copy Set-Cookie headers from the returned PSR-7 response onto the Laravel response. Miss this and OAuth "works" but the nonce/cookie verification fails randomly.
  • FileSessionStorage will bite you in production. Default storage writes to a temp dir — broken on read-only filesystems, broken across multiple app servers, and wiped on redeploy. Implement SessionStorageInterface against the cache or a shopify_sessions table (loadSession, storeSession, deleteSession). This is the standard extension point and takes ~30 lines.
  • Offline vs online tokens. Offline tokens are per-shop and permanent — use them for webhooks, jobs, and anything scheduled. Online tokens are per-user and expire. Mixing them up produces 401s that only appear for some merchants.
  • Your default Laravel middleware can corrupt webhook HMACs. VerifyCsrfToken, TrimStrings, and ConvertEmptyStringsToNull on the webhook route will break signature verification (or mutate the raw body before Registry::process sees it). Exclude the webhook route from CSRF and read the raw body untouched. Symptom: "Invalid webhook HMAC" for payloads that look fine in logs.
  • Two different error shapes. Transport/HTTP failures throw GraphqlQueryException; GraphQL-level errors come back in the response body via getErrors(). Code that only catches exceptions will silently treat failed mutations as successes. Always check getErrors() and userErrors separately.
  • Cost throttling is on you. GraphQL returns extensions.cost.throttleStatus (notably currentlyAvailable). Batch requests into single queries with aliases/fragments, request only fields you use, and implement exponential backoff on THROTTLED. Don't blind-retry a 200-response that contains throttling errors.
  • Validate the shop parameter before any API call — regex ^[a-z0-9][a-z0-9-]*\.myshopify\.com$. It's user input that ends up in your session store and URLs.
  • Never log X-Shopify-Access-Token or full webhook bodies (PII). Redact at the HTTP/log layer, and remember the library will happily include tokens in exception messages on a bad response.
  • Scope changes require reinstall. Adding a scope silently fails for existing merchants until they re-authorize; surface a reinstall prompt rather than debugging phantom ACCESS_DENIED errors.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect