Shopify's app ecosystem powers millions of stores with custom functionality. Building Shopify apps opens opportunities to solve merchant problems at scale or create custom integrations for specific clients. This guide covers developing Shopify apps from a senior developer's perspective.
Why Build for Shopify
Shopify development offers compelling opportunities:
- Massive Market: Millions of active stores worldwide
- App Store Distribution: Built-in marketplace for discovery
- Recurring Revenue: Subscription billing support
- Well-Documented APIs: REST and GraphQL options
- Modern Stack: App Bridge for seamless UI integration
App Types
Public Apps
- Listed on Shopify App Store
- Available to all merchants
- Revenue sharing with Shopify
- Rigorous review process
Custom Apps
- Built for specific stores
- Direct installation
- No app store listing
- Simpler approval process
Private Apps
- Legacy option (being phased out)
- Use custom apps instead
Getting Started
Prerequisites
- Shopify Partner account (free)
- Development store for testing
- ngrok or similar for local development
Create an App in the Partner Dashboard
- Go to Partners → Apps → Create app
- Configure app URLs:
- App URL: https://your-app.com/ - Allowed redirection URL: https://your-app.com/auth/callback 3. Note your API credentials: - Client ID - Client Secret
OAuth Authentication Flow
Step 1: Redirect to Shopify
When a merchant installs your app:
<?php
$client_id = 'your-client-id';
$scopes = 'read_products,write_products,read_orders,write_orders';
$redirect_uri = 'https://your-app.com/auth/callback';
$shop = $_GET['shop'];
$nonce = bin2hex(random_bytes(12));
// Store nonce in session for verification
$_SESSION['oauth_nonce'] = $nonce;
$oauth_url = "https://{$shop}/admin/oauth/authorize?" . http_build_query([
'client_id' => $client_id,
'scope' => $scopes,
'redirect_uri' => $redirect_uri,
'state' => $nonce,
'grant_options[]' => 'per-user'
]);
header("Location: {$oauth_url}");
exit();
Step 2: Handle Callback
<?php
$client_id = 'your-client-id';
$client_secret = 'your-client-secret';
// Verify HMAC signature
$params = $_GET;
$hmac = $params['hmac'];
unset($params['hmac']);
ksort($params);
$computed_hmac = hash_hmac('sha256', http_build_query($params), $client_secret);
if (!hash_equals($hmac, $computed_hmac)) {
die('Invalid signature - possible attack');
}
// Verify nonce
if ($params['state'] !== $_SESSION['oauth_nonce']) {
die('Invalid state parameter');
}
// Exchange code for access token
$shop = $params['shop'];
$code = $params['code'];
$response = file_get_contents("https://{$shop}/admin/oauth/access_token", false, stream_context_create([
'http' => [
'method' => 'POST',
'header' => 'Content-Type: application/x-www-form-urlencoded',
'content' => http_build_query([
'client_id' => $client_id,
'client_secret' => $client_secret,
'code' => $code
])
]
]));
$data = json_decode($response, true);
$access_token = $data['access_token'];
// Store access token securely (database)
saveAccessToken($shop, $access_token);
// Redirect to app
header("Location: /app?shop={$shop}");
REST API Integration
Making API Requests
<?php
function shopifyRequest($shop, $access_token, $endpoint, $method = 'GET', $data = null) {
$url = "https://{$shop}/admin/api/2024-01{$endpoint}";
$headers = [
"X-Shopify-Access-Token: {$access_token}",
"Content-Type: application/json"
];
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
if ($data && in_array($method, ['POST', 'PUT'])) {
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
}
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode >= 400) {
throw new Exception("API Error: {$httpCode} - {$response}");
}
return json_decode($response, true);
}
// Examples
// Get products
$products = shopifyRequest($shop, $token, '/products.json');
// Get a single product
$product = shopifyRequest($shop, $token, '/products/123456789.json');
// Create product
$newProduct = shopifyRequest($shop, $token, '/products.json', 'POST', [
'product' => [
'title' => 'New Product',
'body_html' => '<p>Description</p>',
'vendor' => 'My Store',
'product_type' => 'Widget',
'variants' => [
['price' => '19.99', 'sku' => 'WIDGET-001']
]
]
]);
// Update product
shopifyRequest($shop, $token, '/products/123456789.json', 'PUT', [
'product' => ['title' => 'Updated Title']
]);
// Delete product
shopifyRequest($shop, $token, '/products/123456789.json', 'DELETE');
GraphQL API Integration
GraphQL is recommended for new apps—more efficient and flexible:
<?php
function shopifyGraphQL($shop, $access_token, $query, $variables = []) {
$url = "https://{$shop}/admin/api/2024-01/graphql.json";
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"X-Shopify-Access-Token: {$access_token}",
"Content-Type: application/json"
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
'query' => $query,
'variables' => $variables
]));
$response = curl_exec($ch);
curl_close($ch);
return json_decode($response, true);
}
// Query products
$query = <<<'GRAPHQL'
query getProducts($first: Int!) {
products(first: $first, query: "status:active") {
edges {
node {
id
title
description
variants(first: 10) {
nodes {
id
price
inventoryQuantity
}
}
images(first: 1) {
edges {
node {
url
}
}
}
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
GRAPHQL;
$result = shopifyGraphQL($shop, $token, $query, ['first' => 50]);
// Mutation: Update product
$mutation = <<<'GRAPHQL'
mutation updateProduct($input: ProductInput!) {
productUpdate(input: $input) {
product {
id
title
}
userErrors {
field
message
}
}
}
GRAPHQL;
$result = shopifyGraphQL($shop, $token, $mutation, [
'input' => [
'id' => 'gid://shopify/Product/123456789',
'title' => 'Updated via GraphQL'
]
]);
App Bridge UI
Embed your app seamlessly in Shopify Admin:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="shopify-api-key" content="<?php echo $client_id; ?>" />
<script src="https://cdn.shopify.com/shopifycloud/app-bridge.js"></script>
<link rel="stylesheet" href="https://unpkg.com/@shopify/polaris@latest/build/esm/styles.css" />
</head>
<body>
<!--Title bar with actions-->
<ui-title-bar title="My App">
<button variant="primary" onclick="saveSettings()">Save</button>
<button onclick="showHelp()">Help</button>
</ui-title-bar>
<!--Navigation menu-->
<ui-nav-menu>
<a href="/dashboard" rel="home">Dashboard</a>
<a href="/products">Products</a>
<a href="/settings">Settings</a>
</ui-nav-menu>
<div id="app">
<h1>Welcome to My App</h1>
<button id="pick-product">Select Product</button>
<div id="selected-product"></div>
</div>
<script>
// Resource Picker
document.getElementById('pick-product').addEventListener('click', async () => {
const selected = await shopify.resourcePicker({
type: 'product',
multiple: false
});
if (selected && selected.length > 0) {
document.getElementById('selected-product').textContent =
`Selected: ${selected[0].title}`;
}
});
// Toast notifications
function showSuccess(message) {
shopify.toast.show(message, { duration: 3000 });
}
function showError(message) {
shopify.toast.show(message, { isError: true });
}
// Modal
async function showHelp() {
await shopify.modal.alert({
title: 'Help',
message: 'This app helps you manage products more efficiently.'
});
}
// Confirm dialog
async function confirmDelete() {
const confirmed = await shopify.modal.confirm({
title: 'Delete Item',
message: 'Are you sure? This cannot be undone.'
});
if (confirmed) {
// Proceed with deletion
}
}
// Get session token for authenticated API calls
async function callBackend() {
const token = await shopify.idToken();
const response = await fetch('/api/data', {
headers: {
'Authorization': `Bearer ${token}`
}
});
return response.json();
}
</script>
</body>
</html>
Session Token Verification
Verify requests from your embedded app:
<?php
function verifySessionToken($token, $client_secret) {
$parts = explode('.', $token);
if (count($parts) !== 3) {
return false;
}
list($header, $payload, $signature) = $parts;
// Verify signature
$expected_sig = rtrim(strtr(
base64_encode(hash_hmac('sha256', "{$header}.{$payload}", $client_secret, true)),
'+/', '-_'
), '=');
if (!hash_equals($expected_sig, $signature)) {
return false;
}
// Decode and verify payload
$data = json_decode(base64_decode($payload), true);
// Check expiration
if ($data['exp'] < time()) {
return false;
}
return $data; // Contains shop, sub (user), etc.
}
// Usage in API endpoint
$auth_header = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
if (preg_match('/Bearer\s+(.+)/', $auth_header, $matches)) {
$token_data = verifySessionToken($matches[1], $client_secret);
if ($token_data) {
$shop = $token_data['dest']; // Shop domain
// Process authenticated request
}
}
Webhooks
Subscribe to store events:
<?php
// Register webhook via API
$webhook_data = [
'webhook' => [
'topic' => 'orders/create',
'address' => 'https://your-app.com/webhooks/orders',
'format' => 'json'
]
];
shopifyRequest($shop, $token, '/webhooks.json', 'POST', $webhook_data);
// Webhook endpoint
// webhooks/orders.php
$raw_body = file_get_contents('php://input');
$hmac_header = $_SERVER['HTTP_X_SHOPIFY_HMAC_SHA256'];
// Verify webhook
$computed_hmac = base64_encode(hash_hmac('sha256', $raw_body, $client_secret, true));
if (!hash_equals($hmac_header, $computed_hmac)) {
http_response_code(401);
exit('Unauthorized');
}
// Process webhook
$data = json_decode($raw_body, true);
$order_id = $data['id'];
$customer_email = $data['email'];
// Handle the order...
processNewOrder($data);
http_response_code(200);
App Billing
Charge merchants for your app:
<?php
// Create recurring charge
$charge = shopifyGraphQL($shop, $token, <<<'GRAPHQL'
mutation createSubscription($name: String!, $price: Decimal!, $returnUrl: URL!) {
appSubscriptionCreate(
name: $name
returnUrl: $returnUrl
lineItems: [{
plan: {
appRecurringPricingDetails: {
price: { amount: $price, currencyCode: USD }
interval: EVERY_30_DAYS
}
}
}]
) {
appSubscription {
id
status
}
confirmationUrl
userErrors {
field
message
}
}
}
GRAPHQL, [
'name' => 'Pro Plan',
'price' => '9.99',
'returnUrl' => 'https://your-app.com/billing/confirm'
]);
// Redirect merchant to confirmation URL
if ($charge['data']['appSubscriptionCreate']['confirmationUrl']) {
header('Location: ' . $charge['data']['appSubscriptionCreate']['confirmationUrl']);
}
Testing
Development Store
- Create a development store in the Partner Dashboard
- Install app directly (no review needed)
- Test all functionality
Test Data
# Shopify CLI for generating test data
shopify populate products --count 20
shopify populate customers --count 10
shopify populate orders --count 5
Key Takeaways
- Use GraphQL: More efficient than REST for complex queries
- Verify everything: HMAC signatures, session tokens, webhooks
- App Bridge for UI: Seamless merchant experience
- Handle rate limits: Respect API throttling
- Secure tokens: Never expose access tokens to frontend
- Test thoroughly: Use development stores before production
Shopify development offers a profitable opportunity for developers who master its APIs and merchant-focused mindset.