E-Commerce & Integrations

Integrating Shopify API with Laravel 12: Building a Centralized E-Commerce Management System

Integrate Shopify API with Laravel 12 to build a centralized e-commerce management system. Complete guide covering OAuth, webhooks, product sync, and inventory management.

Muhammad Waqas

Muhammad Waqas

CEO at CentoSquare

|
03-Nov-2025
13 min read
Integrating Shopify API with Laravel 12: Building a Centralized E-Commerce Management System

Introduction

Managing multiple Shopify stores or integrating Shopify with existing systems requires robust API integration. Laravel 12 provides the perfect foundation for building centralized e-commerce management systems that synchronize products, orders, and inventory across multiple Shopify stores while maintaining data consistency and real-time updates.

Whether you're building a multi-store dashboard, inventory management system, or analytics platform, Laravel's elegant architecture combined with Shopify's powerful API enables seamless integration and automation.

In this comprehensive guide, we'll build a production-ready Shopify integration with Laravel 12, covering OAuth authentication, webhook handling, product synchronization, order management, and real-time inventory updates.


Understanding Shopify API Architecture

Shopify API Types

  1. REST Admin API – Traditional REST endpoints for store management
  2. GraphQL Admin API – Modern, flexible data querying
  3. Storefront API – Customer-facing operations
  4. Webhooks – Real-time event notifications

Integration Components

  • OAuth Authentication – Secure store authorization
  • API Client – Request handling and rate limiting
  • Webhook Handler – Event processing
  • Data Synchronization – Products, orders, inventory
  • Queue System – Asynchronous processing

Database Schema Design

Migration for Shopify Integration

// Shopify stores table
Schema::create('shopify_stores', function (Blueprint $table) {
    $table->id();
    $table->string('shop_domain')->unique();
    $table->string('shop_name');
    $table->string('access_token')->nullable();
    $table->string('email')->nullable();
    $table->string('currency')->default('USD');
    $table->string('timezone')->nullable();
    $table->boolean('is_active')->default(true);
    $table->json('scopes')->nullable();
    $table->timestamp('token_expires_at')->nullable();
    $table->timestamps();
});

// Products table
Schema::create('shopify_products', function (Blueprint $table) {
    $table->id();
    $table->foreignId('store_id')->constrained('shopify_stores')->cascadeOnDelete();
    $table->unsignedBigInteger('shopify_product_id');
    $table->string('title');
    $table->text('description')->nullable();
    $table->string('handle')->nullable();
    $table->string('product_type')->nullable();
    $table->string('vendor')->nullable();
    $table->decimal('price', 10, 2);
    $table->integer('inventory_quantity')->default(0);
    $table->string('status')->default('active');
    $table->json('images')->nullable();
    $table->json('variants')->nullable();
    $table->json('options')->nullable();
    $table->json('tags')->nullable();
    $table->timestamp('synced_at')->nullable();
    $table->timestamps();
    
    $table->unique(['store_id', 'shopify_product_id']);
    $table->index(['store_id', 'status']);
});

// Orders table
Schema::create('shopify_orders', function (Blueprint $table) {
    $table->id();
    $table->foreignId('store_id')->constrained('shopify_stores')->cascadeOnDelete();
    $table->unsignedBigInteger('shopify_order_id');
    $table->string('order_number');
    $table->string('customer_email')->nullable();
    $table->string('financial_status');
    $table->string('fulfillment_status')->nullable();
    $table->decimal('total_price', 10, 2);
    $table->decimal('subtotal_price', 10, 2);
    $table->decimal('total_tax', 10, 2);
    $table->string('currency')->default('USD');
    $table->json('line_items');
    $table->json('shipping_address')->nullable();
    $table->json('billing_address')->nullable();
    $table->timestamp('shopify_created_at')->nullable();
    $table->timestamp('synced_at')->nullable();
    $table->timestamps();
    
    $table->unique(['store_id', 'shopify_order_id']);
    $table->index(['store_id', 'financial_status']);
});

// Webhooks table
Schema::create('shopify_webhooks', function (Blueprint $table) {
    $table->id();
    $table->foreignId('store_id')->constrained('shopify_stores')->cascadeOnDelete();
    $table->unsignedBigInteger('webhook_id');
    $table->string('topic');
    $table->string('address');
    $table->boolean('is_active')->default(true);
    $table->timestamps();
    
    $table->unique(['store_id', 'webhook_id']);
});

// Sync logs table
Schema::create('shopify_sync_logs', function (Blueprint $table) {
    $table->id();
    $table->foreignId('store_id')->constrained('shopify_stores')->cascadeOnDelete();
    $table->string('type'); // products, orders, inventory
    $table->enum('status', ['started', 'completed', 'failed']);
    $table->integer('records_processed')->default(0);
    $table->text('error_message')->nullable();
    $table->timestamp('started_at');
    $table->timestamp('completed_at')->nullable();
    $table->timestamps();
});

Shopify OAuth Authentication

1. Install Shopify Package

composer require osiset/laravel-shopify

Or build custom implementation:

composer require guzzlehttp/guzzle

2. OAuth Controller

namespace App\Http\Controllers\Shopify;

use App\Http\Controllers\Controller;
use App\Models\ShopifyStore;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;

class ShopifyAuthController extends Controller
{
    protected string $apiKey;
    protected string $apiSecret;
    protected string $scopes;
    
    public function __construct()
    {
        $this->apiKey = config('services.shopify.api_key');
        $this->apiSecret = config('services.shopify.api_secret');
        $this->scopes = config('services.shopify.scopes');
    }
    
    public function install(Request $request)
    {
        $request->validate([
            'shop' => 'required|string',
        ]);
        
        $shop = $request->shop;
        
        // Validate shop domain
        if (!preg_match('/^[a-zA-Z0-9][a-zA-Z0-9\-]*\.myshopify\.com$/', $shop)) {
            return back()->withErrors(['shop' => 'Invalid shop domain']);
        }
        
        $redirectUri = route('shopify.callback');
        $nonce = bin2hex(random_bytes(16));
        
        session(['shopify_nonce' => $nonce]);
        
        $authUrl = "https://{$shop}/admin/oauth/authorize?" . http_build_query([
            'client_id' => $this->apiKey,
            'scope' => $this->scopes,
            'redirect_uri' => $redirectUri,
            'state' => $nonce,
        ]);
        
        return redirect($authUrl);
    }
    
    public function callback(Request $request)
    {
        $request->validate([
            'code' => 'required|string',
            'shop' => 'required|string',
            'state' => 'required|string',
        ]);
        
        // Verify state/nonce
        if ($request->state !== session('shopify_nonce')) {
            return redirect()->route('home')
                ->withErrors(['error' => 'Invalid state parameter']);
        }
        
        // Verify HMAC
        if (!$this->verifyHmac($request->all())) {
            return redirect()->route('home')
                ->withErrors(['error' => 'HMAC verification failed']);
        }
        
        // Exchange code for access token
        $response = Http::post("https://{$request->shop}/admin/oauth/access_token", [
            'client_id' => $this->apiKey,
            'client_secret' => $this->apiSecret,
            'code' => $request->code,
        ]);
        
        if ($response->failed()) {
            return redirect()->route('home')
                ->withErrors(['error' => 'Failed to obtain access token']);
        }
        
        $accessToken = $response->json('access_token');
        $scopes = $response->json('scope');
        
        // Fetch shop details
        $shopData = Http::withHeaders([
            'X-Shopify-Access-Token' => $accessToken,
        ])->get("https://{$request->shop}/admin/api/2024-01/shop.json");
        
        if ($shopData->successful()) {
            $shop = $shopData->json('shop');
            
            $store = ShopifyStore::updateOrCreate(
                ['shop_domain' => $request->shop],
                [
                    'shop_name' => $shop['name'],
                    'access_token' => encrypt($accessToken),
                    'email' => $shop['email'],
                    'currency' => $shop['currency'],
                    'timezone' => $shop['timezone'],
                    'scopes' => explode(',', $scopes),
                    'is_active' => true,
                ]
            );
            
            // Register webhooks
            dispatch(new RegisterShopifyWebhooks($store));
            
            return redirect()->route('shopify.dashboard', $store)
                ->with('success', 'Store connected successfully');
        }
        
        return redirect()->route('home')
            ->withErrors(['error' => 'Failed to fetch shop details']);
    }
    
    protected function verifyHmac(array $params): bool
    {
        $hmac = $params['hmac'] ?? '';
        unset($params['hmac']);
        
        ksort($params);
        $queryString = http_build_query($params);
        
        $calculatedHmac = hash_hmac('sha256', $queryString, $this->apiSecret);
        
        return hash_equals($hmac, $calculatedHmac);
    }
}

Shopify API Client

API Service Layer

namespace App\Services\Shopify;

use App\Models\ShopifyStore;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Cache;

class ShopifyApiClient
{
    protected ShopifyStore $store;
    protected string $baseUrl;
    protected string $accessToken;
    
    public function __construct(ShopifyStore $store)
    {
        $this->store = $store;
        $this->baseUrl = "https://{$store->shop_domain}/admin/api/2024-01";
        $this->accessToken = decrypt($store->access_token);
    }
    
    protected function makeRequest(string $method, string $endpoint, array $data = [])
    {
        // Rate limiting
        $this->checkRateLimit();
        
        $response = Http::withHeaders([
            'X-Shopify-Access-Token' => $this->accessToken,
            'Content-Type' => 'application/json',
        ])->$method("{$this->baseUrl}{$endpoint}", $data);
        
        // Store rate limit info
        $this->storeRateLimitInfo($response);
        
        if ($response->failed()) {
            throw new \Exception("Shopify API request failed: {$response->body()}");
        }
        
        return $response->json();
    }
    
    protected function checkRateLimit(): void
    {
        $key = "shopify_rate_limit:{$this->store->id}";
        $limit = Cache::get($key, 0);
        
        if ($limit >= 40) { // Shopify allows 40 requests per second
            sleep(1);
            Cache::forget($key);
        }
    }
    
    protected function storeRateLimitInfo($response): void
    {
        $key = "shopify_rate_limit:{$this->store->id}";
        $current = Cache::get($key, 0);
        Cache::put($key, $current + 1, 1);
    }
    
    // Product methods
    public function getProducts(array $params = []): array
    {
        return $this->makeRequest('get', '/products.json', $params);
    }
    
    public function getProduct(int $productId): array
    {
        return $this->makeRequest('get', "/products/{$productId}.json");
    }
    
    public function createProduct(array $productData): array
    {
        return $this->makeRequest('post', '/products.json', ['product' => $productData]);
    }
    
    public function updateProduct(int $productId, array $productData): array
    {
        return $this->makeRequest('put', "/products/{$productId}.json", ['product' => $productData]);
    }
    
    public function deleteProduct(int $productId): void
    {
        $this->makeRequest('delete', "/products/{$productId}.json");
    }
    
    // Order methods
    public function getOrders(array $params = []): array
    {
        return $this->makeRequest('get', '/orders.json', $params);
    }
    
    public function getOrder(int $orderId): array
    {
        return $this->makeRequest('get', "/orders/{$orderId}.json");
    }
    
    // Inventory methods
    public function updateInventoryLevel(int $inventoryItemId, int $locationId, int $available): array
    {
        return $this->makeRequest('post', '/inventory_levels/set.json', [
            'inventory_item_id' => $inventoryItemId,
            'location_id' => $locationId,
            'available' => $available,
        ]);
    }
    
    // Webhook methods
    public function createWebhook(string $topic, string $address): array
    {
        return $this->makeRequest('post', '/webhooks.json', [
            'webhook' => [
                'topic' => $topic,
                'address' => $address,
                'format' => 'json',
            ],
        ]);
    }
    
    public function getWebhooks(): array
    {
        return $this->makeRequest('get', '/webhooks.json');
    }
    
    public function deleteWebhook(int $webhookId): void
    {
        $this->makeRequest('delete', "/webhooks/{$webhookId}.json");
    }
}

Product Synchronization

Product Sync Service

namespace App\Services\Shopify;

use App\Models\{ShopifyStore, ShopifyProduct};
use App\Jobs\SyncShopifyProducts;

class ProductSyncService
{
    protected ShopifyApiClient $apiClient;
    
    public function __construct(protected ShopifyStore $store)
    {
        $this->apiClient = new ShopifyApiClient($store);
    }
    
    public function syncAllProducts(): void
    {
        $page = 1;
        $limit = 250;
        
        do {
            $response = $this->apiClient->getProducts([
                'limit' => $limit,
                'page' => $page,
            ]);
            
            $products = $response['products'] ?? [];
            
            foreach ($products as $productData) {
                $this->syncProduct($productData);
            }
            
            $page++;
        } while (count($products) === $limit);
        
        $this->store->touch('synced_at');
    }
    
    protected function syncProduct(array $productData): ShopifyProduct
    {
        $variants = $productData['variants'] ?? [];
        $firstVariant = $variants[0] ?? [];
        
        return ShopifyProduct::updateOrCreate(
            [
                'store_id' => $this->store->id,
                'shopify_product_id' => $productData['id'],
            ],
            [
                'title' => $productData['title'],
                'description' => $productData['body_html'],
                'handle' => $productData['handle'],
                'product_type' => $productData['product_type'],
                'vendor' => $productData['vendor'],
                'price' => $firstVariant['price'] ?? 0,
                'inventory_quantity' => $firstVariant['inventory_quantity'] ?? 0,
                'status' => $productData['status'],
                'images' => $productData['images'],
                'variants' => $variants,
                'options' => $productData['options'],
                'tags' => explode(', ', $productData['tags']),
                'synced_at' => now(),
            ]
        );
    }
    
    public function pushProductToShopify(ShopifyProduct $product): void
    {
        $productData = [
            'title' => $product->title,
            'body_html' => $product->description,
            'vendor' => $product->vendor,
            'product_type' => $product->product_type,
            'tags' => implode(', ', $product->tags ?? []),
        ];
        
        if ($product->shopify_product_id) {
            // Update existing product
            $response = $this->apiClient->updateProduct(
                $product->shopify_product_id,
                $productData
            );
        } else {
            // Create new product
            $response = $this->apiClient->createProduct($productData);
            
            $product->update([
                'shopify_product_id' => $response['product']['id'],
            ]);
        }
        
        $product->update(['synced_at' => now()]);
    }
}

Webhook Handling

Webhook Controller

namespace App\Http\Controllers\Shopify;

use App\Http\Controllers\Controller;
use App\Models\ShopifyStore;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;

class ShopifyWebhookController extends Controller
{
    public function handle(Request $request)
    {
        // Verify webhook
        if (!$this->verifyWebhook($request)) {
            return response()->json(['error' => 'Unauthorized'], 401);
        }
        
        $shop = $request->header('X-Shopify-Shop-Domain');
        $topic = $request->header('X-Shopify-Topic');
        
        $store = ShopifyStore::where('shop_domain', $shop)->first();
        
        if (!$store) {
            Log::warning("Webhook received for unknown store: {$shop}");
            return response()->json(['status' => 'ok']);
        }
        
        // Dispatch appropriate job based on topic
        match($topic) {
            'products/create' => dispatch(new HandleProductCreated($store, $request->all())),
            'products/update' => dispatch(new HandleProductUpdated($store, $request->all())),
            'products/delete' => dispatch(new HandleProductDeleted($store, $request->all())),
            'orders/create' => dispatch(new HandleOrderCreated($store, $request->all())),
            'orders/updated' => dispatch(new HandleOrderUpdated($store, $request->all())),
            'inventory_levels/update' => dispatch(new HandleInventoryUpdated($store, $request->all())),
            default => Log::info("Unhandled webhook topic: {$topic}"),
        };
        
        return response()->json(['status' => 'ok']);
    }
    
    protected function verifyWebhook(Request $request): bool
    {
        $hmac = $request->header('X-Shopify-Hmac-Sha256');
        $data = $request->getContent();
        $secret = config('services.shopify.api_secret');
        
        $calculatedHmac = base64_encode(hash_hmac('sha256', $data, $secret, true));
        
        return hash_equals($hmac, $calculatedHmac);
    }
}

Webhook Jobs

namespace App\Jobs\Shopify;

use App\Models\ShopifyStore;
use App\Services\Shopify\ProductSyncService;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\{InteractsWithQueue, SerializesModels};

class HandleProductUpdated implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
    
    public function __construct(
        protected ShopifyStore $store,
        protected array $productData
    ) {}
    
    public function handle(): void
    {
        $syncService = new ProductSyncService($this->store);
        $syncService->syncProduct($this->productData);
    }
}

class HandleOrderCreated implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
    
    public function __construct(
        protected ShopifyStore $store,
        protected array $orderData
    ) {}
    
    public function handle(): void
    {
        ShopifyOrder::create([
            'store_id' => $this->store->id,
            'shopify_order_id' => $this->orderData['id'],
            'order_number' => $this->orderData['order_number'],
            'customer_email' => $this->orderData['email'],
            'financial_status' => $this->orderData['financial_status'],
            'fulfillment_status' => $this->orderData['fulfillment_status'],
            'total_price' => $this->orderData['total_price'],
            'subtotal_price' => $this->orderData['subtotal_price'],
            'total_tax' => $this->orderData['total_tax'],
            'currency' => $this->orderData['currency'],
            'line_items' => $this->orderData['line_items'],
            'shipping_address' => $this->orderData['shipping_address'],
            'billing_address' => $this->orderData['billing_address'],
            'shopify_created_at' => $this->orderData['created_at'],
            'synced_at' => now(),
        ]);
    }
}

Register Webhooks Job

namespace App\Jobs\Shopify;

use App\Models\ShopifyStore;
use App\Services\Shopify\ShopifyApiClient;

class RegisterShopifyWebhooks implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
    
    protected array $webhookTopics = [
        'products/create',
        'products/update',
        'products/delete',
        'orders/create',
        'orders/updated',
        'inventory_levels/update',
    ];
    
    public function __construct(protected ShopifyStore $store) {}
    
    public function handle(): void
    {
        $apiClient = new ShopifyApiClient($this->store);
        $webhookUrl = route('shopify.webhook');
        
        // Get existing webhooks
        $existingWebhooks = $apiClient->getWebhooks()['webhooks'] ?? [];
        $existingTopics = collect($existingWebhooks)->pluck('topic')->toArray();
        
        // Register missing webhooks
        foreach ($this->webhookTopics as $topic) {
            if (!in_array($topic, $existingTopics)) {
                try {
                    $response = $apiClient->createWebhook($topic, $webhookUrl);
                    
                    $this->store->webhooks()->create([
                        'webhook_id' => $response['webhook']['id'],
                        'topic' => $topic,
                        'address' => $webhookUrl,
                    ]);
                } catch (\Exception $e) {
                    Log::error("Failed to register webhook: {$topic}", [
                        'error' => $e->getMessage(),
                    ]);
                }
            }
        }
    }
}

Dashboard Controller

namespace App\Http\Controllers\Shopify;

use App\Http\Controllers\Controller;
use App\Models\ShopifyStore;
use App\Services\Shopify\ProductSyncService;
use Illuminate\Http\Request;

class ShopifyDashboardController extends Controller
{
    public function index()
    {
        $stores = ShopifyStore::where('is_active', true)->get();
        
        return view('shopify.dashboard', compact('stores'));
    }
    
    public function show(ShopifyStore $store)
    {
        $products = $store->products()
            ->where('status', 'active')
            ->latest('synced_at')
            ->paginate(50);
        
        $recentOrders = $store->orders()
            ->latest('shopify_created_at')
            ->limit(10)
            ->get();
        
        $stats = [
            'total_products' => $store->products()->count(),
            'active_products' => $store->products()->where('status', 'active')->count(),
            'total_orders' => $store->orders()->count(),
            'pending_orders' => $store->orders()->where('fulfillment_status', null)->count(),
        ];
        
        return view('shopify.store', compact('store', 'products', 'recentOrders', 'stats'));
    }
    
    public function syncProducts(ShopifyStore $store)
    {
        dispatch(new SyncShopifyProducts($store));
        
        return back()->with('success', 'Product sync started');
    }
    
    public function syncOrders(ShopifyStore $store)
    {
        dispatch(new SyncShopifyOrders($store));
        
        return back()->with('success', 'Order sync started');
    }
}

Configuration

services.php

'shopify' => [
    'api_key' => env('SHOPIFY_API_KEY'),
    'api_secret' => env('SHOPIFY_API_SECRET'),
    'scopes' => env('SHOPIFY_SCOPES', 'read_products,write_products,read_orders,write_orders,read_inventory,write_inventory'),
    'api_version' => env('SHOPIFY_API_VERSION', '2024-01'),
],

.env

SHOPIFY_API_KEY=your_api_key
SHOPIFY_API_SECRET=your_api_secret
SHOPIFY_SCOPES=read_products,write_products,read_orders,write_orders

Multi-Store Management Features

Inventory Sync Across Stores

namespace App\Services\Shopify;

class MultiStoreInventorySync
{
    public function syncInventoryAcrossStores(int $productSku, int $quantity): void
    {
        $stores = ShopifyStore::where('is_active', true)->get();
        
        foreach ($stores as $store) {
            $product = $store->products()
                ->where('sku', $productSku)
                ->first();
            
            if ($product) {
                $apiClient = new ShopifyApiClient($store);
                $apiClient->updateInventoryLevel(
                    $product->inventory_item_id,
                    $store->primary_location_id,
                    $quantity
                );
            }
        }
    }
}

Best Practices

1. Rate Limit Handling

protected function handleRateLimitError($response): void
{
    if ($response->status() === 429) {
        $retryAfter = $response->header('Retry-After') ?? 2;
        sleep((int) $retryAfter);
    }
}

2. Bulk Operations

public function bulkUpdateProducts(array $products): void
{
    foreach (array_chunk($products, 100) as $chunk) {
        dispatch(new BulkUpdateProductsJob($this->store, $chunk));
    }
}

3. Error Handling

try {
    $this->apiClient->createProduct($data);
} catch (\Exception $e) {
    Log::error('Shopify product creation failed', [
        'store' => $this->store->shop_domain,
        'error' => $e->getMessage(),
        'data' => $data,
    ]);
    
    throw $e;
}

Conclusion

Integrating Shopify API with Laravel 12 enables powerful centralized e-commerce management systems. By implementing OAuth authentication, webhook handling, product synchronization, and multi-store management, you create scalable solutions that automate operations and provide unified control over multiple Shopify stores.

Key takeaways:

  • Implement secure OAuth authentication flow
  • Use service layer for API abstraction
  • Handle webhooks for real-time updates
  • Implement rate limiting and error handling
  • Queue asynchronous operations
  • Sync products, orders, and inventory
  • Support multi-store management

Need help integrating Shopify with your Laravel application? NeedLaravelSite specializes in e-commerce integrations. Contact us for expert Laravel development services.


Related Resources:


Article Tags

Laravel Shopify integration Shopify API Laravel Laravel 12 Shopify Shopify Laravel app Laravel e-commerce integration Shopify webhook Laravel Laravel Shopify sync Shopify OAuth Laravel Laravel multi-store management Shopify inventory Laravel

About the Author

Muhammad Waqas

Muhammad Waqas

CEO at CentoSquare

Founder & CEO at CentoSquare | Creator of NeedLaravelSite | Helping Businesses Grow with Cutting-Edge Web, Mobile & Marketing Solutions | Building Innovative Products