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
      }
    }
  ]
}'
Node.js

Módulo ES para Node.js 18 o posterior (guárdalo, por ejemplo, como ejemplo.mjs). Usa el fetch incluido, sin dependencias. No debe ejecutarse en el navegador.

const token = process.env.MOKU_PAT;
if (!token) throw new Error('MOKU_PAT');
if (process.env.MOKU_ALLOW_WRITES !== '1') throw new Error('MOKU_ALLOW_WRITES=1');

const url = new URL('https://moku.cl/api/v1/vendors/vendor_demo_ceramica/bulk-operations');
const body = {
  "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
      }
    }
  ]
};
const response = await fetch(url, {
  method: 'POST',
  headers: {
    Accept: 'application/json',
    Authorization: `Bearer ${token}`,
    'Idempotency-Key': 'demo_bulk_stock_001',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify(body),
  signal: AbortSignal.timeout(30_000),
});
const result = await response.json();
if (!response.ok) console.error(`HTTP ${response.status}`);
(response.ok ? console.log : console.error)(JSON.stringify(result, null, 2));
process.exitCode = response.ok ? 0 : 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'
Node.js

Módulo ES para Node.js 18 o posterior (guárdalo, por ejemplo, como ejemplo.mjs). Usa el fetch incluido, sin dependencias. No debe ejecutarse en el navegador.

const token = process.env.MOKU_PAT;
if (!token) throw new Error('MOKU_PAT');

const url = new URL('https://moku.cl/api/v1/vendors/vendor_demo_ceramica/bulk-operations/blk_demo_stock_001');
const response = await fetch(url, {
  method: 'GET',
  headers: {
    Accept: 'application/json',
    Authorization: `Bearer ${token}`,
  },
  signal: AbortSignal.timeout(30_000),
});
const result = await response.json();
if (!response.ok) console.error(`HTTP ${response.status}`);
(response.ok ? console.log : console.error)(JSON.stringify(result, null, 2));
process.exitCode = response.ok ? 0 : 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');
}

$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://storage.googleapis.com/moku-348cf-marketplace-media/seller-media/v1/1a2b61ce63dd668ed37dab741499416563ea6f3f9868274f25e4d99a4a5dfe62/83f70f6a565e1bf5a0f14317e850dc382e57ca3ce623d4c42ef2a100d8670144.webp",
          "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.
unknownEl ajuste ya había comenzado cuando se detuvo el procesamiento, así que el stock puede haber cambiado. Vuelve a leer el ítem de inventario antes de emitir otro ajuste; no supongas que falló.
not_attemptedEl procesamiento se detuvo antes de comenzar este ajuste, así que no cambió el stock. Revisa code, vuelve a leer versiones y envíalo en un lote nuevo.

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'
Node.js

Módulo ES para Node.js 18 o posterior (guárdalo, por ejemplo, como ejemplo.mjs). Usa el fetch incluido, sin dependencias. No debe ejecutarse en el navegador.

const token = process.env.MOKU_PAT;
if (!token) throw new Error('MOKU_PAT');

const url = new URL('https://moku.cl/api/v1/vendors/vendor_demo_ceramica/bulk-operations/blk_demo_stock_001');
const response = await fetch(url, {
  method: 'GET',
  headers: {
    Accept: 'application/json',
    Authorization: `Bearer ${token}`,
  },
  signal: AbortSignal.timeout(30_000),
});
const result = await response.json();
if (!response.ok) console.error(`HTTP ${response.status}`);
(response.ok ? console.log : console.error)(JSON.stringify(result, null, 2));
process.exitCode = response.ok ? 0 : 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');
}

$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": "Processing stopped after this adjustment started; read the inventory item before deciding whether to retry it."
      },
      {
        "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"
  }
}

Códigos propios de los resultados

Además de los códigos públicos de error, un registro puede traer dos códigos que solo existen en los resultados de un lote. BACKGROUND_WORK_FAILED indica que la ejecución en segundo plano se detuvo: el lote queda failed, el registro en curso es unknown solo si su ajuste ya había comenzado y los demás quedan not_attempted. Su completed_at es el momento en que se detuvo la ejecución; es null cuando ya venció el registro privado de esa ejecución. BULK_OPERATION_STOPPED marca los registros que no se intentaron porque un registro anterior detuvo el lote.

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