Guía práctica · cURL y PHP

Conecta un producto con su inventario

Primero descubre y compara. Después, solo con autorización del vendedor, crea el mapeo y realiza un ajuste controlado.

1. Descubre sin escribir

Comienza con un PAT de solo lectura. Conserva el vendor_id de la primera llamada y consulta únicamente recursos de esa tienda. No necesitas crear una conexión para comparar productos e inventario.

cURL se ejecuta desde una terminal. PHP requiere PHP CLI con la extensión cURL; los ejemplos no son un plugin de WooCommerce. No los incrustes en el navegador ni en una página pública de WordPress.

Crear el token y obtener el vendor ID

Elige un producto y, si corresponde, una variación

Lista el catálogo, conserva el id del producto y consulta su detalle. Para un producto variable, selecciona una variación activa y conserva su id; para uno simple, variation_id será null.

Listar productos

Producto y variaciones

Solicitud · ejemplo ilustrativo

cURL
: "${MOKU_PAT:?}" &&
curl --silent --show-error --fail-with-body \
  'https://moku.cl/api/v1/vendors/vendor_demo_ceramica/products/product_demo_tazon' \
  --header "Authorization: Bearer ${MOKU_PAT}" \
  --header 'Accept: application/json'
PHP

Ejemplo para PHP CLI con la extensión cURL. No es un plugin de WordPress ni debe ejecutarse en el navegador.

<?php
$token = getenv('MOKU_PAT');
if ($token === false || $token === '') {
    throw new RuntimeException('MOKU_PAT');
}

$url = 'https://moku.cl/api/v1/vendors/vendor_demo_ceramica/products/product_demo_tazon';
$curl = curl_init($url);
if ($curl === false) {
    throw new RuntimeException('curl_init');
}
curl_setopt_array($curl, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST => 'GET',
    CURLOPT_CONNECTTIMEOUT => 10,
    CURLOPT_TIMEOUT => 30,
    CURLOPT_HTTPHEADER => [
        'Accept: application/json',
        'Authorization: Bearer ' . $token,
    ],
]);
$response = curl_exec($curl);
if ($response === false) {
    $message = curl_error($curl);
    curl_close($curl);
    throw new RuntimeException($message);
}
$status = curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
curl_close($curl);
$result = json_decode($response, false, 512, JSON_THROW_ON_ERROR);
$failed = $status < 200 || $status >= 300;
if ($failed) {
    fwrite(STDERR, "HTTP {$status}\n");
}
fwrite(
    $failed ? STDERR : STDOUT,
    json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR) . PHP_EOL
);
exit($failed ? 1 : 0);

Respuesta 200 application/json

{
  "data": {
    "id": "product_demo_tazon",
    "vendor_id": "vendor_demo_ceramica",
    "catalog_status": "active",
    "status": "published",
    "revision": 3,
    "kind": "variable",
    "name": "Tazón de cerámica",
    "slug": "tazon-de-ceramica",
    "description": "Tazón de cerámica hecho a mano.",
    "price": 15000,
    "original_price": null,
    "discount": 0,
    "category": "hogar",
    "subcategory": "ceramica",
    "image_urls": [
      "https://example.com/tazon-azul.jpg"
    ],
    "badge": null,
    "options": {
      "Color": [
        "Azul"
      ]
    },
    "default_options": {
      "Color": "Azul"
    },
    "variations": [
      {
        "id": "variation_demo_azul",
        "status": "active",
        "options": {
          "Color": "Azul"
        },
        "price": 15000,
        "original_price": null,
        "sku": "TAZ-AZUL"
      }
    ],
    "features": [
      "Hecho a mano"
    ],
    "related_product_ids": [],
    "tags": [
      "ceramica"
    ],
    "sku": null,
    "weight_kg": 0.4,
    "source_region": "metropolitana",
    "created_at": "2026-08-01T12:00:00.000Z",
    "updated_at": "2026-08-26T09:00:00.000Z",
    "published_at": "2026-08-01T12:00:00.000Z",
    "archived_at": null
  }
}

La respuesta de producto no contiene inventory_item_id, ni siquiera después de crearlo. No lo calcules: descúbrelo en inventario o en un mapeo existente.

Busca el item de inventario

Recorre inventory-items con limit y page_after. Compara product_id y variation_id en tu integración y guarda el id del item que coincide. Esta ruta no acepta un filtro product_id.

Inventario de la tienda

Solicitud · ejemplo ilustrativo

cURL
: "${MOKU_PAT:?}" &&
curl --silent --show-error --fail-with-body \
  'https://moku.cl/api/v1/vendors/vendor_demo_ceramica/inventory-items' \
  --header "Authorization: Bearer ${MOKU_PAT}" \
  --header 'Accept: application/json' \
  --get \
  --data-urlencode 'limit=50'
PHP

Ejemplo para PHP CLI con la extensión cURL. No es un plugin de WordPress ni debe ejecutarse en el navegador.

<?php
$token = getenv('MOKU_PAT');
if ($token === false || $token === '') {
    throw new RuntimeException('MOKU_PAT');
}

$url = 'https://moku.cl/api/v1/vendors/vendor_demo_ceramica/inventory-items';
$query = [
    'limit' => '50',
];
$url .= '?' . http_build_query($query, '', '&', PHP_QUERY_RFC3986);
$curl = curl_init($url);
if ($curl === false) {
    throw new RuntimeException('curl_init');
}
curl_setopt_array($curl, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST => 'GET',
    CURLOPT_CONNECTTIMEOUT => 10,
    CURLOPT_TIMEOUT => 30,
    CURLOPT_HTTPHEADER => [
        'Accept: application/json',
        'Authorization: Bearer ' . $token,
    ],
]);
$response = curl_exec($curl);
if ($response === false) {
    $message = curl_error($curl);
    curl_close($curl);
    throw new RuntimeException($message);
}
$status = curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
curl_close($curl);
$result = json_decode($response, false, 512, JSON_THROW_ON_ERROR);
$failed = $status < 200 || $status >= 300;
if ($failed) {
    fwrite(STDERR, "HTTP {$status}\n");
}
fwrite(
    $failed ? STDERR : STDOUT,
    json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR) . PHP_EOL
);
exit($failed ? 1 : 0);

Respuesta 200 application/json

{
  "data": [
    {
      "id": "WyJwcm9kdWN0X2RlbW9fdGF6b24iLCJ2YXJpYXRpb25fZGVtb19henVsIl0",
      "vendor_id": "vendor_demo_ceramica",
      "location_id": "default",
      "stock_pool_id": "default",
      "product_id": "product_demo_tazon",
      "product_name": "Tazón de cerámica",
      "product_image_url": "https://example.com/tazon-azul.jpg",
      "variation_id": "variation_demo_azul",
      "variation_options": {
        "Color": "Azul"
      },
      "sku": "TAZ-AZUL",
      "on_hand": 10,
      "reserved": 2,
      "available": 8,
      "version": 7,
      "status": "low_stock"
    },
    {
      "id": "WyJwcm9kdWN0X2RlbW9fdmFzbyIsbnVsbF0",
      "vendor_id": "vendor_demo_ceramica",
      "location_id": "default",
      "stock_pool_id": "default",
      "product_id": "product_demo_vaso",
      "product_name": "Vaso de greda",
      "product_image_url": "https://example.com/vaso-greda.jpg",
      "variation_id": null,
      "variation_options": {},
      "sku": "VAS-GREDA",
      "on_hand": 4,
      "reserved": 0,
      "available": 4,
      "version": 1,
      "status": "low_stock"
    }
  ],
  "page": {
    "next_cursor": null
  }
}

Si no hay coincidencia, completa todas las páginas antes de concluir que falta. Detén el flujo y revisa los IDs; no sustituyas una lectura fallida por stock cero ni inventes un item.

Reutiliza los mapeos que ya existen

La lista channel-listings abarca la tienda, no una sola conexión. Busca en tu integración la combinación connection_id y external_listing_id; verifica también producto y variación. Si ya existe, conserva su inventory_item_id y no vuelvas a crearlo.

Mapeos existentes

Solicitud · ejemplo ilustrativo

cURL
: "${MOKU_PAT:?}" &&
curl --silent --show-error --fail-with-body \
  'https://moku.cl/api/v1/vendors/vendor_demo_ceramica/channel-listings' \
  --header "Authorization: Bearer ${MOKU_PAT}" \
  --header 'Accept: application/json' \
  --get \
  --data-urlencode 'limit=50'
PHP

Ejemplo para PHP CLI con la extensión cURL. No es un plugin de WordPress ni debe ejecutarse en el navegador.

<?php
$token = getenv('MOKU_PAT');
if ($token === false || $token === '') {
    throw new RuntimeException('MOKU_PAT');
}

$url = 'https://moku.cl/api/v1/vendors/vendor_demo_ceramica/channel-listings';
$query = [
    'limit' => '50',
];
$url .= '?' . http_build_query($query, '', '&', PHP_QUERY_RFC3986);
$curl = curl_init($url);
if ($curl === false) {
    throw new RuntimeException('curl_init');
}
curl_setopt_array($curl, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST => 'GET',
    CURLOPT_CONNECTTIMEOUT => 10,
    CURLOPT_TIMEOUT => 30,
    CURLOPT_HTTPHEADER => [
        'Accept: application/json',
        'Authorization: Bearer ' . $token,
    ],
]);
$response = curl_exec($curl);
if ($response === false) {
    $message = curl_error($curl);
    curl_close($curl);
    throw new RuntimeException($message);
}
$status = curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
curl_close($curl);
$result = json_decode($response, false, 512, JSON_THROW_ON_ERROR);
$failed = $status < 200 || $status >= 300;
if ($failed) {
    fwrite(STDERR, "HTTP {$status}\n");
}
fwrite(
    $failed ? STDERR : STDOUT,
    json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR) . PHP_EOL
);
exit($failed ? 1 : 0);

Respuesta 200 application/json

{
  "data": [
    {
      "id": "lst_demo_tazon_azul",
      "vendor_id": "vendor_demo_ceramica",
      "connection_id": "con_demo_woocommerce",
      "product_id": "product_demo_tazon",
      "variation_id": "variation_demo_azul",
      "external_listing_id": "101:102",
      "external_product_id": "101",
      "external_variation_id": "102",
      "external_sku": "TAZ-AZUL",
      "state": "active",
      "source_revision": "2026-08-26T09:00:00Z",
      "inventory_item_id": "WyJwcm9kdWN0X2RlbW9fdGF6b24iLCJ2YXJpYXRpb25fZGVtb19henVsIl0",
      "version": 1,
      "created_at": "2026-08-26T09:01:00.000Z",
      "updated_at": "2026-08-26T09:01:00.000Z"
    }
  ],
  "page": {
    "next_cursor": null
  }
}
Consultar las conexiones existentes

Usa connections para identificar la cuenta externa y su estado. No crees otra conexión para el mismo canal durante una comparación de solo lectura.

Conexiones existentes

Solicitud · ejemplo ilustrativo

cURL
: "${MOKU_PAT:?}" &&
curl --silent --show-error --fail-with-body \
  'https://moku.cl/api/v1/vendors/vendor_demo_ceramica/connections' \
  --header "Authorization: Bearer ${MOKU_PAT}" \
  --header 'Accept: application/json' \
  --get \
  --data-urlencode 'limit=50'
PHP

Ejemplo para PHP CLI con la extensión cURL. No es un plugin de WordPress ni debe ejecutarse en el navegador.

<?php
$token = getenv('MOKU_PAT');
if ($token === false || $token === '') {
    throw new RuntimeException('MOKU_PAT');
}

$url = 'https://moku.cl/api/v1/vendors/vendor_demo_ceramica/connections';
$query = [
    'limit' => '50',
];
$url .= '?' . http_build_query($query, '', '&', PHP_QUERY_RFC3986);
$curl = curl_init($url);
if ($curl === false) {
    throw new RuntimeException('curl_init');
}
curl_setopt_array($curl, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST => 'GET',
    CURLOPT_CONNECTTIMEOUT => 10,
    CURLOPT_TIMEOUT => 30,
    CURLOPT_HTTPHEADER => [
        'Accept: application/json',
        'Authorization: Bearer ' . $token,
    ],
]);
$response = curl_exec($curl);
if ($response === false) {
    $message = curl_error($curl);
    curl_close($curl);
    throw new RuntimeException($message);
}
$status = curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
curl_close($curl);
$result = json_decode($response, false, 512, JSON_THROW_ON_ERROR);
$failed = $status < 200 || $status >= 300;
if ($failed) {
    fwrite(STDERR, "HTTP {$status}\n");
}
fwrite(
    $failed ? STDERR : STDOUT,
    json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR) . PHP_EOL
);
exit($failed ? 1 : 0);

Respuesta 200 application/json

{
  "data": [
    {
      "id": "con_demo_woocommerce",
      "vendor_id": "vendor_demo_ceramica",
      "provider": "woocommerce",
      "external_account_id": "https://tienda.example.com",
      "display_name": "WooCommerce de ejemplo",
      "status": "active",
      "roles": {
        "catalog": "source",
        "base_price": "source",
        "inventory": "destination",
        "order_ingress": "enabled",
        "external_order_fulfillment": "moku"
      },
      "health": {
        "state": "unknown",
        "last_observed_at": null,
        "last_success_at": null,
        "blockers": []
      },
      "active_listing_count": 1,
      "active_reservation_count": 0,
      "unresolved_external_order_count": 0,
      "active_channel_fulfillment_count": 0,
      "version": 2,
      "created_at": "2026-08-26T09:00:00.000Z",
      "updated_at": "2026-08-26T09:01:00.000Z"
    }
  ],
  "page": {
    "next_cursor": null
  }
}

Hasta aquí solo realizaste lecturas. Ya puedes comparar catálogo y disponibilidad con WooCommerce y registrar diferencias en tu integración, sin corregirlas automáticamente.

2. Elige la fuente de verdad con el vendedor

Consulta la autoridad vigente, no la supongas por el proveedor o el último ajuste. GET /authority informa las fuentes mutables de catálogo, precio e inventario por Stock Pool. El origen y el fulfillment quedan fijados en cada pedido; el preset solo define cómo ingresarán y quién preparará los pedidos futuros.

Autoridad vigente de la tienda

Solicitud · ejemplo ilustrativo

cURL
: "${MOKU_PAT:?}" &&
curl --silent --show-error --fail-with-body \
  'https://moku.cl/api/v1/vendors/vendor_demo_ceramica/authority' \
  --header "Authorization: Bearer ${MOKU_PAT}" \
  --header 'Accept: application/json'
PHP

Ejemplo para PHP CLI con la extensión cURL. No es un plugin de WordPress ni debe ejecutarse en el navegador.

<?php
$token = getenv('MOKU_PAT');
if ($token === false || $token === '') {
    throw new RuntimeException('MOKU_PAT');
}

$url = 'https://moku.cl/api/v1/vendors/vendor_demo_ceramica/authority';
$curl = curl_init($url);
if ($curl === false) {
    throw new RuntimeException('curl_init');
}
curl_setopt_array($curl, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST => 'GET',
    CURLOPT_CONNECTTIMEOUT => 10,
    CURLOPT_TIMEOUT => 30,
    CURLOPT_HTTPHEADER => [
        'Accept: application/json',
        'Authorization: Bearer ' . $token,
    ],
]);
$response = curl_exec($curl);
if ($response === false) {
    $message = curl_error($curl);
    curl_close($curl);
    throw new RuntimeException($message);
}
$status = curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
curl_close($curl);
$result = json_decode($response, false, 512, JSON_THROW_ON_ERROR);
$failed = $status < 200 || $status >= 300;
if ($failed) {
    fwrite(STDERR, "HTTP {$status}\n");
}
fwrite(
    $failed ? STDERR : STDOUT,
    json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR) . PHP_EOL
);
exit($failed ? 1 : 0);

Respuesta 200 application/json

{
  "data": {
    "vendor_id": "vendor_demo_ceramica",
    "catalog_master": {
      "type": "connection",
      "connection_id": "con_demo_woocommerce"
    },
    "base_price_master": {
      "type": "connection",
      "connection_id": "con_demo_woocommerce"
    },
    "inventory_pools": [
      {
        "stock_pool_id": "default",
        "master": {
          "type": "moku",
          "connection_id": null
        }
      }
    ]
  }
}
PresetCatálogo y precioInventarioIngreso y fulfillment futurosCuándo elegirlo
moku_sourceMokuMokuWooCommerce ingresa; Moku preparaMoku proyecta disponibilidad a publicaciones WooCommerce existentes y vinculadas; conserva su contenido y precios.
woocommerce_catalog_sourceCanal externoMokuWooCommerce ingresa; Moku preparaWooCommerce mantiene catálogo y precios; Moku controla stock y checkout.
woocommerce_sourceCanal externoCanal externoWooCommerce ingresa y preparaWooCommerce sigue siendo la fuente de verdad y Moku recibe sus observaciones.

Con woocommerce_source, el checkout nativo de Moku está deshabilitado en v1: todavía no existe una reserva remota que permita prometer ese stock. No cambies los roles solo para seguir el ejemplo si el vendedor quiere mantener el inventario en WooCommerce.

Roles, cambios de autoridad y requisitos

3. Escrituras: solo después de una decisión explícita

La siguiente receta usa woocommerce_catalog_source: WooCommerce controla catálogo y precio, mientras Moku controla inventario. Continúa únicamente con autorización del vendedor, un producto y cantidad acordados, los escritores anteriores detenidos y un PAT de lectura y escritura guardado en el servidor. La interfaz v1 concede el conjunto completo de scopes de escritura; no asumas que ese PAT solo puede ajustar stock.

Ver la receta con roles separados: puede modificar datos reales

Abrir esta sección no envía solicitudes ni habilita una integración. Las llamadas son ejemplos para revisar; no ejecutes el bloque completo como un script ni actives un loop bidireccional.

Los ejemplos de escritura se detienen salvo que definas MOKU_ALLOW_WRITES=1 en el entorno donde los ejecutas. Es una barrera local contra copias accidentales, no una opción de la API ni un modo de prueba. Actívala solo después de la autorización del vendedor y de reemplazar los datos ficticios.

A. Crea o reutiliza una conexión con roles separados

Si la lectura anterior ya encontró la conexión deseada, reutiliza su ID y omite este POST. Si no existe, crearla con woocommerce_catalog_source le asigna de inmediato el catálogo y los precios; el inventario continúa en Moku. No es una operación de diagnóstico.

Crear una conexión con autoridad híbrida

Solicitud · ejemplo ilustrativo

cURL
: "${MOKU_PAT:?}" &&
test "${MOKU_ALLOW_WRITES:-}" = "1" &&
curl --silent --show-error --fail-with-body \
  --request POST \
  'https://moku.cl/api/v1/vendors/vendor_demo_ceramica/connections' \
  --header "Authorization: Bearer ${MOKU_PAT}" \
  --header 'Accept: application/json' \
  --header 'Idempotency-Key: demo_connection_001' \
  --header 'Content-Type: application/json' \
  --data-raw '{
  "provider": "woocommerce",
  "external_account_id": "https://tienda.example.com",
  "display_name": "WooCommerce de ejemplo",
  "roles": {
    "catalog": "source",
    "base_price": "source",
    "inventory": "destination",
    "order_ingress": "enabled",
    "external_order_fulfillment": "moku"
  }
}'
PHP

Ejemplo para PHP CLI con la extensión cURL. No es un plugin de WordPress ni debe ejecutarse en el navegador.

<?php
$token = getenv('MOKU_PAT');
if ($token === false || $token === '') {
    throw new RuntimeException('MOKU_PAT');
}
if (getenv('MOKU_ALLOW_WRITES') !== '1') {
    throw new RuntimeException('MOKU_ALLOW_WRITES=1');
}

$url = 'https://moku.cl/api/v1/vendors/vendor_demo_ceramica/connections';
$body = <<<'MOKU_REQUEST_JSON'
{
  "provider": "woocommerce",
  "external_account_id": "https://tienda.example.com",
  "display_name": "WooCommerce de ejemplo",
  "roles": {
    "catalog": "source",
    "base_price": "source",
    "inventory": "destination",
    "order_ingress": "enabled",
    "external_order_fulfillment": "moku"
  }
}
MOKU_REQUEST_JSON;
$curl = curl_init($url);
if ($curl === false) {
    throw new RuntimeException('curl_init');
}
curl_setopt_array($curl, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST => 'POST',
    CURLOPT_CONNECTTIMEOUT => 10,
    CURLOPT_TIMEOUT => 30,
    CURLOPT_HTTPHEADER => [
        'Accept: application/json',
        'Authorization: Bearer ' . $token,
        'Idempotency-Key: demo_connection_001',
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => $body,
]);
$response = curl_exec($curl);
if ($response === false) {
    $message = curl_error($curl);
    curl_close($curl);
    throw new RuntimeException($message);
}
$status = curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
curl_close($curl);
$result = json_decode($response, false, 512, JSON_THROW_ON_ERROR);
$failed = $status < 200 || $status >= 300;
if ($failed) {
    fwrite(STDERR, "HTTP {$status}\n");
}
fwrite(
    $failed ? STDERR : STDOUT,
    json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR) . PHP_EOL
);
exit($failed ? 1 : 0);

Respuesta 201 application/json

{
  "data": {
    "id": "con_demo_woocommerce",
    "vendor_id": "vendor_demo_ceramica",
    "provider": "woocommerce",
    "external_account_id": "https://tienda.example.com",
    "display_name": "WooCommerce de ejemplo",
    "status": "active",
    "roles": {
      "catalog": "source",
      "base_price": "source",
      "inventory": "destination",
      "order_ingress": "enabled",
      "external_order_fulfillment": "moku"
    },
    "health": {
      "state": "unknown",
      "last_observed_at": null,
      "last_success_at": null,
      "blockers": []
    },
    "active_listing_count": 0,
    "active_reservation_count": 0,
    "unresolved_external_order_count": 0,
    "active_channel_fulfillment_count": 0,
    "version": 1,
    "created_at": "2026-08-26T09:00:00.000Z",
    "updated_at": "2026-08-26T09:00:00.000Z"
  }
}

Guarda data.id como connection_id y vuelve a leer authority. Detente si no coincide con lo acordado. No desconectes ni reemplaces otros roles para forzar que el ejemplo funcione.

B. Usa identidades reales del catálogo

Reutiliza el producto y la variación descubiertos. Si realmente falta un producto, su creación es otra escritura que requiere aprobación: comienza como borrador, crea inventario en cero y devuelve IDs de producto y variaciones, pero no el ID de inventario. Incluye por separado catalog_authority_connection_id y base_price_authority_connection_id; usa null en el dominio que controla Moku.

Contrato para crear un borrador

C. Mapea la publicación externa y guarda el inventario

Relaciona product_id y variation_id de Moku con los IDs del producto y la variación de WooCommerce. Para un producto simple, envía los campos de variación como null. El SKU es una etiqueta, no una clave de identidad.

Vincular una variación a un listing

Solicitud · ejemplo ilustrativo

cURL
: "${MOKU_PAT:?}" &&
test "${MOKU_ALLOW_WRITES:-}" = "1" &&
curl --silent --show-error --fail-with-body \
  --request POST \
  'https://moku.cl/api/v1/vendors/vendor_demo_ceramica/channel-listings' \
  --header "Authorization: Bearer ${MOKU_PAT}" \
  --header 'Accept: application/json' \
  --header 'Idempotency-Key: demo_listing_001' \
  --header 'Content-Type: application/json' \
  --data-raw '{
  "connection_id": "con_demo_woocommerce",
  "product_id": "product_demo_tazon",
  "variation_id": "variation_demo_azul",
  "external_listing_id": "101:102",
  "external_product_id": "101",
  "external_variation_id": "102",
  "external_sku": "TAZ-AZUL",
  "state": "active",
  "source_revision": "2026-08-26T09:00:00Z"
}'
PHP

Ejemplo para PHP CLI con la extensión cURL. No es un plugin de WordPress ni debe ejecutarse en el navegador.

<?php
$token = getenv('MOKU_PAT');
if ($token === false || $token === '') {
    throw new RuntimeException('MOKU_PAT');
}
if (getenv('MOKU_ALLOW_WRITES') !== '1') {
    throw new RuntimeException('MOKU_ALLOW_WRITES=1');
}

$url = 'https://moku.cl/api/v1/vendors/vendor_demo_ceramica/channel-listings';
$body = <<<'MOKU_REQUEST_JSON'
{
  "connection_id": "con_demo_woocommerce",
  "product_id": "product_demo_tazon",
  "variation_id": "variation_demo_azul",
  "external_listing_id": "101:102",
  "external_product_id": "101",
  "external_variation_id": "102",
  "external_sku": "TAZ-AZUL",
  "state": "active",
  "source_revision": "2026-08-26T09:00:00Z"
}
MOKU_REQUEST_JSON;
$curl = curl_init($url);
if ($curl === false) {
    throw new RuntimeException('curl_init');
}
curl_setopt_array($curl, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST => 'POST',
    CURLOPT_CONNECTTIMEOUT => 10,
    CURLOPT_TIMEOUT => 30,
    CURLOPT_HTTPHEADER => [
        'Accept: application/json',
        'Authorization: Bearer ' . $token,
        'Idempotency-Key: demo_listing_001',
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => $body,
]);
$response = curl_exec($curl);
if ($response === false) {
    $message = curl_error($curl);
    curl_close($curl);
    throw new RuntimeException($message);
}
$status = curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
curl_close($curl);
$result = json_decode($response, false, 512, JSON_THROW_ON_ERROR);
$failed = $status < 200 || $status >= 300;
if ($failed) {
    fwrite(STDERR, "HTTP {$status}\n");
}
fwrite(
    $failed ? STDERR : STDOUT,
    json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR) . PHP_EOL
);
exit($failed ? 1 : 0);

Respuesta 201 application/json

{
  "data": {
    "id": "lst_demo_tazon_azul",
    "vendor_id": "vendor_demo_ceramica",
    "connection_id": "con_demo_woocommerce",
    "product_id": "product_demo_tazon",
    "variation_id": "variation_demo_azul",
    "external_listing_id": "101:102",
    "external_product_id": "101",
    "external_variation_id": "102",
    "external_sku": "TAZ-AZUL",
    "state": "active",
    "source_revision": "2026-08-26T09:00:00Z",
    "inventory_item_id": "WyJwcm9kdWN0X2RlbW9fdGF6b24iLCJ2YXJpYXRpb25fZGVtb19henVsIl0",
    "version": 1,
    "created_at": "2026-08-26T09:01:00.000Z",
    "updated_at": "2026-08-26T09:01:00.000Z"
  }
}

Guarda juntos data.id, data.connection_id, los IDs de ambos sistemas y data.inventory_item_id. Este último es el ID que usarás en la URL de inventario. El mapeo no publica un producto en WooCommerce ni instala un conector.

D. Lee la versión justo antes de ajustar

Consulta el item por su ID guardado y usa data.version como expected_version. No reutilices una versión de una sincronización anterior. En el ejemplo hay 10 unidades físicas, 2 reservadas y 8 disponibles, en la versión 7.

Stock y versión del item

Solicitud · ejemplo ilustrativo

cURL
: "${MOKU_PAT:?}" &&
curl --silent --show-error --fail-with-body \
  'https://moku.cl/api/v1/vendors/vendor_demo_ceramica/inventory-items/WyJwcm9kdWN0X2RlbW9fdGF6b24iLCJ2YXJpYXRpb25fZGVtb19henVsIl0' \
  --header "Authorization: Bearer ${MOKU_PAT}" \
  --header 'Accept: application/json'
PHP

Ejemplo para PHP CLI con la extensión cURL. No es un plugin de WordPress ni debe ejecutarse en el navegador.

<?php
$token = getenv('MOKU_PAT');
if ($token === false || $token === '') {
    throw new RuntimeException('MOKU_PAT');
}

$url = 'https://moku.cl/api/v1/vendors/vendor_demo_ceramica/inventory-items/WyJwcm9kdWN0X2RlbW9fdGF6b24iLCJ2YXJpYXRpb25fZGVtb19henVsIl0';
$curl = curl_init($url);
if ($curl === false) {
    throw new RuntimeException('curl_init');
}
curl_setopt_array($curl, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST => 'GET',
    CURLOPT_CONNECTTIMEOUT => 10,
    CURLOPT_TIMEOUT => 30,
    CURLOPT_HTTPHEADER => [
        'Accept: application/json',
        'Authorization: Bearer ' . $token,
    ],
]);
$response = curl_exec($curl);
if ($response === false) {
    $message = curl_error($curl);
    curl_close($curl);
    throw new RuntimeException($message);
}
$status = curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
curl_close($curl);
$result = json_decode($response, false, 512, JSON_THROW_ON_ERROR);
$failed = $status < 200 || $status >= 300;
if ($failed) {
    fwrite(STDERR, "HTTP {$status}\n");
}
fwrite(
    $failed ? STDERR : STDOUT,
    json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR) . PHP_EOL
);
exit($failed ? 1 : 0);

Respuesta 200 application/json

{
  "data": {
    "id": "WyJwcm9kdWN0X2RlbW9fdGF6b24iLCJ2YXJpYXRpb25fZGVtb19henVsIl0",
    "vendor_id": "vendor_demo_ceramica",
    "location_id": "default",
    "stock_pool_id": "default",
    "product_id": "product_demo_tazon",
    "product_name": "Tazón de cerámica",
    "product_image_url": "https://example.com/tazon-azul.jpg",
    "variation_id": "variation_demo_azul",
    "variation_options": {
      "Color": "Azul"
    },
    "sku": "TAZ-AZUL",
    "on_hand": 10,
    "reserved": 2,
    "available": 8,
    "version": 7,
    "status": "low_stock"
  }
}

E. Ajusta una cantidad física aprobada

El ejemplo establece target_on_hand: 8, no resta ocho unidades ni fija ocho disponibles. Con dos reservadas, el resultado es seis disponibles y una nueva versión. reserved y available son calculados por Moku; no los envíes en el cuerpo.

Con woocommerce_catalog_source, omite authority_connection_id al ajustar inventario, porque el dueño es Moku. Este ajuste representa una corrección física acordada, no un permiso para sobreescribir periódicamente el stock Moku con cantidades de WooCommerce. Si WooCommerce controla inventario mediante woocommerce_source, ese es otro flujo y debe enviar el ID exacto de la conexión propietaria.

Ajustar stock físico de forma idempotente

Solicitud · ejemplo ilustrativo

cURL
: "${MOKU_PAT:?}" &&
test "${MOKU_ALLOW_WRITES:-}" = "1" &&
curl --silent --show-error --fail-with-body \
  --request POST \
  'https://moku.cl/api/v1/vendors/vendor_demo_ceramica/inventory-items/WyJwcm9kdWN0X2RlbW9fdGF6b24iLCJ2YXJpYXRpb25fZGVtb19henVsIl0/adjustments' \
  --header "Authorization: Bearer ${MOKU_PAT}" \
  --header 'Accept: application/json' \
  --header 'Idempotency-Key: demo_stock_001' \
  --header 'Content-Type: application/json' \
  --data-raw '{
  "target_on_hand": 8,
  "expected_version": 7
}'
PHP

Ejemplo para PHP CLI con la extensión cURL. No es un plugin de WordPress ni debe ejecutarse en el navegador.

<?php
$token = getenv('MOKU_PAT');
if ($token === false || $token === '') {
    throw new RuntimeException('MOKU_PAT');
}
if (getenv('MOKU_ALLOW_WRITES') !== '1') {
    throw new RuntimeException('MOKU_ALLOW_WRITES=1');
}

$url = 'https://moku.cl/api/v1/vendors/vendor_demo_ceramica/inventory-items/WyJwcm9kdWN0X2RlbW9fdGF6b24iLCJ2YXJpYXRpb25fZGVtb19henVsIl0/adjustments';
$body = <<<'MOKU_REQUEST_JSON'
{
  "target_on_hand": 8,
  "expected_version": 7
}
MOKU_REQUEST_JSON;
$curl = curl_init($url);
if ($curl === false) {
    throw new RuntimeException('curl_init');
}
curl_setopt_array($curl, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST => 'POST',
    CURLOPT_CONNECTTIMEOUT => 10,
    CURLOPT_TIMEOUT => 30,
    CURLOPT_HTTPHEADER => [
        'Accept: application/json',
        'Authorization: Bearer ' . $token,
        'Idempotency-Key: demo_stock_001',
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => $body,
]);
$response = curl_exec($curl);
if ($response === false) {
    $message = curl_error($curl);
    curl_close($curl);
    throw new RuntimeException($message);
}
$status = curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
curl_close($curl);
$result = json_decode($response, false, 512, JSON_THROW_ON_ERROR);
$failed = $status < 200 || $status >= 300;
if ($failed) {
    fwrite(STDERR, "HTTP {$status}\n");
}
fwrite(
    $failed ? STDERR : STDOUT,
    json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR) . PHP_EOL
);
exit($failed ? 1 : 0);

Respuesta 200 application/json

{
  "data": {
    "id": "WyJwcm9kdWN0X2RlbW9fdGF6b24iLCJ2YXJpYXRpb25fZGVtb19henVsIl0",
    "vendor_id": "vendor_demo_ceramica",
    "location_id": "default",
    "stock_pool_id": "default",
    "product_id": "product_demo_tazon",
    "product_name": "Tazón de cerámica",
    "product_image_url": "https://example.com/tazon-azul.jpg",
    "variation_id": "variation_demo_azul",
    "variation_options": {
      "Color": "Azul"
    },
    "sku": "TAZ-AZUL",
    "on_hand": 8,
    "reserved": 2,
    "available": 6,
    "version": 8,
    "status": "low_stock"
  }
}

Genera una Idempotency-Key distinta por intención de ajuste y consérvala con el cuerpo y el resultado. Ante un timeout, una repetición idéntica con el mismo PAT, clave y operación recupera el recibo durante 30 días. Si cambia el cuerpo o la intención, usa una clave nueva. Una credencial nueva no comparte los recibos de la anterior.

F. Resuelve conflictos sin sobrescribir a ciegas

La muestra siguiente intenta una operación distinta con una versión antigua; no es una repetición del ajuste exitoso anterior. Un 409 INVENTORY_VERSION_CONFLICT no se soluciona aumentando un número local.

Conflicto por una versión antigua

Solicitud · ejemplo ilustrativo

cURL
: "${MOKU_PAT:?}" &&
test "${MOKU_ALLOW_WRITES:-}" = "1" &&
curl --silent --show-error --fail-with-body \
  --request POST \
  'https://moku.cl/api/v1/vendors/vendor_demo_ceramica/inventory-items/WyJwcm9kdWN0X2RlbW9fdGF6b24iLCJ2YXJpYXRpb25fZGVtb19henVsIl0/adjustments' \
  --header "Authorization: Bearer ${MOKU_PAT}" \
  --header 'Accept: application/json' \
  --header 'Idempotency-Key: demo_stock_002' \
  --header 'Content-Type: application/json' \
  --data-raw '{
  "target_on_hand": 9,
  "expected_version": 7
}'
PHP

Ejemplo para PHP CLI con la extensión cURL. No es un plugin de WordPress ni debe ejecutarse en el navegador.

<?php
$token = getenv('MOKU_PAT');
if ($token === false || $token === '') {
    throw new RuntimeException('MOKU_PAT');
}
if (getenv('MOKU_ALLOW_WRITES') !== '1') {
    throw new RuntimeException('MOKU_ALLOW_WRITES=1');
}

$url = 'https://moku.cl/api/v1/vendors/vendor_demo_ceramica/inventory-items/WyJwcm9kdWN0X2RlbW9fdGF6b24iLCJ2YXJpYXRpb25fZGVtb19henVsIl0/adjustments';
$body = <<<'MOKU_REQUEST_JSON'
{
  "target_on_hand": 9,
  "expected_version": 7
}
MOKU_REQUEST_JSON;
$curl = curl_init($url);
if ($curl === false) {
    throw new RuntimeException('curl_init');
}
curl_setopt_array($curl, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST => 'POST',
    CURLOPT_CONNECTTIMEOUT => 10,
    CURLOPT_TIMEOUT => 30,
    CURLOPT_HTTPHEADER => [
        'Accept: application/json',
        'Authorization: Bearer ' . $token,
        'Idempotency-Key: demo_stock_002',
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => $body,
]);
$response = curl_exec($curl);
if ($response === false) {
    $message = curl_error($curl);
    curl_close($curl);
    throw new RuntimeException($message);
}
$status = curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
curl_close($curl);
$result = json_decode($response, false, 512, JSON_THROW_ON_ERROR);
$failed = $status < 200 || $status >= 300;
if ($failed) {
    fwrite(STDERR, "HTTP {$status}\n");
}
fwrite(
    $failed ? STDERR : STDOUT,
    json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR) . PHP_EOL
);
exit($failed ? 1 : 0);

Respuesta 409 application/problem+json

{
  "type": "https://developers.moku.cl/reference/errors/#inventory-version-conflict",
  "title": "Inventory version conflict",
  "status": 409,
  "code": "INVENTORY_VERSION_CONFLICT",
  "detail": "Inventory changed; retrieve the item and retry with a new idempotency key.",
  "request_id": "req_00000000000000000000000000000001"
}
  1. Vuelve a leer el item y la autoridad. Revisa si otra venta, reserva o ajuste cambió el estado.
  2. Recalcula la cantidad física con la fuente acordada. Si no puedes reconciliar la diferencia, detente y pide una decisión operativa.
  3. Solo si la intención sigue siendo válida, envía la versión recién leída y una clave nueva. No reintentes en un loop infinito.

Ante ON_HAND_BELOW_RESERVED, no liberes compromisos de clientes para forzar un ajuste. Ante AUTHORITY_CONFLICT, detén escrituras y revisa quién controla el dominio.

Códigos de error y recuperación

PATCH reemplaza los campos editables

Aunque la ruta usa PATCH, no acepta un parche parcial con solo el campo cambiado. Construye un cuerpo completo según ProductUpdateRequest, conserva los valores editables que no quieres cambiar y agrega expected_revision de la última lectura.

No reenvíes a ciegas la respuesta de GET: elimina los campos de solo lectura como id, vendor_id, revision, el status del producto y sus timestamps. Conserva los IDs y estados de las variaciones existentes cuando el esquema los requiera; no cambies kind.

Incluye por separado catalog_authority_connection_id y base_price_authority_connection_id; usa el ID de cada fuente o null cuando Moku controla ese dominio. Omitir campos opcionales puede vaciar o restablecer esos valores; envía los que quieres conservar. Publicar, despublicar, archivar y restaurar usan operaciones separadas.

Ver un cuerpo completo de actualización y su respuesta

Este ejemplo es una escritura en producción y requiere autorización. Muestra el cambio desde la revisión 3 a la 4; no lo ejecutes sobre tu catálogo sin reconstruir el cuerpo con sus datos actuales.

Reemplazar los campos editables del producto

Solicitud · ejemplo ilustrativo

cURL
: "${MOKU_PAT:?}" &&
test "${MOKU_ALLOW_WRITES:-}" = "1" &&
curl --silent --show-error --fail-with-body \
  --request PATCH \
  'https://moku.cl/api/v1/vendors/vendor_demo_ceramica/products/product_demo_tazon' \
  --header "Authorization: Bearer ${MOKU_PAT}" \
  --header 'Accept: application/json' \
  --header 'Content-Type: application/json' \
  --data-raw '{
  "kind": "variable",
  "name": "Tazón de cerámica",
  "slug": "tazon-de-ceramica",
  "description": "Tazón de cerámica hecho a mano, esmaltado en azul.",
  "price": 15000,
  "original_price": null,
  "category": "hogar",
  "subcategory": "ceramica",
  "image_urls": [
    "https://example.com/tazon-azul.jpg"
  ],
  "badge": null,
  "options": {
    "Color": [
      "Azul"
    ]
  },
  "default_options": {
    "Color": "Azul"
  },
  "variations": [
    {
      "id": "variation_demo_azul",
      "status": "active",
      "options": {
        "Color": "Azul"
      },
      "price": 15000,
      "original_price": null,
      "sku": "TAZ-AZUL"
    }
  ],
  "features": [
    "Hecho a mano"
  ],
  "related_product_ids": [],
  "tags": [
    "ceramica"
  ],
  "sku": null,
  "weight_kg": 0.4,
  "source_region": "metropolitana",
  "expected_revision": 3,
  "catalog_authority_connection_id": "con_demo_woocommerce",
  "base_price_authority_connection_id": "con_demo_woocommerce"
}'
PHP

Ejemplo para PHP CLI con la extensión cURL. No es un plugin de WordPress ni debe ejecutarse en el navegador.

<?php
$token = getenv('MOKU_PAT');
if ($token === false || $token === '') {
    throw new RuntimeException('MOKU_PAT');
}
if (getenv('MOKU_ALLOW_WRITES') !== '1') {
    throw new RuntimeException('MOKU_ALLOW_WRITES=1');
}

$url = 'https://moku.cl/api/v1/vendors/vendor_demo_ceramica/products/product_demo_tazon';
$body = <<<'MOKU_REQUEST_JSON'
{
  "kind": "variable",
  "name": "Tazón de cerámica",
  "slug": "tazon-de-ceramica",
  "description": "Tazón de cerámica hecho a mano, esmaltado en azul.",
  "price": 15000,
  "original_price": null,
  "category": "hogar",
  "subcategory": "ceramica",
  "image_urls": [
    "https://example.com/tazon-azul.jpg"
  ],
  "badge": null,
  "options": {
    "Color": [
      "Azul"
    ]
  },
  "default_options": {
    "Color": "Azul"
  },
  "variations": [
    {
      "id": "variation_demo_azul",
      "status": "active",
      "options": {
        "Color": "Azul"
      },
      "price": 15000,
      "original_price": null,
      "sku": "TAZ-AZUL"
    }
  ],
  "features": [
    "Hecho a mano"
  ],
  "related_product_ids": [],
  "tags": [
    "ceramica"
  ],
  "sku": null,
  "weight_kg": 0.4,
  "source_region": "metropolitana",
  "expected_revision": 3,
  "catalog_authority_connection_id": "con_demo_woocommerce",
  "base_price_authority_connection_id": "con_demo_woocommerce"
}
MOKU_REQUEST_JSON;
$curl = curl_init($url);
if ($curl === false) {
    throw new RuntimeException('curl_init');
}
curl_setopt_array($curl, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST => 'PATCH',
    CURLOPT_CONNECTTIMEOUT => 10,
    CURLOPT_TIMEOUT => 30,
    CURLOPT_HTTPHEADER => [
        'Accept: application/json',
        'Authorization: Bearer ' . $token,
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => $body,
]);
$response = curl_exec($curl);
if ($response === false) {
    $message = curl_error($curl);
    curl_close($curl);
    throw new RuntimeException($message);
}
$status = curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
curl_close($curl);
$result = json_decode($response, false, 512, JSON_THROW_ON_ERROR);
$failed = $status < 200 || $status >= 300;
if ($failed) {
    fwrite(STDERR, "HTTP {$status}\n");
}
fwrite(
    $failed ? STDERR : STDOUT,
    json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR) . PHP_EOL
);
exit($failed ? 1 : 0);

Respuesta 200 application/json

{
  "data": {
    "id": "product_demo_tazon",
    "vendor_id": "vendor_demo_ceramica",
    "catalog_status": "active",
    "status": "published",
    "revision": 4,
    "kind": "variable",
    "name": "Tazón de cerámica",
    "slug": "tazon-de-ceramica",
    "description": "Tazón de cerámica hecho a mano, esmaltado en azul.",
    "price": 15000,
    "original_price": null,
    "discount": 0,
    "category": "hogar",
    "subcategory": "ceramica",
    "image_urls": [
      "https://example.com/tazon-azul.jpg"
    ],
    "badge": null,
    "options": {
      "Color": [
        "Azul"
      ]
    },
    "default_options": {
      "Color": "Azul"
    },
    "variations": [
      {
        "id": "variation_demo_azul",
        "status": "active",
        "options": {
          "Color": "Azul"
        },
        "price": 15000,
        "original_price": null,
        "sku": "TAZ-AZUL"
      }
    ],
    "features": [
      "Hecho a mano"
    ],
    "related_product_ids": [],
    "tags": [
      "ceramica"
    ],
    "sku": null,
    "weight_kg": 0.4,
    "source_region": "metropolitana",
    "created_at": "2026-08-01T12:00:00.000Z",
    "updated_at": "2026-08-26T09:03:00.000Z",
    "published_at": "2026-08-01T12:00:00.000Z",
    "archived_at": null
  }
}

Ante PRODUCT_REVISION_CONFLICT, vuelve a leer, compara y reconstruye el cuerpo editable con la revisión vigente. No descartes cambios ajenos ni sustituyas solo el número de revisión.

Antes de automatizar

Mantén el piloto en solo lectura hasta acordar autoridad, IDs, dirección de sincronización y gestión de conflictos. Prueba reintentos y reconciliación antes de habilitar un dominio de escritura; no se incluyen un plugin WooCommerce ni cambios automáticos en esta guía.

Conectar WooCommerce →