Está viendo la documentación de la API v1 (versión anterior). v1 sigue funcionando sin cambios hasta el 31 de enero de 2027, fecha en la que se apagará. Las integraciones nuevas deben usar la API v2, que cubre todo lo de v1 con nombres limpios y paginación por cursor. Si ya integró con v1, la guía de migración lista las equivalencias campo por campo.

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.