• 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 Metadata API (add_post_meta)

How to securely integrate Shopify headless API endpoints into WordPress custom plugins using Metadata API (add_post_meta)

Leveraging Shopify Headless API with WordPress Custom Plugins via Metadata API

Integrating external data sources into WordPress is a common requirement for modern web applications. When building a headless WordPress setup or augmenting a standard WordPress site with e-commerce functionality from a platform like Shopify, securely fetching and storing product data is paramount. This guide details how to integrate Shopify’s headless API endpoints into a custom WordPress plugin, specifically focusing on storing and retrieving product metadata using WordPress’s built-in `add_post_meta` function and related APIs.

Prerequisites and Setup

Before diving into the code, ensure you have the following:

  • A Shopify store with API credentials (Storefront API access token is recommended for public data).
  • A WordPress installation with a custom plugin skeleton.
  • Basic understanding of WordPress plugin development, PHP, and REST APIs.

For this example, we’ll assume you’re fetching product data from Shopify and want to associate it with a custom post type in WordPress, such as ‘products’.

Fetching Shopify Product Data

The Shopify Storefront API allows you to query product information. We’ll use PHP’s cURL functions to make these requests. It’s crucial to handle API keys securely, ideally by storing them in environment variables or a secure configuration file outside the webroot, and then accessing them via `get_option` or a custom settings API within WordPress.

Shopify Storefront API Query Example

Here’s a PHP function to fetch a list of products from Shopify. Replace YOUR_SHOPIFY_DOMAIN and YOUR_STOREFRONT_ACCESS_TOKEN with your actual credentials.

<?php
/**
 * Fetches product data from Shopify Storefront API.
 *
 * @return array|WP_Error An array of product data or a WP_Error object on failure.
 */
function fetch_shopify_products() {
    $shopify_domain = 'your-shopify-domain.myshopify.com'; // e.g., 'my-awesome-store.myshopify.com'
    $storefront_access_token = 'your-storefront-access-token'; // Get this from your Shopify admin

    $graphql_query = '{
        products(first: 10) {
            edges {
                node {
                    id
                    title
                    handle
                    descriptionHtml
                    images(first: 1) {
                        edges {
                            node {
                                url
                                altText
                            }
                        }
                    }
                    variants(first: 1) {
                        edges {
                            node {
                                id
                                priceV2 {
                                    amount
                                    currencyCode
                                }
                            }
                        }
                    }
                }
            }
        }
    }';

    $url = "https://{$shopify_domain}/api/2023-07/graphql.json"; // Adjust API version as needed

    $headers = [
        'Content-Type: application/json',
        'X-Shopify-Storefront-Access-Token: ' . $storefront_access_token,
    ];

    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, $url);
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode(['query' => $graphql_query]));
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
    curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true); // Important for security
    curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);

    $response = curl_exec($ch);
    $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    $curl_error = curl_error($ch);
    curl_close($ch);

    if ($curl_error) {
        return new WP_Error('shopify_api_curl_error', 'cURL Error: ' . $curl_error);
    }

    if ($http_code !== 200) {
        $error_message = "Shopify API request failed with HTTP code: {$http_code}. Response: " . $response;
        return new WP_Error('shopify_api_http_error', $error_message);
    }

    $data = json_decode($response, true);

    if (json_last_error() !== JSON_ERROR_NONE) {
        return new WP_Error('shopify_api_json_decode_error', 'Failed to decode Shopify API response.');
    }

    if (isset($data['errors'])) {
        return new WP_Error('shopify_api_graphql_errors', 'Shopify GraphQL Errors: ' . print_r($data['errors'], true));
    }

    if (isset($data['data']['products']['edges'])) {
        return $data['data']['products']['edges'];
    }

    return new WP_Error('shopify_api_no_products', 'No products found or unexpected response structure.');
}
?>

Integrating Data into WordPress Custom Post Types

Once you have the product data, you’ll want to store it within WordPress. This typically involves creating or updating custom post types and then using `add_post_meta` to attach the Shopify-specific data. We’ll create a function that iterates through the fetched products and adds them as posts, storing their Shopify IDs and other relevant details as post meta.

Creating/Updating Posts and Storing Metadata

This function will fetch products, check if a product with the same Shopify ID already exists in WordPress, and either create a new post or update existing meta. For simplicity, we’ll use the Shopify product’s `handle` as a unique identifier for checking existence. In a production environment, you might use the Shopify `id` directly.

<?php
/**
 * Imports Shopify products into WordPress custom post type and stores metadata.
 */
function import_shopify_products_to_wp() {
    $products_data = fetch_shopify_products();

    if (is_wp_error($products_data)) {
        error_log('Shopify Product Import Error: ' . $products_data->get_error_message());
        return;
    }

    if (empty($products_data)) {
        error_log('No Shopify products fetched or empty response.');
        return;
    }

    foreach ($products_data as $product_edge) {
        $product = $product_edge['node'];

        // Sanitize and prepare data
        $shopify_product_id = sanitize_key($product['id']); // Shopify's global ID
        $product_handle = sanitize_title($product['handle']); // For URL slugs and uniqueness
        $product_title = sanitize_text_field($product['title']);
        $product_description = wp_kses_post($product['descriptionHtml']); // Allow basic HTML

        // Get image URL and alt text
        $image_url = isset($product['images']['edges'][0]['node']['url']) ? esc_url_raw($product['images']['edges'][0]['node']['url']) : '';
        $image_alt = isset($product['images']['edges'][0]['node']['altText']) ? sanitize_text_field($product['images']['edges'][0]['node']['altText']) : '';

        // Get price and currency
        $price_amount = isset($product['variants']['edges'][0]['node']['priceV2']['amount']) ? floatval($product['variants']['edges'][0]['node']['priceV2']['amount']) : 0.00;
        $currency_code = isset($product['variants']['edges'][0]['node']['priceV2']['currencyCode']) ? sanitize_key($product['variants']['edges'][0]['node']['priceV2']['currencyCode']) : '';

        // Check if post already exists using Shopify ID as meta
        $existing_post_id = get_posts([
            'post_type' => 'product', // Your custom post type slug
            'meta_key' => '_shopify_product_id',
            'meta_value' => $shopify_product_id,
            'posts_per_page' => 1,
            'fields' => 'ids',
        ]);

        $post_id = false;

        if (!empty($existing_post_id)) {
            // Post exists, update it
            $post_id = $existing_post_id[0];
            $post_data = [
                'ID' => $post_id,
                'post_title' => $product_title,
                'post_content' => $product_description,
                'post_name' => $product_handle, // Update slug if needed
            ];
            wp_update_post($post_data);
            error_log("Updated Shopify product: {$product_title} (ID: {$shopify_product_id})");
        } else {
            // Post does not exist, create it
            $post_data = [
                'post_title' => $product_title,
                'post_content' => $product_description,
                'post_status' => 'publish',
                'post_type' => 'product', // Your custom post type slug
                'post_name' => $product_handle, // Use handle for slug
            ];
            $post_id = wp_insert_post($post_data);
            error_log("Inserted Shopify product: {$product_title} (ID: {$shopify_product_id})");
        }

        if ($post_id && !is_wp_error($post_id)) {
            // Store Shopify specific data as post meta
            // Using add_post_meta: it will add a new meta entry if the key doesn't exist,
            // or add another entry if it does. For unique values, use update_post_meta.
            // Here, we want to ensure these are unique, so update_post_meta is better.

            update_post_meta($post_id, '_shopify_product_id', $shopify_product_id);
            update_post_meta($post_id, '_shopify_product_handle', $product_handle);
            update_post_meta($post_id, '_shopify_product_image_url', $image_url);
            update_post_meta($post_id, '_shopify_product_image_alt', $image_alt);
            update_post_meta($post_id, '_shopify_product_price', $price_amount);
            update_post_meta($post_id, '_shopify_product_currency', $currency_code);

            // Optionally, handle featured image
            if (!empty($image_url)) {
                handle_remote_featured_image($post_id, $image_url, $product_title);
            }
        } else {
            error_log("Failed to insert or update post for Shopify product ID: {$shopify_product_id}");
        }
    }
}

/**
 * Helper function to set a remote image as the featured image.
 *
 * @param int $post_id The ID of the post to attach the image to.
 * @param string $image_url The URL of the remote image.
 * @param string $image_title The title for the attachment.
 */
function handle_remote_featured_image($post_id, $image_url, $image_title) {
    require_once(ABSPATH . 'wp-admin/includes/media.php');
    require_once(ABSPATH . 'wp-admin/includes/file.php');
    require_once(ABSPATH . 'wp-admin/includes/image.php');

    $attachment_id = media_sideload_image($image_url, $post_id, $image_title);

    if (!is_wp_error($attachment_id)) {
        set_post_thumbnail($post_id, $attachment_id);
    } else {
        error_log("Failed to set featured image for post ID {$post_id} from URL {$image_url}: " . $attachment_id->get_error_message());
    }
}
?>

Understanding `add_post_meta` vs. `update_post_meta`

The example above uses `update_post_meta`. It’s important to understand the difference:

  • add_post_meta( $post_id, $meta_key, $meta_value, $unique = false ): Adds a new meta entry. If $unique is true, it will only add the meta if the key does not already exist for that post. If $unique is false (default), it will add a new entry even if the key already exists, creating duplicate meta entries.
  • update_post_meta( $post_id, $meta_key, $meta_value, $prev_value = '' ): Updates an existing meta entry. If the meta key does not exist, it will be added. If multiple entries exist for the same key, only the first one matching $prev_value (if provided) will be updated. If $prev_value is empty, all matching entries are updated. This is generally preferred for ensuring a single, correct value for a given meta key.

For storing unique identifiers like _shopify_product_id or the product price, update_post_meta is the safer and more predictable choice to avoid data duplication and ensure data integrity.

Triggering the Import Process

You need a mechanism to trigger the import_shopify_products_to_wp() function. Common methods include:

  • Manual Trigger: A button in the WordPress admin area (e.g., on a custom settings page or a dedicated import screen).
  • WP-Cron: Schedule the import to run periodically (e.g., daily, hourly).
  • Webhooks: If Shopify can send webhooks for product updates, you could set up an endpoint in WordPress to receive these and trigger an update for specific products.

Example: Manual Trigger via Admin Button

This example adds a simple link to the WordPress admin bar that, when clicked, triggers the import. For a more robust solution, consider creating a dedicated admin page with a form and nonce verification.

<?php
/**
 * Add a link to the admin bar to trigger the Shopify import.
 */
function add_shopify_import_admin_bar_link() {
    global $wp_admin_bar;

    // Check if user can manage options and if the import function exists
    if (current_user_can('manage_options') && function_exists('import_shopify_products_to_wp')) {
        $wp_admin_bar->add_node([
            'id'    => 'shopify_import_products',
            'title' => __('Import Shopify Products', 'your-text-domain'),
            'href'  => wp_nonce_url(admin_url('admin-post.php?action=shopify_import_products'), 'shopify_import_products_nonce'),
            'meta'  => [
                'class' => 'shopify-import-link',
            ],
        ]);
    }
}
add_action('admin_bar_menu', 'add_shopify_import_admin_bar_link', 999);

/**
 * Handle the admin-POST request for Shopify product import.
 */
function handle_shopify_import_products_request() {
    // Verify nonce
    if (!isset($_GET['_wpnonce']) || !wp_verify_nonce($_GET['_wpnonce'], 'shopify_import_products_nonce')) {
        wp_die(__('Security check failed!', 'your-text-domain'));
    }

    // Check user capabilities
    if (!current_user_can('manage_options')) {
        wp_die(__('You do not have permission to perform this action.', 'your-text-domain'));
    }

    // Perform the import
    import_shopify_products_to_wp();

    // Redirect back to the previous page or a designated admin page
    $redirect_url = admin_url('admin.php?page=your-custom-admin-page'); // Replace with your actual admin page slug
    if (isset($_SERVER['HTTP_REFERER'])) {
        $redirect_url = $_SERVER['HTTP_REFERER'];
    }
    wp_redirect($redirect_url);
    exit;
}
add_action('admin_post_shopify_import_products', 'handle_shopify_import_products_request');
?>

Security Considerations

API Keys: Never hardcode your Shopify API keys directly in the plugin files. Use WordPress’s Settings API to create an options page where administrators can securely input these credentials. Store these options using `update_option` and retrieve them using `get_option`. Consider encrypting sensitive keys if possible, although for Storefront API tokens, this might be overkill if they are read-only.

Nonce Verification: Always use nonces for any action that modifies data or performs sensitive operations (like imports) triggered via GET or POST requests in the admin area. This prevents Cross-Site Request Forgery (CSRF) attacks.

Input Sanitization and Validation: Sanitize all data received from external APIs before storing it in your WordPress database. Use functions like `sanitize_text_field`, `esc_url_raw`, `wp_kses_post`, `floatval`, etc. Validate data types and formats.

Error Handling: Implement robust error handling for API requests (cURL errors, HTTP status codes, GraphQL errors) and database operations. Log errors appropriately using `error_log` or a dedicated logging plugin.

Conclusion

By combining Shopify’s headless API capabilities with WordPress’s powerful Metadata API (`add_post_meta` and `update_post_meta`), you can create sophisticated integrations. This approach allows you to leverage Shopify’s e-commerce engine while maintaining content flexibility and management within WordPress. Remember to prioritize security, proper data handling, and a user-friendly interface for triggering and managing these integrations.

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

  • Debugging Guide: Diagnosing PHP-FPM child process pool exhaustion in multi-site network environments with modern tools
  • Debugging and Resolving complex namespace class loading collisions issues during heavy concurrent database traffic
  • Step-by-Step Guide: Offloading high-frequency customer support tickets metadata writes to a Redis KV store
  • How to refactor legacy event ticket registers queries using modern WP_Query and custom Transient caching
  • Step-by-Step Guide: Offloading high-frequency member profile directories metadata writes to a Redis KV store

Categories

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

Recent Posts

  • Debugging Guide: Diagnosing PHP-FPM child process pool exhaustion in multi-site network environments with modern tools
  • Debugging and Resolving complex namespace class loading collisions issues during heavy concurrent database traffic
  • Step-by-Step Guide: Offloading high-frequency customer support tickets metadata writes to a Redis KV store

Top Categories

  • DevOps & Cloud Scaling (962)
  • Performance & Optimization (873)
  • WordPress Plugin Development (726)
  • Debugging & Troubleshooting (662)
  • Security & Compliance (647)
  • SEO & Growth (492)

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