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
- REST Admin API – Traditional REST endpoints for store management
- GraphQL Admin API – Modern, flexible data querying
- Storefront API – Customer-facing operations
- 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: