Guía
Publicar un catálogo de 1000 (o más)
El patrón completo para integraciones ERP: dividir el catálogo en jobs, respetar los límites, reintentar sin duplicar y reconciliar resultados. Copie el script, póngale su lista de SKUs y ejecútelo.
- 1.10.000 items por job. Un catálogo más grande se parte en varios jobs. Recomendamos lotes de 1.000–2.000: terminan antes y dan feedback más granular si algo falla.
- 2.6 jobs activos a la vez. Crear un 7º job mientras hay 6 en
queued/processingdevuelve429 E_PRODUCT_MAX_CONCURRENT_JOBS. Procese en olas de 6. - 3.Créditos. Cada item publicado consume 1 crédito. Verifique GET /v1/account antes de un batch grande. Las keys
testno consumen.
automeli_test_*, revise con productos de prueba y recién después promueva a real. Ver flujo test → live.En un batch largo, los timeouts de red son inevitables. Envíe un Idempotency-Key estable por lote (derivado del contenido del lote, no aleatorio) para que reintentar el mismo POST no cree un job duplicado.
Misma key + mismo body devuelve la respuesta original cacheada (24h). Detalle en la guía de idempotencia.
Maneja los 3 límites: parte en lotes, procesa en olas de 6 jobs, reintenta los 429 con espera, y hace poll de cada job hasta que termina.
// Publica un catálogo grande respetando los límites de la API.
const BASE = 'https://api.automeli.com/api/v1';
const KEY = process.env.AUTOMELI_KEY; // automeli_live_* o automeli_test_*
const BATCH_SIZE = 1000; // ≤ 10.000; lotes chicos = feedback más rápido
const MAX_CONCURRENT = 6; // tope de jobs activos a la vez
const headers = (extra = {}) => ({ 'X-API-Key': KEY, 'Content-Type': 'application/json', ...extra });
const sleep = ms => new Promise(r => setTimeout(r, ms));
const chunk = (arr, n) => Array.from({ length: Math.ceil(arr.length / n) }, (_, i) => arr.slice(i * n, i * n + n));
// 1 UUID estable por lote → reintentar el mismo lote no duplica el job.
function batchKey(skus) { return 'cat-' + skus[0] + '-' + skus.length; }
async function createJob(skus) {
const body = { items: skus.map(sku => ({ sku, auto_categorize: true })) };
while (true) {
const res = await fetch(BASE + '/products', {
method: 'POST',
headers: headers({ 'Idempotency-Key': batchKey(skus) }),
body: JSON.stringify(body),
});
if (res.status === 429) { await sleep(5000); continue; } // límite o 6 jobs activos → esperar
const data = await res.json();
if (!res.ok) throw new Error(data.code + ': ' + (data.detail || data.title));
return data.job_id;
}
}
async function waitForJob(jobId) {
while (true) {
const res = await fetch(BASE + '/products/jobs/' + jobId, { headers: headers() });
const data = await res.json();
const status = data.job.status;
if (['completed', 'failed', 'cancelled'].includes(status)) return data.job;
await sleep(7000); // poll cada 5-10s
}
}
async function run(allSkus) {
const batches = chunk(allSkus, BATCH_SIZE);
const results = [];
for (let i = 0; i < batches.length; i += MAX_CONCURRENT) {
const wave = batches.slice(i, i + MAX_CONCURRENT);
const jobIds = await Promise.all(wave.map(createJob));
const jobs = await Promise.all(jobIds.map(waitForJob));
jobs.forEach(j => results.push(j));
console.log(`Ola ${i / MAX_CONCURRENT + 1}: ${jobs.length} jobs terminados`);
}
const ok = results.reduce((s, j) => s + j.successful, 0);
const fail = results.reduce((s, j) => s + j.failed, 0);
console.log(`Total: ${ok} publicados, ${fail} fallidos`);
}
run(require('./skus.json'));# Publica un catálogo grande respetando los límites de la API.
import os, time, requests
BASE = "https://api.automeli.com/api/v1"
KEY = os.environ["AUTOMELI_KEY"] # automeli_live_* o automeli_test_*
BATCH_SIZE = 1000 # ≤ 10.000
MAX_CONCURRENT = 6 # tope de jobs activos
def headers(extra=None):
h = {"X-API-Key": KEY, "Content-Type": "application/json"}
if extra: h.update(extra)
return h
def chunks(xs, n):
for i in range(0, len(xs), n):
yield xs[i:i + n]
def batch_key(skus): # idempotencia: mismo lote → no duplica
return f"cat-{skus[0]}-{len(skus)}"
def create_job(skus):
body = {"items": [{"sku": s, "auto_categorize": True} for s in skus]}
while True:
r = requests.post(BASE + "/products",
headers=headers({"Idempotency-Key": batch_key(skus)}),
json=body)
if r.status_code == 429: # límite o 6 jobs activos
time.sleep(5); continue
data = r.json()
if not r.ok:
raise RuntimeError(data["code"] + ": " + data.get("detail", data.get("title", "")))
return data["job_id"]
def wait_for_job(job_id):
while True:
data = requests.get(BASE + f"/products/jobs/{job_id}", headers=headers()).json()
if data["job"]["status"] in ("completed", "failed", "cancelled"):
return data["job"]
time.sleep(7) # poll cada 5-10s
def run(all_skus):
batches = list(chunks(all_skus, BATCH_SIZE))
ok = fail = 0
for i in range(0, len(batches), MAX_CONCURRENT):
wave = batches[i:i + MAX_CONCURRENT]
job_ids = [create_job(b) for b in wave] # se procesan en paralelo del lado servidor
for jid in job_ids:
job = wait_for_job(jid)
ok += job["successful"]; fail += job["failed"]
print(f"Ola {i // MAX_CONCURRENT + 1}: {len(wave)} jobs terminados")
print(f"Total: {ok} publicados, {fail} fallidos")El GET del job devuelve sus items paginados (limit/offset). Para reconciliar contra su sistema, recorra todas las páginas hasta que una venga incompleta.
// Traer TODOS los items de un job grande (paginado).
async function allItems(jobId) {
const items = [];
let offset = 0;
const limit = 100;
while (true) {
const res = await fetch(`${BASE}/products/jobs/${jobId}?limit=${limit}&offset=${offset}`, { headers: headers() });
const data = await res.json();
items.push(...data.items);
if (data.items.length < limit) break; // última página
offset += limit;
}
return items;
}Cada item trae sku, status, listing_id y, si falló, retryable. Reintente los fallidos con POST /v1/products/jobs/{jobId}/retry.