Como sincronizar stock vía endpoint seguro en WooCommerce en WordPress

Contents

Introducción

Este artículo ofrece un tutorial completo y detallado para implementar la sincronización de stock en WooCommerce a través de un endpoint REST seguro. Se explica paso a paso cómo exponer un endpoint personalizado, qué modelo de autenticación y firma usar (HMAC timestamp), cómo procesar las peticiones en el servidor (actualizar stock por SKU o ID), y ejemplos de cliente (PHP, Python y cURL). También se tratan aspectos de seguridad, rendimiento y buenas prácticas para entornos de producción.

Requisitos previos

  • WordPress con WooCommerce instalado y activo.
  • Acceso para editar el archivo functions.php del tema hijo o crear un pequeño plugin personalizado.
  • Conexión HTTPS en el servidor (obligatorio para protección de credenciales y evitar ataques MITM).
  • Conocimientos básicos de PHP y del REST API de WordPress.

Visión general de la arquitectura

La idea es publicar un endpoint REST privado (por ejemplo: /wp-json/custom/v1/stock) que reciba lotes de actualizaciones de stock. Cada petición va firmada con HMAC usando una clave secreta compartida además se incluye un timestamp para evitar replays. El servidor valida la firma y la frescura del timestamp, procesa las actualizaciones (por SKU o product_id) usando las APIs CRUD de WooCommerce y devuelve un resumen con resultados y errores.

Diseño de la seguridad

  • Protocolo: HTTPS obligatorio.
  • Autenticación: API Key pública HMAC con clave secreta para firma de cada petición.
  • Protección contra replay: incluir un header X-Timestamp y aceptar solo requests dentro de una ventana (ej. ±300 segundos).
  • Tamaño de lote: limitar número de líneas por petición (ej. 200) para controlar carga.
  • Rotación de claves: planear rotación periódica de secret y registro/alertas en caso de fallos repetidos.
  • Rate limiting: aplicar límites por IP o por API Key (ej. 60/min) con plugin o firewall.

Contratos y formato de la petición

Recomendación de payload JSON (array de items). Se admite actualizar por sku o por product_id. La firma se calcula sobre: timestamp . JSON_CANONICAL (sin espacios ni saltos), y HMAC-SHA256 con la clave secreta.

Ejemplo de cuerpo (payload):

{
  items: [
    { sku: ABC-123, quantity: 10 },
    { product_id: 456, quantity: 0 },
    { sku: VAR-001, quantity: -2, operation: increment }
  ]
}

Headers esperados:

  • X-API-KEY: clave pública
  • X-TIMESTAMP: unix epoch en segundos
  • X-SIGNATURE: HMAC-SHA256 en hexa (por ejemplo: d2f…)

Implementación del endpoint (plugin o functions.php)

A continuación un ejemplo listo para colocar en un plugin o en el functions.php del tema hijo. El ejemplo registra la ruta REST, valida headers (API key, timestamp y firma) y actualiza stock usando la API CRUD de WooCommerce.

 POST,
            callback => custom_stock_update_callback,
            permission_callback => custom_stock_permission_callback,
        )
    )
} )

function custom_stock_permission_callback( request ) {
    headers = request->get_headers()

    api_key   = isset( headers[x-api-key][0] ) ? sanitize_text_field( headers[x-api-key][0] ) : 
    timestamp = isset( headers[x-timestamp][0] ) ? sanitize_text_field( headers[x-timestamp][0] ) : 
    signature = isset( headers[x-signature][0] ) ? sanitize_text_field( headers[x-signature][0] ) : 

    if ( empty( api_key )  empty( timestamp )  empty( signature ) ) {
        return new WP_Error( missing_headers, Faltan headers de autenticación, array( status => 401 ) )
    }

    if ( api_key !== STOCK_API_PUBLIC_KEY ) {
        return new WP_Error( invalid_key, API key inválida, array( status => 403 ) )
    }

    // Verificar timestamp (evitar replay)
    now = time()
    if ( abs( now - intval( timestamp ) ) > STOCK_API_TIME_WINDOW ) {
        return new WP_Error( stale_request, Timestamp fuera de ventana permitida, array( status => 403 ) )
    }

    // Calcular firma esperada: HMAC_SHA256( timestamp   .   canonical_json, secret )
    params = request->get_body() // cuerpo raw
    canonical = timestamp . . . params
    expected_sig = hash_hmac( sha256, canonical, STOCK_API_SECRET )

    // Comparación segura (timing-attack safe)
    if ( ! hash_equals( expected_sig, signature ) ) {
        return new WP_Error( invalid_signature, Firma HMAC no válida, array( status => 403 ) )
    }

    return true
}

function custom_stock_update_callback( request ) {
    body = json_decode( request->get_body(), true )
    if ( json_last_error() !== JSON_ERROR_NONE ) {
        return new WP_Error( invalid_json, JSON inválido, array( status => 400 ) )
    }

    if ( ! isset( body[items] )  ! is_array( body[items] ) ) {
        return new WP_Error( missing_items, Se requiere array items, array( status => 400 ) )
    }

    if ( count( body[items] ) > STOCK_API_MAX_ITEMS ) {
        return new WP_Error( too_many_items, Demasiados items en una sola petición, array( status => 413 ) )
    }

    results = array( success => array(), errors => array() )

    foreach ( body[items] as idx => item ) {
        line_id = idx   1
        sku = isset( item[sku] ) ? wc_clean( item[sku] ) : 
        product_id = isset( item[product_id] ) ? intval( item[product_id] ) : 0
        quantity = isset( item[quantity] ) ? intval( item[quantity] ) : null
        operation = isset( item[operation] ) ? item[operation] : set // set  increment  decrement

        if ( quantity === null ) {
            results[errors][] = array( line => line_id, message => Falta quantity )
            continue
        }

        // Resolver product_id por SKU si es necesario
        if ( empty( product_id )  ! empty( sku ) ) {
            product_id = wc_get_product_id_by_sku( sku )
            if ( ! product_id ) {
                results[errors][] = array( line => line_id, message => SKU no encontrado, sku => sku )
                continue
            }
        }

        if ( product_id <= 0 ) {
            results[errors][] = array( line => line_id, message => product_id inválido )
            continue
        }

        product = wc_get_product( product_id )
        if ( ! product ) {
            results[errors][] = array( line => line_id, message => Producto no encontrado, product_id => product_id )
            continue
        }

        // Manejo para productos que no gestionan stock: activarlo si se quiere forzar
        if ( ! product->managing_stock() ) {
            // Opción: activar gestión de stock automáticamente
            product->set_manage_stock( true )
        }

        // Calcular nuevo stock
        current_qty = product->get_stock_quantity()
        if ( current_qty === null ) {
            current_qty = 0
        }

        if ( operation === increment ) {
            new_qty = current_qty   quantity
        } elseif ( operation === decrement ) {
            new_qty = current_qty - quantity
        } else { // set
            new_qty = quantity
        }

        if ( new_qty < 0 ) {
            new_qty = 0
        }

        // Set stock y estado
        product->set_stock_quantity( intval( new_qty ) )
        if ( new_qty > 0 ) {
            product->set_stock_status( instock )
        } else {
            product->set_stock_status( outofstock )
        }

        try {
            product->save()
            results[success][] = array(
                line => line_id,
                product_id => product_id,
                sku => product->get_sku(),
                new_quantity => intval( new_qty )
            )
        } catch ( Exception e ) {
            results[errors][] = array( line => line_id, message => Error al guardar producto, exception => e->getMessage() )
        }
    }

    return rest_ensure_response( results )
}

Notas sobre la implementación PHP

  • Se usa wc_get_product y los métodos CRUD (set_stock_quantity, set_stock_status, save) recomendados por WooCommerce.
  • Se valida firma con hash_equals para evitar ataques por temporización.
  • En producción conviene mover las claves a opciones seguras o al archivo wp-config.php.
  • Si procesa grandes volúmenes, en lugar de guardar sincrónicamente conviene encolar las actualizaciones con Action Scheduler o WP-Cron para procesar en background.

Ejemplo de cliente en Python

Ejemplo que crea la firma HMAC y llama al endpoint con requests.

import time
import hmac
import hashlib
import json
import requests

API_KEY = tu_public_key_aqui
API_SECRET = tu_secret_key_aqui
URL = https://tudominio.com/wp-json/custom/v1/stock

payload = {
    items: [
        {sku: ABC-123, quantity: 5},
        {product_id: 456, quantity: 0}
    ]
}

body = json.dumps(payload, separators=(,, :))  # canonical JSON (sin espacios)
timestamp = str(int(time.time()))
canonical = timestamp   .   body
signature = hmac.new(API_SECRET.encode(utf-8), canonical.encode(utf-8), hashlib.sha256).hexdigest()

headers = {
    X-API-KEY: API_KEY,
    X-TIMESTAMP: timestamp,
    X-SIGNATURE: signature,
    Content-Type: application/json
}

resp = requests.post(URL, headers=headers, data=body, timeout=30)
print(resp.status_code, resp.text)

Ejemplo de cliente en PHP (cURL)

 array(
        array(sku => ABC-123, quantity => 5),
        array(product_id => 456, quantity => 0)
    )
)

body = json_encode(payload, JSON_UNESCAPED_SLASHES)
timestamp = time()
canonical = timestamp . . . body
signature = hash_hmac(sha256, canonical, api_secret)

ch = curl_init(url)
curl_setopt(ch, CURLOPT_RETURNTRANSFER, true)
curl_setopt(ch, CURLOPT_POST, true)
curl_setopt(ch, CURLOPT_POSTFIELDS, body)
curl_setopt(ch, CURLOPT_HTTPHEADER, array(
    Content-Type: application/json,
    X-API-KEY:  . api_key,
    X-TIMESTAMP:  . timestamp,
    X-SIGNATURE:  . signature
))
response = curl_exec(ch)
code = curl_getinfo(ch, CURLINFO_HTTP_CODE)
curl_close(ch)

echo HTTP: codenresponsen

Ejemplo con cURL (línea de comandos)

# Construir body en local y firmarlo con openssl o con script. Ejemplo simple (no recomendado para producción):
BODY={items:[{sku:ABC-123,quantity:5}]}
TIMESTAMP=(date  %s)
SECRET=tu_secret_key_aqui
CANONICAL={TIMESTAMP}.{BODY}
SIG=(printf %s CANONICAL  openssl dgst -sha256 -hmac SECRET  sed s/^. //)
curl -X POST https://tudominio.com/wp-json/custom/v1/stock 
  -H Content-Type: application/json 
  -H X-API-KEY: tu_public_key_aqui 
  -H X-TIMESTAMP: {TIMESTAMP} 
  -H X-SIGNATURE: {SIG} 
  -d {BODY}

Buenas prácticas y recomendaciones

  1. HTTPS obligatorio: nunca enviar credenciales por HTTP.
  2. Rate limiting y monitorización: aplicar límites y alertas (falla repetida de firmas puede indicar compromiso).
  3. Encolar actualizaciones grandes: usar Action Scheduler o colas para procesar lotes masivos sin bloquear la petición.
  4. Manejo de concurrencia: si múltiples sistemas actualizan stock, almacenar eventos en una cola y resolver consistencia con lógica eventual o locks si es crítico.
  5. Logs y auditoría: registrar peticiones fallidas y exitosas (evitar almacenar secretos en logs).
  6. Pruebas y sandbox: disponer de un entorno de prueba para validar mítines de carga y errores.
  7. Rotación y revocación de claves: permitir revocar claves comprometidas y rotarlas periódicamente.
  8. Validación adicional: asegurar que las cantidades entrantes sean valores razonables (p. ej. no aceptar inventarios absurdos que desborden enteros).

Casos especiales

  • Productos variables: si el SKU pertenece a una variación se actualizará la variación correcta. Asegúrate de que wc_get_product_id_by_sku funcione para variaciones.
  • Productos agrupados o compuestos: puede requerir lógica adicional para repercutir el stock en hijos/padres según tus reglas comerciales.
  • Pedidos pendientes: si se quiere reservar stock en pedidos, la sincronización debe coordinarse con la lógica de pedidos para evitar overselling.

Pruebas y verificación

  1. Probar primero en un entorno staging con certificados válidos.
  2. Enviar peticiones con body malformado y confirmar códigos 400/401/403 según corresponda.
  3. Verificar que la firma inválida o timestamp fuera de rango devuelven errores y no hacen cambios.
  4. Hacer pruebas de concurrencia simulada y validar que no se pierden actualizaciones.
  5. Probar variaciones, productos sin gestión de stock y comportamientos esperados cuando quantity = 0.

Enlaces útiles

Resumen

Crear un endpoint seguro para sincronizar stock en WooCommerce es factible y robusto si se siguen buenas prácticas: HTTPS, autenticación con HMAC timestamp, validaciones estrictas, y uso de las APIs CRUD propias de WooCommerce. Para volúmenes altos, conviene encolar las actualizaciones y aplicar rate limiting, mientras que en entornos críticos de inventario debes diseñar estrategias de concurrencia y auditoría.



Acepto donaciones de BAT's mediante el navegador Brave 🙂



Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *