• Skip to secondary menu
  • Skip to main content
  • Skip to primary sidebar
  • Home
  • Projects
  • Products
  • Themes
  • Tools
  • Request for Quote

Vengala Vinay

Having 12+ Years of Experience in Software Development

  • Home
  • WordPress
  • PHP
    • Codeigniter
  • Django
  • Magento
  • Selenium
  • Server
Home » How to securely integrate Shopify headless API endpoints into WordPress custom plugins using Rewrite API custom endpoints

How to securely integrate Shopify headless API endpoints into WordPress custom plugins using Rewrite API custom endpoints

Leveraging WordPress Rewrite API for Secure Shopify Headless Integration

Integrating external APIs into WordPress, especially for e-commerce functionalities like those offered by Shopify’s headless API, requires a robust and secure approach. While direct AJAX calls are common, they often expose sensitive endpoints and can lead to cross-origin issues or security vulnerabilities. This guide details how to use WordPress’s built-in Rewrite API to create custom endpoints that act as secure proxies for your Shopify headless API calls, enhancing both security and maintainability within your custom WordPress plugins.

Understanding the WordPress Rewrite API

The WordPress Rewrite API is primarily used for managing permalinks and creating custom URL structures. However, its core functionality of intercepting requests and mapping them to specific PHP callbacks is exceptionally well-suited for building custom API endpoints. By defining custom rewrite rules and associated query variables, we can hook into WordPress’s request processing flow to handle incoming requests, perform necessary actions (like fetching data from Shopify), and return the results.

Plugin Structure and Initialization

We’ll create a simple WordPress plugin to house our custom endpoints. This plugin will register the necessary rewrite rules and the callback functions that will handle the API requests.

Create a new directory in your WordPress plugins folder, e.g., wp-content/plugins/shopify-headless-proxy/. Inside this directory, create your main plugin file, shopify-headless-proxy.php.

Main Plugin File: shopify-headless-proxy.php

<?php
/**
 * Plugin Name: Shopify Headless Proxy
 * Description: Securely proxies Shopify headless API requests.
 * Version: 1.0
 * Author: Antigravity
 */

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

// Define plugin constants.
define( 'SHOPIFY_HEADLESS_PROXY_VERSION', '1.0' );
define( 'SHOPIFY_API_ENDPOINT', 'https://your-shop-name.myshopify.com/api/2023-10/graphql.json' ); // Replace with your actual GraphQL endpoint
define( 'SHOPIFY_API_KEY', 'your-api-key' ); // Replace with your actual API key
define( 'SHOPIFY_API_PASSWORD', 'your-api-password' ); // Replace with your actual API password

// Include necessary files.
require_once plugin_dir_path( __FILE__ ) . 'includes/class-shopify-headless-proxy-api.php';

// Instantiate the API handler.
function register_shopify_headless_proxy_api() {
    $api_handler = new Shopify_Headless_Proxy_API();
    $api_handler->register_endpoints();
}
add_action( 'plugins_loaded', 'register_shopify_headless_proxy_api' );

// Flush rewrite rules on plugin activation/deactivation.
register_activation_hook( __FILE__, array( 'Shopify_Headless_Proxy_API', 'flush_rewrite_rules' ) );
register_deactivation_hook( __FILE__, array( 'Shopify_Headless_Proxy_API', 'flush_rewrite_rules' ) );
?>

Implementing the API Handler Class

Create a new directory includes/ inside your plugin folder. Inside includes/, create the file class-shopify-headless-proxy-api.php. This class will manage the rewrite rules, query variables, and the actual API request handling.

API Handler Class: includes/class-shopify-headless-proxy-api.php

<?php
/**
 * Shopify Headless Proxy API Handler.
 */
class Shopify_Headless_Proxy_API {

    /**
     * Constructor.
     */
    public function __construct() {
        // Hook into WordPress to add rewrite rules and query vars.
        add_action( 'init', array( $this, 'add_rewrite_rules' ) );
        add_filter( 'query_vars', array( $this, 'add_query_vars' ) );
        add_action( 'parse_request', array( $this, 'handle_api_request' ) );
    }

    /**
     * Registers custom rewrite rules for our API endpoints.
     */
    public function add_rewrite_rules() {
        // Example: Proxy for fetching products.
        // This rule will match URLs like /api/shopify/products/
        add_rewrite_rule(
            '^api/shopify/products/?$', // Regex for the URL pattern
            'index.php?shopify_api_action=products', // Query vars to set
            'top' // Position in the rewrite rules (top means it's checked first)
        );

        // Example: Proxy for fetching a single product by ID.
        // This rule will match URLs like /api/shopify/products/(\d+)/
        add_rewrite_rule(
            '^api/shopify/products/(\d+)/?$',
            'index.php?shopify_api_action=single_product&product_id=$matches1',
            'top'
        );

        // Add more rules for other Shopify API endpoints as needed.
        // For example, for collections:
        // add_rewrite_rule(
        //     '^api/shopify/collections/?$',
        //     'index.php?shopify_api_action=collections',
        //     'top'
        // );
    }

    /**
     * Adds custom query variables to WordPress.
     *
     * @param array $vars Existing query variables.
     * @return array Modified query variables.
     */
    public function add_query_vars( $vars ) {
        $vars[] = 'shopify_api_action';
        $vars[] = 'product_id';
        // Add more query vars for other actions and parameters.
        return $vars;
    }

    /**
     * Handles incoming requests and routes them to the appropriate API action.
     *
     * @param WP_Query $wp_query The current WP_Query object.
     */
    public function handle_api_request( $wp_query ) {
        // Check if our custom query variables are set.
        if ( ! isset( $wp_query->query_vars['shopify_api_action'] ) ) {
            return; // Not our endpoint, let WordPress handle it.
        }

        // Prevent WordPress from loading the theme and template.
        $wp_query->is_404       = false;
        $wp_query->is_page      = true; // Treat as a page to prevent theme loading
        $wp_query->is_singular  = true;
        $wp_query->is_home      = false;
        $wp_query->is_archive   = false;
        $wp_query->is_category  = false;
        $wp_query->is_tag       = false;
        $wp_query->is_tax       = false;
        $wp_query->is_single    = false;
        $wp_query->is_post_type_archive = false;
        $wp_query->is_feed      = false;
        $wp_query->is_comment_feed = false;
        $wp_query->is_trackback   = false;
        $wp_query->is_search    = false;
        $wp_query->is_paged     = false;
        $wp_query->is_robots    = false;
        $wp_query->is_posts_page = false;
        $wp_query->is_attachment = false;
        $wp_query->is_comments_popup = false;
        $wp_query->is_admin     = false; // Crucial to prevent admin context

        // Set the response header for JSON content.
        header( 'Content-Type: application/json' );

        // Get the action and any associated parameters.
        $action = $wp_query->get( 'shopify_api_action' );
        $product_id = $wp_query->get( 'product_id' );

        // Dispatch to the appropriate handler method.
        switch ( $action ) {
            case 'products':
                $this->get_shopify_products();
                break;
            case 'single_product':
                if ( $product_id ) {
                    $this->get_shopify_single_product( $product_id );
                } else {
                    $this->send_error_response( 'Product ID is missing.', 400 );
                }
                break;
            // Add cases for other actions (e.g., 'collections').
            default:
                $this->send_error_response( 'Unknown Shopify API action.', 400 );
                break;
        }

        // Stop WordPress from further processing.
        exit;
    }

    /**
     * Fetches products from Shopify's GraphQL API.
     */
    private function get_shopify_products() {
        $query = '
            query {
              products(first: 10) {
                edges {
                  node {
                    id
                    title
                    handle
                    descriptionHtml
                    images(first: 1) {
                      edges {
                        node {
                          url
                          altText
                        }
                      }
                    }
                    variants(first: 1) {
                      edges {
                        node {
                          priceV2 {
                            amount
                            currencyCode
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
        ';

        $response = $this->make_shopify_request( $query );

        if ( is_wp_error( $response ) ) {
            $this->send_error_response( 'Failed to fetch products from Shopify.', 500, $response->get_error_message() );
        }

        $data = json_decode( $response['body'], true );

        if ( isset( $data['errors'] ) ) {
            $this->send_error_response( 'Shopify API returned errors.', 500, $data['errors'] );
        }

        // Process and format the data as needed.
        $formatted_products = [];
        if ( isset( $data['data']['products']['edges'] ) ) {
            foreach ( $data['data']['products']['edges'] as $edge ) {
                $node = $edge['node'];
                $formatted_products[] = [
                    'id' => $node['id'],
                    'title' => $node['title'],
                    'handle' => $node['handle'],
                    'descriptionHtml' => $node['descriptionHtml'],
                    'imageUrl' => $node['images']['edges'][0]['node']['url'] ?? null,
                    'price' => $node['variants']['edges'][0]['node']['priceV2']['amount'] ?? null,
                    'currency' => $node['variants']['edges'][0]['node']['priceV2']['currencyCode'] ?? null,
                ];
            }
        }

        wp_send_json_success( $formatted_products );
    }

    /**
     * Fetches a single product by ID from Shopify's GraphQL API.
     *
     * @param string $product_id The Shopify product ID (e.g., 'gid://shopify/Product/1234567890').
     */
    private function get_shopify_single_product( $product_id ) {
        // Ensure the product ID is in the correct GraphQL global ID format if necessary.
        // WordPress rewrite rules might capture just the numeric ID.
        // We need to prepend 'gid://shopify/Product/' if that's how Shopify expects it.
        // For simplicity, assuming the rewrite rule captures the full GID or we can construct it.
        // If your rewrite rule captures only the numeric ID, you might need to adjust this.
        $graphql_product_id = $product_id;
        if ( ! str_starts_with( $product_id, 'gid://shopify/Product/' ) ) {
             $graphql_product_id = 'gid://shopify/Product/' . $product_id;
        }


        $query = sprintf( '
            query {
              product(id: "%s") {
                id
                title
                handle
                descriptionHtml
                images(first: 5) {
                  edges {
                    node {
                      url
                      altText
                    }
                  }
                }
                variants(first: 10) {
                  edges {
                    node {
                      id
                      title
                      priceV2 {
                        amount
                        currencyCode
                      }
                      selectedOptions {
                        name
                        value
                      }
                    }
                  }
                }
              }
            }
        ', esc_attr( $graphql_product_id ) );

        $response = $this->make_shopify_request( $query );

        if ( is_wp_error( $response ) ) {
            $this->send_error_response( 'Failed to fetch product from Shopify.', 500, $response->get_error_message() );
        }

        $data = json_decode( $response['body'], true );

        if ( isset( $data['errors'] ) ) {
            $this->send_error_response( 'Shopify API returned errors.', 500, $data['errors'] );
        }

        if ( ! isset( $data['data']['product'] ) || $data['data']['product'] === null ) {
            $this->send_error_response( 'Product not found.', 404 );
        }

        wp_send_json_success( $data['data']['product'] );
    }

    /**
     * Makes a request to the Shopify GraphQL API.
     *
     * @param string $query The GraphQL query string.
     * @return array|WP_Error The response from the API or a WP_Error object.
     */
    private function make_shopify_request( $query ) {
        $api_endpoint = SHOPIFY_API_ENDPOINT;
        $api_key = SHOPIFY_API_KEY;
        $api_password = SHOPIFY_API_PASSWORD;

        // Basic Authentication for Shopify Admin API
        $auth_string = base64_encode( "{$api_key}:{$api_password}" );

        $args = array(
            'method'    => 'POST',
            'timeout'   => 30,
            'headers'   => array(
                'Content-Type'  => 'application/json',
                'X-Shopify-Storefront-Access-Token' => 'YOUR_STOREFRONT_ACCESS_TOKEN', // Use Storefront API token for public data
                // For Admin API, use Basic Auth:
                // 'Authorization' => 'Basic ' . $auth_string,
            ),
            'body'      => json_encode( array( 'query' => $query ) ),
        );

        // IMPORTANT: For public data (products, collections), use the Storefront API.
        // You'll need a Storefront API access token. Replace 'YOUR_STOREFRONT_ACCESS_TOKEN' above.
        // If you need to access private data (orders, customers), you'll need the Admin API
        // and use the 'Authorization: Basic ...' header instead of 'X-Shopify-Storefront-Access-Token'.
        // Ensure your Shopify app has the correct permissions.

        $response = wp_remote_request( $api_endpoint, $args );

        return $response;
    }

    /**
     * Sends a JSON error response.
     *
     * @param string $message The error message.
     * @param int $status_code The HTTP status code.
     * @param mixed $details Optional details about the error.
     */
    private function send_error_response( $message, $status_code = 400, $details = null ) {
        header( 'Content-Type: application/json', true, $status_code );
        $response = array(
            'success' => false,
            'message' => $message,
        );
        if ( $details ) {
            $response['details'] = $details;
        }
        wp_send_json( $response, $status_code );
        exit;
    }

    /**
     * Flushes rewrite rules.
     * This static method is called on plugin activation/deactivation.
     */
    public static function flush_rewrite_rules() {
        // Ensure rewrite rules are flushed when the plugin is activated or deactivated.
        flush_rewrite_rules();
    }
}
?>

Configuration and Security Considerations

Before activating the plugin, ensure you have configured the constants at the top of shopify-headless-proxy.php:

  • SHOPIFY_API_ENDPOINT: This should be your Shopify GraphQL endpoint. For public data (products, collections), this is typically your Storefront API endpoint. For private data (orders, customers), it’s your Admin API endpoint.
  • SHOPIFY_API_KEY and SHOPIFY_API_PASSWORD: These are your Shopify API credentials. For the Storefront API, you’ll use a Storefront API access token. For the Admin API, you’ll use your API key and password.

Crucially, for accessing public product data, you should use the Shopify Storefront API. This requires a Storefront API access token, which is different from Admin API credentials. In the make_shopify_request method, ensure you are using the correct header: X-Shopify-Storefront-Access-Token with your Storefront API token. If you intend to access private data (like orders), you would switch to the Admin API endpoint and use the Authorization: Basic ... header.

Security Best Practices:

  • Never expose sensitive API keys or passwords directly in client-side JavaScript. This plugin acts as a server-side proxy, keeping these credentials secure within your WordPress environment.
  • Use environment variables or a secure configuration management system for storing API keys and passwords in a production environment, rather than hardcoding them directly in the plugin file.
  • Implement proper authentication and authorization if your API endpoints are intended for specific user roles or authenticated users within WordPress.
  • Rate Limiting: Be mindful of Shopify’s API rate limits. For high-traffic sites, consider implementing caching mechanisms or rate limiting on your WordPress endpoints.
  • Input Validation: Always validate and sanitize any input received from the client before using it in API requests (e.g., product IDs, search queries).

Activation and Testing

1. **Save your files:** Ensure all files are saved in their correct locations.

2. **Activate the plugin:** Go to your WordPress admin dashboard, navigate to “Plugins,” and activate “Shopify Headless Proxy.”

3. **Flush Rewrite Rules:** Because we’ve added rewrite rules, you need to flush them. This is handled automatically by the `register_activation_hook` and `register_deactivation_hook` in the main plugin file. If you encounter issues, you can manually flush them by going to Settings > Permalinks and clicking “Save Changes.”

4. **Test the endpoints:** You can now test your custom endpoints using tools like curl or Postman:

Fetching all products:

curl https://your-wordpress-site.com/api/shopify/products/

Fetching a single product (replace ‘1234567890’ with an actual product ID or handle if your rule is adjusted):

curl https://your-wordpress-site.com/api/shopify/products/1234567890/

If everything is configured correctly, you should receive a JSON response from Shopify, proxied through your WordPress site.

Extending the Proxy

This example provides a foundation for proxying Shopify’s product data. You can extend this pattern to:

  • Proxy other Shopify API endpoints (collections, customers, orders, etc.).
  • Implement more sophisticated data transformation and filtering before sending data to the client.
  • Add authentication checks to ensure only authorized users can access certain endpoints.
  • Integrate with WordPress’s caching mechanisms (e.g., Transients API) to reduce direct calls to Shopify and improve performance.
  • Handle different HTTP methods (POST, PUT, DELETE) for actions like creating or updating data, if your use case requires it and your Shopify API access allows it.

By utilizing the WordPress Rewrite API, you create a clean, maintainable, and secure way to integrate external services like Shopify headless into your WordPress ecosystem, abstracting away direct API complexities and enhancing your application’s architecture.

Primary Sidebar

A little about the Author

Having 12+ Years of Experience in Software Development, Vinay is a principal software architect, senior systems engineer, and elite technical consultant. He specializes in bespoke PHP/WordPress development, high-performance Magento 2 & Shopify architectures, custom plugin/theme development from scratch, and legacy code modernization (including VB6, VB.NET, PyQt, and Crystal Reports). Known for solving complex database bottlenecks, speed optimization (Core Web Vitals), and advanced security code auditing, Vinay engineers production-ready systems designed to scale under heavy concurrent load conditions.



Chat on WhatsApp

Recent Posts

  • Step-by-Step Guide to building a custom user session manager block for Gutenberg using Tailwind CSS isolated elements
  • Step-by-Step Guide to building a custom CSV bulk exporter block for Gutenberg using Vanilla JS Web Components
  • Optimizing p99 database query response latency in multi-site Model-View-Controller (MVC) modular custom tables
  • Step-by-Step Guide to building a custom automatic translation switcher block for Gutenberg using Vue micro-frontends
  • How to design secure Salesforce CRM webhook listeners using signature validation and payload queues

Categories

  • apache (1)
  • Business & Monetization (390)
  • Centos (4)
  • Comparisons & Decision Making (55)
  • Debian (2)
  • Debugging & Troubleshooting (653)
  • Desktop Applications (14)
  • DevOps (7)
  • DevOps & Cloud Scaling (962)
  • Django (1)
  • Laravel (4)
  • Migration & Architecture (192)
  • Mobile Applications (24)
  • MySQL (1)
  • Performance & Optimization (870)
  • PHP (5)
  • PHP Development (38)
  • Plugins & Themes (244)
  • Programming Languages (9)
  • Python (20)
  • Ruby on Rails (1)
  • Security & Compliance (638)
  • SEO & Growth (492)
  • Server (23)
  • Ubuntu (9)
  • VB6 & VB.NET (8)
  • Web Applications & Frontend (19)
  • Web Assembly (Wasm) (2)
  • WordPress (22)
  • WordPress Plugin Development (326)
  • WordPress Theme Development (357)

Recent Posts

  • Step-by-Step Guide to building a custom user session manager block for Gutenberg using Tailwind CSS isolated elements
  • Step-by-Step Guide to building a custom CSV bulk exporter block for Gutenberg using Vanilla JS Web Components
  • Optimizing p99 database query response latency in multi-site Model-View-Controller (MVC) modular custom tables

Top Categories

  • DevOps & Cloud Scaling (962)
  • Performance & Optimization (870)
  • Debugging & Troubleshooting (653)
  • Security & Compliance (638)
  • SEO & Growth (492)
  • Business & Monetization (390)

Our Products

  • ERP & LMS Systems (4)
  • Directories & Marketplaces (4)
  • Healthcare Portals (3)
  • Point of Sale (POS) (2)
  • E-Commerce Engines (2)

Our Services

  • E-Commerce Development (10)
  • WordPress Development (8)
  • Python & Desktop GUI (7)
  • General Consulting (7)
  • Legacy Modernization (5)
  • Mobile App Development (4)

Copyright © 2026 · Vinay Vengala