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 registro | Qué significa y qué hacer |
|---|---|
pending | Aún no existe un resultado durable. Sigue consultando mientras el lote permanezca queued o running. |
succeeded | El ajuste se confirmó. data es el resultado histórico de esa operación, no una lectura actual de stock. |
failed | El registro fue rechazado. Revisa code y detail; ante conflicto de versión, vuelve a leer inventario y decide un nuevo ajuste. |
unknown | El 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_attempted | El 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