API · Operaciones asíncronas

Un lote, un resultado por registro

Un lote de inventario agrupa ajustes sin convertirlos en una transacción única. Cada registro conserva los controles de autoridad, versión y reservas de una llamada individual.

1. Enviar un lote acotado

Envía entre 1 y 25 ajustes con una key única por registro y el mismo body que usarías en el ajuste individual. El cuerpo completo admite hasta 64 KiB. Una estructura inválida rechaza todo el lote antes de cambiar stock. La respuesta 202 confirma que el trabajo quedó guardado, no que los ajustes se aplicaron.

jobs.write + inventory.write · jobs.read

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.

Aceptar un lote sin prometer su ejecución

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/bulk-operations' \
  --header "Authorization: Bearer ${MOKU_PAT}" \
  --header 'Accept: application/json' \
  --header 'Idempotency-Key: demo_bulk_stock_001' \
  --header 'Content-Type: application/json' \
  --data-raw '{
  "kind": "inventory_adjustment",
  "items": [
    {
      "key": "tazon",
      "inventory_item_id": "WyJwcm9kdWN0X2RlbW9fdGF6b24iLCJ2YXJpYXRpb25fZGVtb19henVsIl0",
      "body": {
        "target_on_hand": 8,
        "expected_version": 7
      }
    },
    {
      "key": "vaso",
      "inventory_item_id": "WyJwcm9kdWN0X2RlbW9fdmFzbyIsbnVsbF0",
      "body": {
        "target_on_hand": 3,
        "expected_version": 1
      }
    }
  ]
}'
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/bulk-operations';
$body = <<<'MOKU_REQUEST_JSON'
{
  "kind": "inventory_adjustment",
  "items": [
    {
      "key": "tazon",
      "inventory_item_id": "WyJwcm9kdWN0X2RlbW9fdGF6b24iLCJ2YXJpYXRpb25fZGVtb19henVsIl0",
      "body": {
        "target_on_hand": 8,
        "expected_version": 7
      }
    },
    {
      "key": "vaso",
      "inventory_item_id": "WyJwcm9kdWN0X2RlbW9fdmFzbyIsbnVsbF0",
      "body": {
        "target_on_hand": 3,
        "expected_version": 1
      }
    }
  ]
}
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_bulk_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 202 application/json

{
  "data": {
    "id": "blk_demo_stock_001",
    "vendor_id": "vendor_demo_ceramica",
    "kind": "inventory_adjustment",
    "status": "queued",
    "counts": {
      "total": 2,
      "succeeded": 0,
      "failed": 0,
      "pending": 2,
      "unknown": 0,
      "not_attempted": 0
    },
    "results": [
      {
        "key": "tazon",
        "inventory_item_id": "WyJwcm9kdWN0X2RlbW9fdGF6b24iLCJ2YXJpYXRpb25fZGVtb19henVsIl0",
        "status": "pending",
        "data": null,
        "code": null,
        "detail": null
      },
      {
        "key": "vaso",
        "inventory_item_id": "WyJwcm9kdWN0X2RlbW9fdmFzbyIsbnVsbF0",
        "status": "pending",
        "data": null,
        "code": null,
        "detail": null
      }
    ],
    "created_at": "2026-08-26T09:00:00.000Z",
    "started_at": null,
    "completed_at": null
  }
}

2. Consultar el ID devuelto

Guarda data.id y consulta su recurso con GET. Los registros se procesan en el orden enviado, pero otros lotes y compradores pueden cambiar inventario entre registros. Un conflicto no revierte registros exitosos. bulk_operation.completed avisa que debes consultar el estado; los eventos pueden duplicarse, retrasarse o perderse.

Consultar resultados parciales del lote

Solicitud · ejemplo ilustrativo

cURL
: "${MOKU_PAT:?}" &&
curl --silent --show-error --fail-with-body \
  'https://moku.cl/api/v1/vendors/vendor_demo_ceramica/bulk-operations/blk_demo_stock_001' \
  --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/bulk-operations/blk_demo_stock_001';
$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": "blk_demo_stock_001",
    "vendor_id": "vendor_demo_ceramica",
    "kind": "inventory_adjustment",
    "status": "completed_with_errors",
    "counts": {
      "total": 2,
      "succeeded": 1,
      "failed": 1,
      "pending": 0,
      "unknown": 0,
      "not_attempted": 0
    },
    "results": [
      {
        "key": "tazon",
        "inventory_item_id": "WyJwcm9kdWN0X2RlbW9fdGF6b24iLCJ2YXJpYXRpb25fZGVtb19henVsIl0",
        "status": "succeeded",
        "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"
        },
        "code": null,
        "detail": null
      },
      {
        "key": "vaso",
        "inventory_item_id": "WyJwcm9kdWN0X2RlbW9fdmFzbyIsbnVsbF0",
        "status": "failed",
        "data": null,
        "code": "INVENTORY_VERSION_CONFLICT",
        "detail": "Inventory changed; retrieve the item before issuing another adjustment."
      }
    ],
    "created_at": "2026-08-26T09:00:00.000Z",
    "started_at": "2026-08-26T09:00:00.000Z",
    "completed_at": "2026-08-26T09:02:00.000Z"
  }
}

3. Interpretar cada resultado

Estado del registroQué significa y qué hacer
pendingAún no existe un resultado durable. Sigue consultando mientras el lote permanezca queued o running.
succeededEl ajuste se confirmó. data es el resultado histórico de esa operación, no una lectura actual de stock.
failedEl registro fue rechazado. Revisa code y detail; ante conflicto de versión, vuelve a leer inventario y decide un nuevo ajuste.
unknownNo se pudo confirmar el resultado y el stock puede haber cambiado. Reconcilia el inventario actual antes de emitir otro ajuste; no supongas que falló.
not_attemptedEl lote se detuvo antes de intentar este registro. Corrige la causa y vuelve a leer versiones antes de crear otro lote.

Reintentos sin duplicar efectos

Si pierdes la respuesta del POST, repite el mismo cuerpo con la misma Idempotency-Key y credencial. Recibirás el acuse original, incluso si el trabajo ya terminó; consulta después el ID. No reutilices una clave para un lote diferente. El trabajador conserva una identidad idempotente por registro y no repite efectos confirmados.

Antes de cada ajuste se verifica de nuevo la credencial original, su vencimiento, permisos y dueño actual. Rotar un PAT no transfiere el lote a otro token. Si revocas una credencial después de una respuesta perdida, el registro puede quedar unknown: la revocación no deshace un ajuste previo.

Conservar un resultado incierto tras una revocación

Solicitud · ejemplo ilustrativo

cURL
: "${MOKU_PAT:?}" &&
curl --silent --show-error --fail-with-body \
  'https://moku.cl/api/v1/vendors/vendor_demo_ceramica/bulk-operations/blk_demo_stock_001' \
  --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/bulk-operations/blk_demo_stock_001';
$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": "blk_demo_stock_001",
    "vendor_id": "vendor_demo_ceramica",
    "kind": "inventory_adjustment",
    "status": "failed",
    "counts": {
      "total": 2,
      "succeeded": 0,
      "failed": 0,
      "pending": 0,
      "unknown": 1,
      "not_attempted": 1
    },
    "results": [
      {
        "key": "tazon",
        "inventory_item_id": "WyJwcm9kdWN0X2RlbW9fdGF6b24iLCJ2YXJpYXRpb25fZGVtb19henVsIl0",
        "status": "unknown",
        "data": null,
        "code": "UNAUTHENTICATED",
        "detail": "No durable result was recorded. Reconcile this record before issuing a new operation."
      },
      {
        "key": "vaso",
        "inventory_item_id": "WyJwcm9kdWN0X2RlbW9fdmFzbyIsbnVsbF0",
        "status": "not_attempted",
        "data": null,
        "code": "BULK_OPERATION_STOPPED",
        "detail": "The operation stopped before this record was attempted."
      }
    ],
    "created_at": "2026-08-26T09:00:00.000Z",
    "started_at": "2026-08-26T09:00:00.000Z",
    "completed_at": "2026-08-26T09:02:00.000Z"
  }
}

Límites y retención

El lote no evita la cuota: cada ajuste usa el cupo de 12 escrituras por minuto de la credencial original. Cada registro tiene intentos finitos y usa el trabajador existente, sin un servicio siempre encendido. Conservamos resultados durante al menos 30 días después de la finalización. Una ejecución agotada se muestra como failed con resultados no resueltos; no permanece presentada como una cola saludable.

Ver contrato de operaciones por lotes · Reconciliación y conflictos