Arquitectura de integración

Una fuente, varios destinos

La Autoridad mutable existe solo para Catálogo, precio base y cada Stock Pool. El origen y el fulfillment quedan guardados en cada pedido; no cambian cuando se reconfigura una conexión.

Roles de conexión

Preset WooCatálogo y precio baseInventarioPedidos
moku_sourceDestinoDestinoIngreso habilitado
woocommerce_catalog_sourceFuenteDestinoIngreso habilitado
woocommerce_sourceFuenteFuenteIngreso habilitado

Un rol source reclama Autoridad; destination nunca la reclama. Por eso WooCommerce puede recibir stock cuando Moku u otra conexión sea la fuente.

Limitación actual

Si una conexión externa controla Inventario, el checkout nativo de Moku sigue bloqueado hasta que exista un Adapter de reserva y débito remoto. Los roles no fingen que ese handshake ya está implementado.

Crear una conexión

La API usa producción, aunque los pagos estén en sandbox. Los IDs y las respuestas de esta guía son ficticios; reemplázalos por los de tu tienda.

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"
  }
}

Mapear publicaciones

Un channel_listing relaciona un listing externo con un inventory_item_id estable. Guarda los IDs de ambos sistemas; no uses SKU como identidad.

Seguir la guía de producto a stock

Cambiar roles o desconectar

Reemplazar roles exige que no queden listings activos, reservas, pedidos externos sin resolver ni fulfillment del canal. Pausa, reconcilia, deja los contadores en cero y envía expected_version a PUT /connections/{id}/roles. Consulta GET /authority después del cambio.