Files
2025-11-30 08:48:52 +08:00

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 catalog
  • read_orders, write_orders - Order management
  • read_customers, write_customers - Customer data
  • read_inventory, write_inventory - Stock levels
  • read_fulfillments, write_fulfillments - Order fulfillment
  • read_shipping, write_shipping - Shipping rates
  • read_analytics - Store analytics
  • read_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/delete
  • orders/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_field
  • number_integer, number_decimal
  • date, date_time
  • url, json
  • file_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