Beta inestable. La API está en desarrollo activo — pueden ocurrir errores o cambiar campos. Si encuentra algo raro, avísenos.
Reportar por WhatsApp

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.

Los 3 límites que importan

  • 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/processing devuelve 429 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 test no consumen.
Empiece en prueba. Ejecute el catálogo entero con una key automeli_test_*, revise con productos de prueba y recién después promueva a real. Ver flujo test → live.

Idempotencia: reintente sin miedo

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.

Script completo (batching + olas de 6 + poll)

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.

javascript
// 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'));
python
# 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")

Reconciliar: paginar los resultados

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.

javascript
// 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.