How to sync stock via a secure endpoint in WooCommerce in WordPress

Contents

Introduction

This tutorial explains, in detail, how to implement a secure REST endpoint in WordPress/WooCommerce that allows an external system to synchronize stock levels. It covers security, payload formats, single and bulk updates, handling variations, concurrency concerns, testing, logging, and a full plugin-ready code example you can drop into a production site (use HTTPS). Every example and code snippet is included as runnable code adapt keys and environment settings for your installation.

Why a secure endpoint instead of direct DB access or webhook?

  • Security: A REST endpoint with authentication provides controlled access and logging.
  • Reliability: You can validate payloads and return consistent responses for retries.
  • Compatibility: Works with any external system that can call HTTPS endpoints (ERP, PIM, marketplaces).
  • Extensibility: You can add throttling, queuing, and background processing (Action Scheduler) later.

Prerequisites

  • WordPress 5.6 (for Application Passwords if you want that auth method), PHP 7.4 recommended.
  • WooCommerce installed and active.
  • HTTPS (TLS) configured for your site — do not expose credentials on plain HTTP.
  • Basic PHP and WordPress development ability (adding a small plugin or mu-plugin).

Security considerations (must read)

  • Transport: Use HTTPS only.
  • Authentication: Use token-based auth (custom header), Application Passwords, or JWT. Do not accept anonymous writes.
  • Permissions: Check that the calling account has appropriate capability or that the token is validated against a secret stored outside the database (preferably in wp-config.php or environment variable).
  • Rate limiting: Implement throttling to avoid accidental large updates — you can implement basic checks or use a firewall/proxy.
  • Validation: Validate incoming SKUs/IDs and quantities avoid negative stock unless explicitly required.
  • Logging: Log updates and failures to file or an audit table for troubleshooting and reconciliation.

Design overview

  1. Expose REST endpoints: /wp-json/stock-sync/v1/product and /wp-json/stock-sync/v1/bulk
  2. Authenticate requests using a secure token in a custom header (X-Stock-Api-Key) or using Application Passwords/Basic Auth
  3. Accept payloads with product ID or SKU (or variation SKU), and quantity
  4. Validate and update stock using WooCommerce APIs (wc_get_product, set_stock_quantity, save())
  5. Return JSON with success/failure and debug info

Payload formats

Two common payloads:

  • Single update (POST /wp-json/stock-sync/v1/product):

    JSON body examples:

    {
      sku: ABC-123,
      quantity: 25
    }

    or

    {
      product_id: 123,
      quantity: 25
    }
  • Bulk update (POST /wp-json/stock-sync/v1/bulk):

    JSON array of objects:

    [
      {sku: ABC-123, quantity: 25},
      {product_id: 124, quantity: 0},
      {sku: VAR-RED-L, quantity: 3}
    ]

Core implementation — register REST routes and handlers

The following plugin code registers two endpoints and implements secure permission checks based on a custom header token stored in wp-config.php or environment variable. It validates payloads, finds products by SKU or ID, updates stock using WooCommerce APIs, and returns detailed responses.

get_header()
    provided = request->get_header( x-stock-api-key )
    if ( empty( provided ) ) {
        return new WP_Error( stock_sync_no_key, API key is missing, array( status => 401 ) )
    }
    if ( ! defined( STOCK_SYNC_KEY )  empty( STOCK_SYNC_KEY ) ) {
        return new WP_Error( stock_sync_no_server_key, Server API key not configured, array( status => 500 ) )
    }
    if ( hash_equals( STOCK_SYNC_KEY, provided ) ) {
        return true
    }
    return new WP_Error( stock_sync_invalid_key, Invalid API key, array( status => 403 ) )
}

// Helper: resolve product by SKU or ID (supports variations)
function stock_sync_resolve_product( identifier ) {
    if ( isset( identifier[product_id] )  ! empty( identifier[product_id] ) ) {
        id = intval( identifier[product_id] )
        product = wc_get_product( id )
        if ( ! product ) {
            return new WP_Error( product_not_found, Product with ID {id} not found, array( status => 404 ) )
        }
        return product
    }
    if ( isset( identifier[sku] )  ! empty( identifier[sku] ) ) {
        sku = sanitize_text_field( identifier[sku] )
        // wc_get_product_id_by_sku can return a product or variation ID
        prod_id = wc_get_product_id_by_sku( sku )
        if ( ! prod_id ) {
            return new WP_Error( product_not_found, Product with SKU {sku} not found, array( status => 404 ) )
        }
        product = wc_get_product( prod_id )
        if ( ! product ) {
            return new WP_Error( product_not_found, Product ID {prod_id} (from SKU {sku}) not found, array( status => 404 ) )
        }
        return product
    }
    return new WP_Error( missing_identifier, Missing product_id or sku, array( status => 400 ) )
}

// Helper: update stock and return array with result information
function stock_sync_update_stock_for_product( product, quantity ) {
    // Ensure quantity is integer and non-negative unless you intentionally allow negative
    if ( ! is_numeric( quantity ) ) {
        return array( success => false, message => Invalid quantity )
    }
    quantity = intval( quantity )

    // If you want to reject negative values, do so:
    if ( quantity < 0 ) {
        return array( success => false, message => Quantity cannot be negative )
    }

    // Use WooCommerce CRUD methods
    try {
        // Ensure the product supports stock (simple/variation or manages stock)
        if ( ! product->managing_stock() ) {
            // enable stock management optionally, or return an error
            // product->set_manage_stock(true) // only if you want to enable it automatically
        }

        product->set_stock_quantity( quantity )

        // If you want to manage stock status:
        product->set_stock_status( quantity > 0 ? instock : outofstock )

        // Save product (this triggers appropriate WooCommerce actions)
        product->save()

        return array(
            success => true,
            product_id => product->get_id(),
            sku => product->get_sku(),
            quantity => quantity
        )
    } catch ( Exception e ) {
        return array( success => false, message => e->getMessage() )
    }
}

// Register REST routes
add_action( rest_api_init, function() {
    register_rest_route( stock-sync/v1, /product, array(
        array(
            methods             => POST,
            callback            => stock_sync_single_handler,
            permission_callback => stock_sync_permission_callback,
        ),
    ) )

    register_rest_route( stock-sync/v1, /bulk, array(
        array(
            methods             => POST,
            callback            => stock_sync_bulk_handler,
            permission_callback => stock_sync_permission_callback,
        ),
    ) )
} )

// Permission callback
function stock_sync_permission_callback( request ) {
    check = stock_sync_validate_key( request )
    if ( is_wp_error( check ) ) {
        return check
    }
    // Optionally also validate that a specific user exists or capability is present.
    return true
}

// Single handler
function stock_sync_single_handler( request ) {
    params = request->get_json_params()
    if ( empty( params ) ) {
        return new WP_REST_Response( array( success => false, message => Empty request body ), 400 )
    }

    product = stock_sync_resolve_product( params )
    if ( is_wp_error( product ) ) {
        return new WP_REST_Response( array( success => false, message => product->get_error_message() ), product->get_error_data()[status] ?? 400 )
    }

    if ( ! isset( params[quantity] ) ) {
        return new WP_REST_Response( array( success => false, message => Missing quantity ), 400 )
    }

    result = stock_sync_update_stock_for_product( product, params[quantity] )

    if ( result[success] ) {
        return new WP_REST_Response( result, 200 )
    }

    return new WP_REST_Response( result, 500 )
}

// Bulk handler
function stock_sync_bulk_handler( request ) {
    items = request->get_json_params()
    if ( empty( items )  ! is_array( items ) ) {
        return new WP_REST_Response( array( success => false, message => Expected JSON array ), 400 )
    }

    results = array()
    foreach ( items as index => item ) {
        // Basic validation
        if ( ! is_array( item ) ) {
            results[] = array( index => index, success => false, message => Item is not an object )
            continue
        }

        product = stock_sync_resolve_product( item )
        if ( is_wp_error( product ) ) {
            results[] = array( index => index, success => false, message => product->get_error_message() )
            continue
        }

        if ( ! isset( item[quantity] ) ) {
            results[] = array( index => index, success => false, message => Missing quantity )
            continue
        }

        res = stock_sync_update_stock_for_product( product, item[quantity] )
        res[index] = index
        results[] = res
    }

    return new WP_REST_Response( array( success => true, results => results ), 200 )
}

Where to store your secret key

  • Best: Add to wp-config.php: define(STOCK_SYNC_KEY, super-long-secret)
  • Alternative: Environment variable or server secrets store. Avoid using plain options in DB unless encrypted.

Example requests

Using the custom header token method (recommended approach in the plugin above). Replace your-token and domain/path as needed.

curl -X POST https://yourstore.com/wp-json/stock-sync/v1/product 
  -H Content-Type: application/json 
  -H X-Stock-Api-Key: your-token 
  -d {sku:ABC-123,quantity:12}

Bulk:

curl -X POST https://yourstore.com/wp-json/stock-sync/v1/bulk 
  -H Content-Type: application/json 
  -H X-Stock-Api-Key: your-token 
  -d [{sku:ABC-123,quantity:12},{product_id:124,quantity:0}]

Alternative: Application Passwords (WP built-in)

WordPress application passwords allow basic auth: username:application-password. Example:

curl -X POST https://yourstore.com/wp-json/stock-sync/v1/product 
  -u username:app-password 
  -H Content-Type: application/json 
  -d {sku:ABC-123,quantity:12}

To support this in permission callback, check current_user_can() or validate user credentials using WP REST authentication mechanisms. If using our plugin with STOCK_SYNC_KEY, either modify the code to accept basic auth or create a separate route with user capability checks.

Handling variations

  • Use SKU to identify variations uniquely. In WooCommerce, each variation can have its own SKU wc_get_product_id_by_sku() finds product or variation IDs by SKU.
  • If variations do not have individual SKUs, resolve by parent product ID attributes (requires more complex lookup via method that iterates variations).
  • Updating parent product stock for variable products is not typically used — manage variation stock individually.

Backorders, stock status, and thresholds

  • Backorders: If you allow backorders, decide whether incoming sync should update backorder settings. The sample code simply sets quantity and stock status.
  • Low-stock notifications: If you rely on WooCommerce low stock emails, ensure settings are correct. You can conditionally trigger those actions if needed.

Concurrency and race conditions

  • Concurrent updates can cause race conditions if two systems write at once. Strategies:
  • Use a single writer pattern or a queue (Action Scheduler or WP background processing) and ensure idempotency on the client side.
  • For operations like decrement/increment, prefer WooCommerce helper functions that are safe: wc_update_product_stock() with appropriate operation. For absolute set, use set_stock_quantity() as in the sample.

Scaling and bulk performance

  • For very large bulk updates (thousands of SKUs), either split requests into chunks or accept bulk import files (CSV) and process in the background with Action Scheduler or WP-CLI.
  • Avoid saving after each product if you can batch. However, WooCommerce product save triggers many hooks for safety use product->save() per item or implement a more advanced direct CRUD caching approach.

Logging and reconciliation

  • Log each incoming request and result (file or custom table). Example: use error_log for quick implementation or write a custom table for audit trail.
  • Provide endpoints to query last update time or failed items for reconciliation.

Testing

  • Test on staging first.
  • Use curl or a REST client (Postman) to validate headers, payloads, and responses.
  • Test edge cases: non-existing SKU, negative quantities, zero quantities, rapid consecutive updates.

Troubleshooting common errors

  • 401 Unauthorized: The request lacked the API key or was malformed. Ensure header name is X-Stock-Api-Key and the secret matches.
  • 403 Forbidden: Wrong key or permission check failed.
  • 404 Product not found: SKU or ID not found. Check SKU uniqueness.
  • 500 Server error: Inspect PHP error logs may be WooCommerce/plugin conflict.

Advanced improvements (optional)

  • Use JWT tokens (via a plugin) to authenticate and expire tokens.
  • Implement HMAC signatures on payloads to ensure integrity and replay protection.
  • Add rate limiting and IP allowlists on server or application firewall.
  • Provide a GET status endpoint for health checks and last-sync timestamp.
  • Push updates upstream with events to notify external systems on failure.

Example: GET status endpoint (optional)

This endpoint returns last sync time (youd store that when processing). Minimal example follows:

 GET,
        callback => function( request ) {
            last = get_option( stock_sync_last_run, null )
            return rest_ensure_response( array( ok => true, last_run => last ) )
        },
        permission_callback => function( request ) {
            // Optionally protect status too
            return true
        }
    ) )
} )

Full checklist before going live

  1. Add secret key to wp-config.php or environment and remove test keys.
  2. Use HTTPS and enforce it for the endpoint.
  3. Limit accepted payload size and validate JSON strictly.
  4. Test on staging with different SKU/ID scenarios and variations.
  5. Set up logging and error reporting to monitor issues.
  6. Consider adding rate-limiting or IP allowlisting at server or CDN level.
  7. Document the endpoint and payloads for integrators (include examples and error codes).

Useful links

Summary

A secure REST endpoint for stock synchronization in WooCommerce gives you a controlled, extensible way to keep inventory synchronized with external systems. Use HTTPS, strong authentication (custom header token, Application Passwords, or JWT), and WooCommerce CRUD APIs to set stock safely. For high volume, adopt queuing and background processing. Log everything and test thoroughly in staging before production.



Acepto donaciones de BAT's mediante el navegador Brave 🙂



Leave a Reply

Your email address will not be published. Required fields are marked *