smplx.
Shopify Architecture

Shopify API Strategies: Using REST, GraphQL and Webhooks the Right Way [2026]

Claudio Gerlich··13 min

In our Shopify Architecture Guide, we look at the big picture. Today we go deeper: How do we use Shopify's APIs correctly? When REST, when GraphQL, how Webhooks?

At smplx., we've been building integrations between Shopify and external systems since 2020. We've learned: The right API strategy determines whether your integration is robust or constantly failing.

The Three Shopify APIs at a Glance

1. Admin REST API

What is it?

REST is the classic API. You send HTTP requests, get JSON back.

Endpoint structure:

POST https://myshop.myshopify.com/admin/api/2024-01/products.json

Pros:

  • Easy to understand (HTTP, JSON)
  • Good documentation
  • Many SDKs (Ruby, Node, PHP, etc.)
  • Familiar – most developers know REST

Cons:

  • Over-fetching: You get data you don't need
  • Multiple requests necessary (e.g., Product → Variants → Images)
  • Less query control

Example: Fetching a product with variants

# Request 1: Product
GET /admin/api/2024-01/products/123.json

# Request 2: Variants
GET /admin/api/2024-01/products/123/variants.json

# Request 3: Images
GET /admin/api/2024-01/products/123/images.json

# Total: 3 API calls for one "complete" product info

2. Admin GraphQL API

What is GraphQL?

GraphQL is a query language. You ask for exactly what you need – nothing more, nothing less.

Query structure:

query GetProduct($id: ID!) {
  product(id: $id) {
    id
    title
    variants(first: 5) {
      edges {
        node {
          id
          title
          price
        }
      }
    }
    images(first: 3) {
      edges {
        node {
          url
          altText
        }
      }
    }
  }
}

Pros:

  • No over-fetching
  • One request instead of three
  • Type system (knows all fields)
  • Modern, powerful
  • Better error messages

Cons:

  • Steeper learning curve (what is GraphQL?)
  • Debugging is different from REST
  • Rate limits are more complex

Example: Same scenario with GraphQL

const query = `
  query GetProduct($id: ID!) {
    product(id: $id) {
      id
      title
      variants(first: 5) {
        edges { node { id title price } }
      }
      images(first: 3) {
        edges { node { url altText } }
      }
    }
  }
`;

const response = await fetch(shopifyEndpoint, {
  method: 'POST',
  headers: { 'X-Shopify-Access-Token': token },
  body: JSON.stringify({ query, variables: { id: 'gid://shopify/Product/123' } })
});

// Everything in one request!

3. Storefront API

What is the Storefront API?

The Storefront API is GraphQL, but for your customer (not admin). It's public (with limited scope).

Differences from Admin GraphQL:

  • Cannot write (read-only)
  • Less information (e.g., no inventory details for other variants)
  • Built for customer scenarios (Cart, Checkout)
  • Rate limits are different (more generous for read-only)

When to use?

  • Your frontend (headless shop)
  • External websites (embedding product search results)
  • Mobile apps

Webhooks: Event-Driven Architecture

What are Webhooks?

Instead of constantly polling the API ("Are there new orders?"), Shopify tells you: "Attention, a new order!" – via webhook.

Architecture:

Customer purchases → Shopify calls your backend (HTTP POST) → Your code reacts

Webhook Example: Order Created

Your webhook endpoint:

const express = require('express');
const app = express();

// Validate webhook (important!)
const verifyShopifyWebhook = (req, shopifySecret) => {
  const hmac = req.headers['x-shopify-hmac-sha256'];
  const body = req.rawBody;

  const hash = crypto
    .createHmac('sha256', shopifySecret)
    .update(body, 'utf8')
    .digest('base64');

  return hash === hmac;
};

app.post('/webhooks/orders/create', (req, res) => {
  if (!verifyShopifyWebhook(req, process.env.SHOPIFY_API_SECRET)) {
    return res.status(401).send('Unauthorized');
  }

  const order = req.body;

  // Your logic here
  console.log(`Order #${order.order_number} created`);

  // Send to ERP
  sendToERP(order);

  // Send to email service
  sendConfirmationEmail(order.customer);

  res.status(200).send('OK');
});

app.listen(3000);

Important webhook security:

  1. Validate the signature (see code above)
  2. Make your endpoints idempotent (webhook could arrive twice)
  3. Respond quickly (Shopify only waits 30 seconds)

Idempotency Example

app.post('/webhooks/orders/create', async (req, res) => {
  const order = req.body;
  const webhookId = req.headers['x-shopify-webhook-id'];

  // Check: Have we already processed this webhook?
  const existing = await db.webhooks.findOne({
    shopify_webhook_id: webhookId
  });

  if (existing) {
    console.log('Webhook already processed');
    return res.status(200).send('OK');
  }

  // New → Process
  await processOrder(order);

  // Mark as processed
  await db.webhooks.insert({
    shopify_webhook_id: webhookId,
    processed_at: new Date()
  });

  res.status(200).send('OK');
});

Important: You need idempotency because webhooks can arrive multiple times.

Rate Limits: How to Handle Them

REST API Rate Limits

Quota: 2 API calls per second (Standard Plan)

const rateLimiter = new RateLimiter({
  maxRequests: 2,
  interval: 1000 // 1 second
});

const makeRequest = async (path) => {
  await rateLimiter.acquire();

  return fetch(`https://myshop.myshopify.com/admin/api/2024-01/${path}`, {
    headers: { 'X-Shopify-Access-Token': token }
  });
};

GraphQL Rate Limits

More complex: Based on query complexity, not just count.

Shopify header after request:

X-Shopify-GraphQL-Bulkoperation-Resource-State: running
Graphql-Cost: { "requestedQueryCost": 25, "actualQueryCost": 10, "throttleStatus": { "maximumAvailable": 1000, "currentlyAvailable": 990, "restoreRate": 50 } }

What this means:

  • You have 1000 "points"
  • This query cost 10 points
  • You regenerate 50 points per second
  • If < 100 points: Throttle!
const checkRateLimit = (response) => {
  const cost = JSON.parse(response.headers['graphql-cost']);

  if (cost.throttleStatus.currentlyAvailable < 100) {
    console.log('Rate limit critical! Waiting...');
    const waitTime = (100 - cost.throttleStatus.currentlyAvailable) / cost.throttleStatus.restoreRate * 1000;
    return new Promise(resolve => setTimeout(resolve, waitTime));
  }
};

Real-World: helpyourself Case Study

helpyourself is a medical commerce shop that needs to synchronize Shopify with their ERP (iLabServer).

Challenge:

  • 10,000+ products
  • Inventory must be synced in real-time
  • GDPR-critical (customer data)
  • Errors must not happen (medical devices!)

Solution: Webhook-Driven with Retry Logic

// 1. Shopify Webhook → Order created
app.post('/webhooks/orders/create', async (req, res) => {
  const order = req.body;

  // 2. Immediately: Queue for ERP (webhooks must be fast!)
  const jobId = await queue.add('sync-order-to-erp', {
    orderId: order.id,
    attempt: 1
  }, {
    priority: 'high',
    attempts: 5, // Up to 5 attempts
    backoff: { type: 'exponential', delay: 2000 } // 2s, 4s, 8s, etc.
  });

  res.status(202).send({ jobId });
});

// 3. Background Worker (Bull Queue)
queue.process('sync-order-to-erp', async (job) => {
  const { orderId, attempt } = job.data;

  try {
    const order = await shopify.get(`/admin/api/2024-01/orders/${orderId}.json`);
    const response = await iLabServer.post('/api/orders', {
      order_number: order.order_number,
      customer: order.customer,
      items: order.line_items,
      total: order.total_price
    });

    if (!response.ok) {
      throw new Error(`iLabServer returned ${response.status}`);
    }

    await logSuccess(orderId);
  } catch (error) {
    console.error(`Attempt ${attempt} failed:`, error);
    throw error; // Bull will trigger retry
  }
});

Why webhooks are brilliant here:

  • Asynchronous (customers don't see the delay)
  • Reliable with retry logic
  • Scalable (many orders in parallel)
  • GDPR-proof (data doesn't go through the frontend)

REST vs. GraphQL: When to Use Which?

Scenario REST GraphQL
One-time data fetching
Batch operations (1000+ orders)
Writing (editing products) ❌ (not in Admin)
Complex queries (3+ requests)
Performance-critical
Rate limit handling important ⚠️ Simple
Team doesn't know GraphQL ⚠️

Our rule: GraphQL for complex queries, REST for simple CRUD operations.

Common Mistakes We See

Mistake 1: No Retry Logic on Webhooks

// ❌ WRONG
app.post('/webhooks/orders/create', async (req, res) => {
  const result = await externalAPI.post(order);
  // What if externalAPI is down? Order is lost!
});

// ✅ RIGHT
app.post('/webhooks/orders/create', async (req, res) => {
  // Respond immediately
  res.status(202).send('OK');

  // Then process async with retry
  await queue.add('process-order', order, { attempts: 5 });
});

Mistake 2: No Webhook Signature Validation

// ❌ WRONG: Trusts any POST
app.post('/webhooks/orders/create', (req, res) => {
  processOrder(req.body); // Anyone can trigger this!
});

// ✅ RIGHT
app.post('/webhooks/orders/create', (req, res) => {
  if (!verifySignature(req)) {
    return res.status(401).send('Unauthorized');
  }
  processOrder(req.body);
});

Mistake 3: No Idempotency Handling

// ❌ WRONG
app.post('/webhooks/orders/create', async (req, res) => {
  const order = req.body;
  // Shopify sends it twice? Two orders in the ERP!
  await erp.createOrder(order);
});

// ✅ RIGHT
app.post('/webhooks/orders/create', async (req, res) => {
  const webhookId = req.headers['x-shopify-webhook-id'];

  if (await alreadyProcessed(webhookId)) {
    return res.status(200).send('OK');
  }

  await erp.createOrder(req.body);
  await markProcessed(webhookId);
});

Best Practices for Robust Integrations

1. Event-Driven (Webhooks) Instead of Polling

// ❌ WRONG: Polling every 10 seconds
setInterval(async () => {
  const orders = await shopify.get('/admin/api/2024-01/orders.json');
  // API-expensive and data delay
}, 10000);

// ✅ RIGHT: Webhooks
shopify.webhookSubscribe('orders/create', myWebhookEndpoint);

2. Always Include Logging/Monitoring

const logger = require('winston');

app.post('/webhooks/orders/create', async (req, res) => {
  logger.info('Webhook received', {
    webhook_id: req.headers['x-shopify-webhook-id'],
    order_id: req.body.id
  });

  try {
    await processOrder(req.body);
    logger.info('Order processed successfully');
  } catch (error) {
    logger.error('Order processing failed', { error: error.message });
    throw error; // Queue will trigger retry
  }
});

3. Build Health Checks

app.get('/health', async (req, res) => {
  const shopifyReachable = await checkShopifyConnection();
  const erpReachable = await checkERPConnection();

  if (shopifyReachable && erpReachable) {
    res.status(200).send('OK');
  } else {
    res.status(503).send('Service Unavailable');
  }
});

4. Proactively Manage Rate Limits

const monitorRateLimit = async () => {
  const response = await makeGraphQLQuery(testQuery);
  const { throttleStatus } = JSON.parse(
    response.headers['graphql-cost']
  );

  if (throttleStatus.currentlyAvailable < 200) {
    logger.warn('Rate limit critical', {
      available: throttleStatus.currentlyAvailable
    });
    // Could trigger business logic here (e.g., pause batch job)
  }
};

Summary: The API Strategy for 2026

  1. REST for simple operations (change one product, read one order)
  2. GraphQL for complex queries (many related data with one request)
  3. Webhooks for event-driven architecture (whenever possible)
  4. Storefront API for customer-facing (headless shops, mobile apps)

The combination of all three is often the best solution.


About the Author

Claudio Gerlich is Technical Architect at smplx. and has specialized in Shopify integrations since 2020. With smplx., we've implemented complex sync scenarios with ERPs, CRMs, and specialized systems – always with a focus on reliability and error handling.

helpyourself is a great example of a robust integration: medical-grade reliability with webhooks and retry logic.

smplx. is a Shopify Technical Partner (since 2020) based in the Münsterland, NRW.