9.4 KiB
9.4 KiB
App Development Reference
Guide for building Shopify apps with OAuth, GraphQL/REST APIs, webhooks, and billing.
OAuth Authentication
OAuth 2.0 Flow
1. Redirect to Authorization URL:
https://{shop}.myshopify.com/admin/oauth/authorize?
client_id={api_key}&
scope={scopes}&
redirect_uri={redirect_uri}&
state={nonce}
2. Handle Callback:
app.get('/auth/callback', async (req, res) => {
const { code, shop, state } = req.query;
// Verify state to prevent CSRF
if (state !== storedState) {
return res.status(403).send('Invalid state');
}
// Exchange code for access token
const accessToken = await exchangeCodeForToken(shop, code);
// Store token securely
await storeAccessToken(shop, accessToken);
res.redirect(`https://${shop}/admin/apps/${appHandle}`);
});
3. Exchange Code for Token:
async function exchangeCodeForToken(shop, code) {
const response = await fetch(`https://${shop}/admin/oauth/access_token`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
client_id: process.env.SHOPIFY_API_KEY,
client_secret: process.env.SHOPIFY_API_SECRET,
code
})
});
const { access_token } = await response.json();
return access_token;
}
Access Scopes
Common Scopes:
read_products,write_products- Product catalogread_orders,write_orders- Order managementread_customers,write_customers- Customer dataread_inventory,write_inventory- Stock levelsread_fulfillments,write_fulfillments- Order fulfillmentread_shipping,write_shipping- Shipping ratesread_analytics- Store analyticsread_checkouts,write_checkouts- Checkout data
Full list: https://shopify.dev/api/usage/access-scopes
Session Tokens (Embedded Apps)
For embedded apps using App Bridge:
import { getSessionToken } from '@shopify/app-bridge/utilities';
async function authenticatedFetch(url, options = {}) {
const app = createApp({ ... });
const token = await getSessionToken(app);
return fetch(url, {
...options,
headers: {
...options.headers,
'Authorization': `Bearer ${token}`
}
});
}
GraphQL Admin API
Making Requests
async function graphqlRequest(shop, accessToken, query, variables = {}) {
const response = await fetch(
`https://${shop}/admin/api/2025-01/graphql.json`,
{
method: 'POST',
headers: {
'X-Shopify-Access-Token': accessToken,
'Content-Type': 'application/json'
},
body: JSON.stringify({ query, variables })
}
);
const data = await response.json();
if (data.errors) {
throw new Error(`GraphQL errors: ${JSON.stringify(data.errors)}`);
}
return data.data;
}
Product Operations
Create Product:
mutation CreateProduct($input: ProductInput!) {
productCreate(input: $input) {
product {
id
title
handle
}
userErrors {
field
message
}
}
}
Variables:
{
"input": {
"title": "New Product",
"productType": "Apparel",
"vendor": "Brand",
"status": "ACTIVE",
"variants": [
{ "price": "29.99", "sku": "SKU-001", "inventoryQuantity": 100 }
]
}
}
Update Product:
mutation UpdateProduct($input: ProductInput!) {
productUpdate(input: $input) {
product { id title }
userErrors { field message }
}
}
Query Products:
query GetProducts($first: Int!, $query: String) {
products(first: $first, query: $query) {
edges {
node {
id
title
status
variants(first: 5) {
edges {
node { id price inventoryQuantity }
}
}
}
}
pageInfo { hasNextPage endCursor }
}
}
Order Operations
Query Orders:
query GetOrders($first: Int!) {
orders(first: $first) {
edges {
node {
id
name
createdAt
displayFinancialStatus
totalPriceSet {
shopMoney { amount currencyCode }
}
customer { email firstName lastName }
}
}
}
}
Fulfill Order:
mutation FulfillOrder($input: FulfillmentInput!) {
fulfillmentCreate(input: $input) {
fulfillment { id status trackingInfo { number url } }
userErrors { field message }
}
}
Webhooks
Configuration
In shopify.app.toml:
[webhooks]
api_version = "2025-01"
[[webhooks.subscriptions]]
topics = ["orders/create"]
uri = "/webhooks/orders/create"
[[webhooks.subscriptions]]
topics = ["products/update"]
uri = "/webhooks/products/update"
[[webhooks.subscriptions]]
topics = ["app/uninstalled"]
uri = "/webhooks/app/uninstalled"
# GDPR mandatory webhooks
[webhooks.privacy_compliance]
customer_data_request_url = "/webhooks/gdpr/data-request"
customer_deletion_url = "/webhooks/gdpr/customer-deletion"
shop_deletion_url = "/webhooks/gdpr/shop-deletion"
Webhook Handler
import crypto from 'crypto';
function verifyWebhook(req) {
const hmac = req.headers['x-shopify-hmac-sha256'];
const body = req.rawBody; // Raw body buffer
const hash = crypto
.createHmac('sha256', process.env.SHOPIFY_API_SECRET)
.update(body, 'utf8')
.digest('base64');
return hmac === hash;
}
app.post('/webhooks/orders/create', async (req, res) => {
if (!verifyWebhook(req)) {
return res.status(401).send('Unauthorized');
}
const order = req.body;
console.log('New order:', order.id, order.name);
// Process order...
res.status(200).send('OK');
});
Common Webhook Topics
Orders:
orders/create,orders/updated,orders/deleteorders/paid,orders/cancelled,orders/fulfilled
Products:
products/create,products/update,products/delete
Customers:
customers/create,customers/update,customers/delete
Inventory:
inventory_levels/update
App:
app/uninstalled(critical for cleanup)
Billing Integration
App Charges
One-time Charge:
mutation CreateCharge($input: AppPurchaseOneTimeInput!) {
appPurchaseOneTimeCreate(input: $input) {
appPurchaseOneTime {
id
name
price { amount }
status
confirmationUrl
}
userErrors { field message }
}
}
Variables:
{
"input": {
"name": "Premium Feature",
"price": { "amount": 49.99, "currencyCode": "USD" },
"returnUrl": "https://your-app.com/billing/callback"
}
}
Recurring Charge (Subscription):
mutation CreateSubscription($input: AppSubscriptionCreateInput!) {
appSubscriptionCreate(input: $input) {
appSubscription {
id
name
status
confirmationUrl
}
userErrors { field message }
}
}
Variables:
{
"input": {
"name": "Monthly Subscription",
"returnUrl": "https://your-app.com/billing/callback",
"lineItems": [
{
"plan": {
"appRecurringPricingDetails": {
"price": { "amount": 29.99, "currencyCode": "USD" },
"interval": "EVERY_30_DAYS"
}
}
}
]
}
}
Usage-based Billing:
mutation CreateUsageCharge($input: AppUsageRecordCreateInput!) {
appUsageRecordCreate(input: $input) {
appUsageRecord {
id
price { amount }
description
}
userErrors { field message }
}
}
Metafields
Create Metafield
mutation CreateMetafield($input: MetafieldInput!) {
metafieldsSet(metafields: [$input]) {
metafields {
id
namespace
key
value
}
userErrors { field message }
}
}
Variables:
{
"input": {
"ownerId": "gid://shopify/Product/123",
"namespace": "custom",
"key": "instructions",
"value": "Handle with care",
"type": "single_line_text_field"
}
}
Metafield Types:
single_line_text_field,multi_line_text_fieldnumber_integer,number_decimaldate,date_timeurl,jsonfile_reference,product_reference
Rate Limiting
GraphQL Cost-Based Limits
Limits:
- Available points: 2000
- Restore rate: 100 points/second
- Max query cost: 2000
Check Cost:
const response = await graphqlRequest(shop, token, query);
const cost = response.extensions?.cost;
console.log(`Cost: ${cost.actualQueryCost}/${cost.throttleStatus.maximumAvailable}`);
Handle Throttling:
async function graphqlWithRetry(shop, token, query, retries = 3) {
for (let i = 0; i < retries; i++) {
try {
return await graphqlRequest(shop, token, query);
} catch (error) {
if (error.message.includes('Throttled') && i < retries - 1) {
await sleep(Math.pow(2, i) * 1000); // Exponential backoff
continue;
}
throw error;
}
}
}
Best Practices
Security:
- Store credentials in environment variables
- Verify webhook HMAC signatures
- Validate OAuth state parameter
- Use HTTPS for all endpoints
- Implement rate limiting on your endpoints
Performance:
- Cache access tokens securely
- Use bulk operations for large datasets
- Implement pagination for queries
- Monitor GraphQL query costs
Reliability:
- Implement exponential backoff for retries
- Handle webhook delivery failures
- Log errors for debugging
- Monitor app health metrics
Compliance:
- Implement GDPR webhooks (mandatory)
- Handle customer data deletion requests
- Provide data export functionality
- Follow data retention policies