← Documentación del Nuevo PIM ← Catalogación por API: la propuesta completa

Catalogación por API: las APIs y el flujo, con JSON de ejemplo

Para darse idea de cómo se ve cada llamada · propuesta del 16-oct-2026 · alineación con Mercado Libre, 17-oct-2026 por la tarde

Propuesta sin ratificar · ejemplos ilustrativos

Los ejemplos salen de la propuesta funcional y de la propuesta de arquitectura, que todavía no se ratifican. El JSON del producto (atributo, variante, búsqueda y categoría) es la alineación con la guía de producto de Mercado Libre, del 17-oct por la tarde: todavía no está en decisiones.md. Los datos son inventados. El contrato final sale de la spec de FT-PRD-10 y FT-USR-03. Las decisiones, los componentes, los estados y lo que falta decidir están en la propuesta completa.

1. En una mirada: cuántas APIs se ocupan

No. Para catalogar un producto no se ocupan las 45 que contaba el inventario de la mañana. Se ocupan 8 llamadas, y dos de ellas se hacen una sola vez (quién soy, y dónde llegan los avisos). Las otras 33 sí se ocupan, para lo que viene después del alta y que un ítem de marketplace no resuelve: lotes, correcciones, variantes que se suman después, fotos sin URL pública, historial, entrega por canal, modificaciones con permiso y la operación de los avisos. 4 dejan de ser ruta porque el dato ya viene en otra respuesta. Quedan 41.

8llamadas para catalogar un producto
33rutas del ciclo, después del alta
4de las 45 se funden en otra respuesta
41rutas que quedan bajo /partner/v1
Para catalogarCuándoRuta
1Una vez por sesiónGET /me
2Una vezPOST /webhooks
3Por productoGET /categories/discovery?q=
4Por categoríaGET /categories/{categoryId}
5Opcional, antes de crearPOST /validations, con el mismo JSON del alta
6Por productoGET /products/search
7Por producto, o un lotePOST /proposals o POST /batches
8Cuando quiera el estadoGET /proposals/{proposalId}

Si la búsqueda dice que el producto ya existe, no hay novena llamada: la respuesta trae el id de catálogo, el SKU y si Mirakl ya acusó. La oferta se crea en Mirakl. Si aun así se manda el alta, lleva catalogProductId y no reenvía la ficha.

Deja de ser rutaDónde queda el dato
GET /value-lists/{listCode}/valuesvalues del atributo, en GET /categories/{categoryId}
GET /brandsel atributo BRAND, con sus valores
GET /offer-targets/{sku}sku, allowedAction y offer.miraklStatus, en la búsqueda
GET /products/{id}/mediapictures, en GET /products/{id}

Cómo conté. El 17-oct por la mañana había 45: cada método con su ruta, una vez, como en las tablas del §5 de la propuesta funcional (POST /batches sale dos veces y cuenta una). No entran el token, que lo da el proveedor de identidad, ni los 17 avisos, que son eventos y no rutas. Tampoco las tres rutas para rotar credenciales, que la arquitectura saca de la versión 1. Por la tarde se restan las 4 que se funden: 41. De esas, 8 son el camino de arriba y 33 el ciclo. Cinco rutas no se borran, cambian de nombre: POST /suggestions/classification pasa a GET /categories/discovery, GET /templates/{templateId} a GET /categories/{categoryId}, su /schema y GET /templates/changes igual, y POST /products/lookup a GET /products/search. El id de categoría es el de la plantilla: por dentro sigue siendo la vista efectiva del negocio.

GrupoEndpointsPara qué
§5.1 Autenticación · 1GET /me (el token, POST /oauth/token, lo da el proveedor de identidad)Saber quién es el consumidor, qué puede hacer y sus límites.
§5.2 Catálogos de referencia · 5GET /categories · GET /categories/{categoryId} · GET /categories/{categoryId}/schema · POST /brand-requests · GET /categories/changesRecorrer el árbol, bajar atributos y valores de una categoría, pedir una marca que no está y ver qué cambió. El esquema JSON se queda: Mercado Libre no lo da y el integrador valida en su casa.
§5.3 Sugerencias con IA · 1GET /categories/discovery?q=Predecir la categoría a partir del título. La IA sigue detrás; la puerta es un GET, como el predictor de Mercado Libre.
§5.4 Validación previa · 1POST /validationsProbar hasta 100 productos sin crear nada.
§5.5 Propuestas y lotes · 10POST /proposals · POST /batches · GET /batches/{batchId} · GET /batches/{batchId}/items · GET /proposals/{proposalId} · GET /proposals · PATCH /proposals/{proposalId} · POST /proposals/{proposalId}/variants · DELETE /proposals/{proposalId}/variants/{variantId} · POST /proposals/{proposalId}/cancelMandar uno o un lote, seguirlo, corregirlo, sumar o quitar variantes y cancelar.
§5.6 Identificación · 1GET /products/searchSaber si ya existe, por GTIN o por texto, y si toca crearlo o ofertar. Trae el SKU y el acuse de Mirakl.
§5.8 Imágenes · 4POST /media/uploads · PUT /proposals/{id}/media · PUT /products/{id}/media · POST /media/batchesSubir un archivo cuando no hay URL pública. Si la hay, va en pictures.source del alta y estas rutas no se llaman.
§5.9 Estado e historial · 6GET /proposals/{id}/history · GET /products/{id}/history · GET /products/{id} · GET /products/{id}/channels · GET /events · GET /receipts/{requestId}Consultar la ficha, la historia, la entrega por canal, los avisos y los recibos.
§5.10 Modificaciones · 4GET /products/{id}/permissions · POST /products/{id}/modifications · POST /modifications/batches · GET /modifications/{id}Cambiar un producto publicado y seguir el cambio.
§5.11 Webhooks · 8POST /webhooks · GET /webhooks · PATCH /webhooks/{id} · DELETE /webhooks/{id} · POST /webhooks/{id}/test · POST /webhooks/{id}/secret · GET /webhooks/{id}/deliveries · POST /webhooks/{id}/deliveries/{deliveryId}/retrySuscribirse a los avisos, probarlos, rotar el secreto y reintentar entregas.
§5.12 Sandbox · 0Sin rutas propiasProbar de punta a punta con las mismas rutas y credenciales de prueba.

Los 17 avisos: proposal.received, proposal.validated, proposal.correction_required, proposal.rejected, proposal.already_exists, proposal.accepted, proposal.cancelled, product.sku_created, product.in_preparation, product.publication_sent, product.published, product.publication_failed, product.updated, product.modification_rejected, product.retired, batch.completed y template.changed. El aviso de que cambió la categoría sigue llamándose template.changed hasta cerrar el catálogo de eventos.

2. Solo para catalogar

Catalogar es dar de alta la ficha de un producto que no existe. Hay dos flujos. Empiezan igual y se separan al mandar: uno, o un lote. Si la búsqueda encuentra el producto, aquí se acaba: no se cataloga.

API de tercerosSistema delproveedor o sellerProveedorde identidadAPI de tercerosSistema delproveedor o sellerProveedorde identidadUna sola vezIgual en los dos flujos, por productoopt[Si quiere probar antes de crear]alt[Flujo 1 · se cataloga uno][Flujo 2 · se cataloga un lote]POST /oauth/token1token de 15 minutos2GET /me3quién es y sus límites4POST /webhooks5201 y el secreto6GET /categories/discovery?q=7de una a tres categorías8GET /categories/{categoryId}9atributos y valores10POST /validationsel mismo JSON del alta11200, errores por campo,sin crear nada12GET /products/search13results vacío:el producto no existe14POST /proposalsla ficha completa15201 · proposalId,en aprobación16POST /batcheshasta 1,000, el mismo JSON17202 · batchId18
Solo el alta. El visto bueno, SAP, los canales y las modificaciones están en el flujo completo, más abajo.

Antes, una sola vez

LlamadaPara qué
1POST /oauth/tokenPedir el token. Lo da el proveedor de identidad.
2GET /meSaber quién es y sus límites.
3POST /webhooksDecir dónde llegan los avisos.

Igual en los dos flujos

LlamadaPara qué
4GET /categories/discovery?q=Predecir la categoría.
5GET /categories/{categoryId}Bajar atributos, valores y versión.
6POST /validationsOpcional. El mismo JSON del alta, sin crear nada.
7GET /products/searchConfirmar que no existe. results viene vacío.

Flujo 1. Se cataloga uno

POST /proposals con la ficha completa: atributos, attribute_combinations y, si hay URL, pictures.source. Responde 201 con proposalId y estado IN_APPROVAL. Para ver cómo va, GET /proposals/{proposalId}.

Flujo 2. Se cataloga un lote

Las mismas llamadas, y en el paso 7 se manda POST /batches con hasta 1,000 productos. Cada renglón es el JSON del flujo 1. Responde 202 con batchId. Saber si terminó ya es el ciclo: el aviso batch.completed y GET /batches/{batchId}/items.

Estos no catalogan

La búsqueda responde allowedAction. Solo CREATE_PROPOSAL entra a los dos flujos de arriba. Las otras cinco acciones están en la propuesta funcional, §5.6, y no crean ficha:

allowedActionQué sigue
CREATE_PROPOSALFlujo 1 o flujo 2. Es el único que cataloga.
CREATE_OFFERLa oferta se crea en Mirakl. La ficha ya existe.
MULTI_OFFERLa oferta se crea en Mirakl. La ficha ya existe.
STOCK_OUT_OFFERLa oferta se crea en Mirakl, sobre un producto sin stock.
REQUEST_EAN_RELEASEPedir que liberen el EAN. No se manda la ficha.
NOT_ALLOWEDNo se crea nada. En Mercaderías, un producto que ya existe no se duplica.

Si aun así se manda el alta de uno que ya existe, el cuerpo solo lleva catalogProductId y no la ficha. El ejemplo está en mandar uno.

3. El flujo completo

Canalesde ventaSAPRevisiónCompras o IANúcleo del PIMAPI de tercerosSistema delproveedor o sellerProveedorde identidadCanalesde ventaSAPRevisiónCompras o IANúcleo del PIMAPI de tercerosSistema delproveedor o sellerProveedorde identidad1 · Prepararse, una sola vez2 · Encontrar la categoría y probaropt[Ya existe en Marketplace: no se reenvía la ficha]3 · Enviar uno o un lotealt[Uno por uno, en línea][Un lote, en segundo plano]4 · Seguir: todo llega por avisosalt[Mercaderías][Marketplace]opt[Si no alcanza o Compras rechaza]opt[Si no trae URL pública y le tocaponer la foto]opt[Si se le perdió algún aviso]5 · Cambiar lo publicadoPOST /oauth/tokenpide un token1token de 15 minutos2GET /mequién soy y qué puedo hacer3negocios, tiendas, alcances y límites4POST /webhooksdónde quiere recibir los avisos5201 · webhookId y secreto de la firma6GET /categories/discovery?q=qué categoría le toca7de una a tres categoríascon su confianza8GET /categories/{categoryId}baja atributos y valores9atributos, obligatorios,valores y versión10POST /validationsel mismo JSON del alta11las reglas del portal,sin escribir12200 · por producto,errores y qué pasaría13GET /products/search¿ya existe?14si existe, id de catálogo,SKU y si Mirakl acusó15crea la oferta en Mirakl, fuera del PIM16POST /proposalsmanda un producto17revisa y, si pasa,crea la propuesta18201 · proposalId, en aprobacióno 409 / 422 sin crear nada19POST /batchesmanda hasta 1,000 productos20202 · batchId, acuse inmediato21POST a su webhookproposal.received22una propuestapor producto23POST a su webhookbatch.completed24GET /batches/{batchId}/itemsresultado por producto25creada, requiere corrección,rechazada o ya existe26pide el visto bueno27Compras acepta,rechaza o cancela28la IA categorizay califica29alcanza o noel umbral30POST a su webhookproposal.correction_required31PATCH /proposals/{proposalId}corrige sólo lo que se pidió32POST a su webhookproposal.accepted33pide el SKU34SKU y EAN por variante35POST a su webhookproduct.sku_created36GET /proposals/{proposalId}consulta el estado cuando quiera37estado, SKU por variantey acciones pendientes38POST /media/uploadspide dónde subir cada imagen39mediaId y URL de subidapor archivo40PUT /proposals/{id}/mediafija sus imágenes41publica en cada canal42POST a su webhookproduct.publication_sent43acuse de cada canal44POST a su webhookproduct.published, por canal45GET /events?cursor=los avisos, desde donde se quedó46los avisos en orden47GET /products/{id}/permissionsqué puede cambiar48editable, requiere aprobacióno sólo lectura49POST /products/{id}/modificationsmanda el cambio50202 · modificationId,en modificación51POST a su webhookproduct.updated52
Las flechas llenas son llamadas y las punteadas, respuestas; las de punta abierta son avisos (webhooks) que el PIM manda al sistema del tercero. Las rutas de la API de terceros van bajo /partner/v1; el token lo da el proveedor de identidad. Desde la fase 4 todo pasa en segundo plano. Mercaderías y Marketplace sólo se separan en el visto bueno: Compras o la IA.
PasoAPIPara quéFlecha
1POST /oauth/token, en el proveedor de identidadPedir un token de 15 minutos1
2GET /meSaber quién es, qué puede hacer y sus límites3
3POST /webhooksDecir dónde quiere los avisos; se hace una vez5
4GET /categories/discovery?q=Predecir la categoría7
5GET /categories/{categoryId}Bajar atributos, valores y versión de esa categoría9
6POST /validationsProbar con el mismo JSON del alta, sin crear nada11
7GET /products/searchSaber si ya existe; si existe, trae el SKU y el acuse de Mirakl14
8No es una llamada al PIMSi ya existe en Marketplace, la oferta se crea en Mirakl. El alta, si se manda, lleva catalogProductId y no reenvía la ficha16
9POST /proposalsMandar un producto; responde en el momento17
10POST /batchesO mandar un lote de hasta 1,000; acusa en el momento20
11Aviso proposal.receivedEl PIM recibió el producto del lote22
12Aviso batch.completedEl lote terminó, con su resumen24
13GET /batches/{batchId}/itemsVer el resultado de cada producto del lote25
14Aviso proposal.correction_requiredHay algo que corregir, campo por campo31
15PATCH /proposals/{proposalId}Corregir sólo lo que se pidió32
16Aviso proposal.acceptedRecibió el visto bueno; se pide el SKU a SAP33
17Aviso product.sku_createdYa tiene SKU y EAN por variante36
18GET /proposals/{proposalId}Consultar el estado cuando quiera37
19POST /media/uploadsPedir dónde subir cada imagen, si no trae URL39
20PUT /proposals/{id}/mediaFijar las imágenes de cada variante, si no iban en el alta41
21Aviso product.publication_sentSe envió a un canal43
22Aviso product.publishedEl canal confirmó45
23GET /eventsRecuperar avisos perdidos, en orden46
24GET /products/{id}/permissionsSaber qué puede cambiar48
25POST /products/{id}/modificationsMandar un cambio50
26Aviso product.updatedEl cambio quedó aplicado, con su origen52

4. Cada API

Paso 1POST/oauth/tokenen el proveedor de identidad

Pedir un token de 15 minutos con las credenciales del sistema. Lo da el proveedor de identidad, no la API de terceros.

Request
POST /oauth/token
Host: {host-del-proveedor-de-identidad}
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials&client_id=CLI-DEMO-01&client_secret=<client_secret>&scope=catalog.read%20suggestions.use%20proposals.write%20media.write%20products.modify%20webhooks.manage
Response 200
{
  "access_token": "eyJ...ejemplo",
  "token_type": "Bearer",
  "expires_in": 900,
  "scope": "catalog.read suggestions.use proposals.write media.write products.modify webhooks.manage"
}

Según la arquitectura: la ruta la fija el proveedor de identidad que salga de PD-26, y el token sólo identifica al cliente y sus alcances; negocios, tiendas y estado se leen en cada llamada del registro de terceros (la propuesta funcional los metía en el token).

Paso 2GET/partner/v1/me

Saber quién es, qué negocios, tiendas y alcances tiene, en qué ambiente está, cuáles son sus límites y cuánto lleva consumido.

Request
GET /partner/v1/me
Authorization: Bearer eyJ...ejemplo
X-Request-Id: REQ-DEMO-0001
Response 200
{
  "success": true,
  "data": {
    "partnerId": "SELLER-DEMO-01",
    "type": "SELLER",
    "status": "ACTIVE",
    "supplierId": null,
    "shopIds": ["SHOP-DEMO-01"],
    "businesses": ["MKP"],
    "scopes": [
      "catalog.read",
      "suggestions.use",
      "proposals.write",
      "media.write",
      "products.modify",
      "webhooks.manage"
    ],
    "env": "SANDBOX",
    "limits": {
      "readsPerMinute": 600,
      "writesPerMinute": 60,
      "suggestionsPerMinute": 30,
      "maxItemsPerBatch": 1000,
      "maxBatchesInProcess": 2,
      "newProductsPerDay": 100
    },
    "usage": {
      "newProductsToday": 0,
      "batchesInProcess": 0
    }
  }
}

⚠️ Ilustrativo: usage. Los límites son los de arranque del sandbox y el piloto; el tope diario se fija a mano al dar el permiso y el 100 es de ejemplo. Según la arquitectura, un límite sin configurar vale null, nunca cero ni ilimitado, y con él no se puede escribir.

Paso 3POST/partner/v1/webhooks

Decir a qué dirección HTTPS quiere recibir los avisos y cuáles. La respuesta trae, una sola vez, el secreto para verificar la firma de cada aviso.

Request
POST /partner/v1/webhooks
Authorization: Bearer eyJ...ejemplo
Idempotency-Key: 00000000-0000-4000-8000-000000000001
Content-Type: application/json
{
  "url": "https://{host-del-seller}/avisos-pim",
  "events": [
    "proposal.received",
    "proposal.correction_required",
    "proposal.accepted",
    "product.sku_created",
    "product.publication_sent",
    "product.published",
    "product.updated",
    "batch.completed"
  ],
  "description": "Avisos para el orquestador de pruebas"
}
Response 201
{
  "success": true,
  "data": {
    "webhookId": "WHK-DEMO-01",
    "secret": "<webhook_secret>"
  }
}

Paso 4GET/partner/v1/categories/discovery?q=

Predecir de una a tres categorías a partir del título, como el predictor de Mercado Libre (domain_discovery). La IA sigue detrás de esta puerta. categoryId es el id de la plantilla.

Request
GET /partner/v1/categories/discovery?business=MKP&q=bateria%20portatil%2020000%20mAh
Authorization: Bearer eyJ...ejemplo
Response 200
{
  "success": true,
  "data": {
    "suggestionId": "SUG-DEMO-0001",
    "results": [
      {
        "categoryId": "CAT-DEMO-0412",
        "categoryName": "Baterías externas",
        "domainId": "POWER_BANKS",
        "confidence": 0.91,
        "attributes": [
          { "id": "BRAND", "value_id": "MARCADEMO", "value_name": "Marca Demo" },
          { "id": "MODEL", "value_name": "PB-DEMO-20" }
        ]
      },
      {
        "categoryId": "CAT-DEMO-0415",
        "categoryName": "Cargadores de pared",
        "domainId": "WALL_CHARGERS",
        "confidence": 0.47,
        "attributes": []
      }
    ],
    "itemGroup": "IG-DEMO-0101",
    "webCategory": "Electrónica / Accesorios para celular",
    "expiresAt": "2026-11-02T17:00:00Z"
  }
}

El atributo va como en Mercado Libre: id, value_id y value_name. confidence es de Liverpool; Mercado Libre ordena por probabilidad y no manda un número. ⚠️ Ilustrativo: itemGroup y webCategory. La categoría web es informativa: la asigna Liverpool y el tercero nunca la manda.

Paso 5GET/partner/v1/categories/{categoryId}?business=MKP

Bajar la categoría, que es la plantilla del negocio: atributos, tipo, valores cerrados, cuáles son obligatorios y en qué versión. Los valores vienen aquí; no hay una llamada aparte por lista ni por marca.

Request
GET /partner/v1/categories/CAT-DEMO-0412?business=MKP
Authorization: Bearer eyJ...ejemplo
Response 200
{
  "success": true,
  "data": {
    "categoryId": "CAT-DEMO-0412",
    "name": "Baterías externas",
    "domainId": "POWER_BANKS",
    "version": 7,
    "effectiveFrom": "2026-10-01T00:00:00Z",
    "attributes": [
      {
        "id": "BRAND",
        "name": "Marca",
        "value_type": "list",
        "tags": { "required": true },
        "values": [ { "id": "MARCADEMO", "name": "Marca Demo" } ],
        "attribute_group_id": "BASICOS",
        "attribute_group_name": "Datos básicos",
        "filledBy": "SUPPLIER"
      },
      {
        "id": "CAPACIDAD",
        "name": "Capacidad",
        "value_type": "number_unit",
        "allowed_units": [ { "id": "MAH", "name": "mAh" } ],
        "tags": { "required": true },
        "attribute_group_id": "BASICOS",
        "attribute_group_name": "Datos básicos",
        "filledBy": "SUPPLIER"
      },
      {
        "id": "TIPO_CONECTOR",
        "name": "Tipo de conector",
        "value_type": "list",
        "tags": { "required": true, "multivalued": true },
        "values": [
          { "id": "USB_C", "name": "USB-C" },
          { "id": "USB_A", "name": "USB-A" }
        ],
        "attribute_group_id": "BASICOS",
        "attribute_group_name": "Datos básicos",
        "filledBy": "SUPPLIER"
      },
      {
        "id": "MATERIAL",
        "name": "Material",
        "value_type": "list",
        "tags": { "recommended": true },
        "values": [ { "id": "ALUMINIO", "name": "Aluminio" } ],
        "attribute_group_id": "BASICOS",
        "attribute_group_name": "Datos básicos",
        "filledBy": "SUPPLIER"
      },
      {
        "id": "COLOR",
        "name": "Color",
        "value_type": "list",
        "tags": { "required": true, "allow_variations": true },
        "values": [
          { "id": "NEGRO", "name": "Negro" },
          { "id": "BLANCO", "name": "Blanco" }
        ],
        "attribute_group_id": "BASICOS",
        "attribute_group_name": "Datos básicos",
        "filledBy": "SUPPLIER",
        "acceptedOnlyAtCreation": true
      },
      {
        "id": "GTIN",
        "name": "GTIN",
        "value_type": "string",
        "tags": { "variation_attribute": true, "conditional_required": true },
        "attribute_group_id": "BASICOS",
        "attribute_group_name": "Datos básicos",
        "filledBy": "SUPPLIER"
      }
    ],
    "imageRules": { "mainImageRequired": true, "maxPicturesPerVariation": 20 },
    "calculatedFields": ["TITULO_WEB"]
  }
}

id, value_type, tags (required, allow_variations, variation_attribute) y values siguen a Mercado Libre. filledBy, acceptedOnlyAtCreation, version y recommended son de Liverpool. ⚠️ Ilustrativo: imageRules y el código de calculatedFields. La plantilla trae cinco secciones; aquí los atributos van en una.

Paso 6POST/partner/v1/validations

Probar hasta 100 productos con las mismas reglas del portal, sin crear nada. Cada elemento de items es el cuerpo de POST /proposals. Validar uno es ese cuerpo, sin envoltura. Por producto dice qué está mal, campo por campo, y qué pasaría al mandarlo.

Request
POST /partner/v1/validations
Authorization: Bearer eyJ...ejemplo
Content-Type: application/json
{
  "business": "MKP",
  "items": [
    {
      "externalRef": "DEMO-PB20",
      "categoryId": "CAT-DEMO-0412",
      "categoryVersion": 7,
      "title": "PB-DEMO-20 Marca Demo",
      "attributes": [
        { "id": "BRAND", "value_id": "MARCADEMO", "value_name": "Marca Demo" },
        { "id": "MODEL", "value_name": "PB-DEMO-20" },
        { "id": "CAPACIDAD", "value_name": "20000 mAh", "value_struct": { "number": 20000, "unit": "MAH" } },
        { "id": "TIPO_CONECTOR", "value_id": "USB C", "value_name": "USB C" },
        { "id": "TIPO_CONECTOR", "value_id": "USB_A", "value_name": "USB-A" }
      ],
      "variations": [
        {
          "externalRef": "DEMO-PB20-NEG",
          "attribute_combinations": [ { "id": "COLOR", "value_id": "NEGRO", "value_name": "Negro" } ],
          "attributes": [ { "id": "GTIN", "value_name": "2000000000015" } ]
        },
        {
          "externalRef": "DEMO-PB20-BLA",
          "attribute_combinations": [ { "id": "COLOR", "value_id": "BLANCO", "value_name": "Blanco" } ],
          "attributes": [ { "id": "GTIN", "value_name": "2000000000023" } ]
        }
      ]
    }
  ]
}
Response 200, con errores por campo
{
  "success": true,
  "data": {
    "results": [
      {
        "externalRef": "DEMO-PB20",
        "valid": false,
        "outcome": "WOULD_CREATE",
        "completeness": 0.82,
        "photoResponsibility": "SUPPLIER",
        "categoryVersion": 7,
        "calculated": { "TITULO_WEB": "Batería externa Marca Demo PB-DEMO-20" },
        "errors": [
          {
            "variantRef": "DEMO-PB20-BLA",
            "path": "/items/0/variations/1/attributes/0/value_name",
            "attribute": "GTIN",
            "code": "EAN_CHECK_DIGIT",
            "message": "El dígito verificador no corresponde a un EAN-13",
            "value": "2000000000023"
          },
          {
            "path": "/items/0/attributes/3/value_id",
            "attribute": "TIPO_CONECTOR",
            "code": "VALUE_NOT_IN_LIST",
            "message": "«USB C» no está en la lista; manda el id del valor",
            "value": "USB C",
            "allowedValues": "values de TIPO_CONECTOR en GET /categories/CAT-DEMO-0412"
          }
        ],
        "warnings": [
          { "attribute": "MATERIAL", "code": "RECOMMENDED_MISSING", "message": "Recomendado para esta plantilla" }
        ]
      }
    ]
  }
}

Según la arquitectura: hasta 100 por llamada (la propuesta funcional decía 50) y sin lote de sólo validación en la v1. Los errores de negocio vienen dentro de cada producto, con 200; no reserva el EAN. ⚠️ Ilustrativo: el código de calculated.

Paso 7GET/partner/v1/products/search

Saber, antes de mandar, si el producto ya existe y qué le toca: crearlo, ofertar o nada. Se busca por GTIN (product_identifier) o por texto (q). Nunca dice qué seller es el dueño. Si existe, la misma respuesta trae el SKU y si Mirakl ya acusó: no hay otra ruta para «sobre qué ofertar».

Request
GET /partner/v1/products/search?business=MKP&status=active&product_identifier=2000000000039
Authorization: Bearer eyJ...ejemplo
Response 200, encontrado
{
  "success": true,
  "data": {
    "query": { "productIdentifier": "2000000000039", "status": "active" },
    "results": [
      {
        "id": "PRD-DEMO-0009",
        "status": "active",
        "name": "Batería externa 20,000 mAh",
        "sku": "1000000009",
        "catalog": "MARKETPLACE",
        "ownedByYou": false,
        "allowedAction": "MULTI_OFFER",
        "offer": {
          "offerType": "MULTI_OFFER",
          "miraklStatus": "ACKNOWLEDGED",
          "acknowledgedAt": "2026-10-28T17:05:12Z"
        },
        "attributes": [
          { "id": "BRAND", "value_id": "MARCADEMO", "value_name": "Marca Demo" },
          { "id": "GTIN", "value_name": "2000000000039" }
        ]
      }
    ]
  }
}

Si no hay resultado, results viene vacío y toca crear. Con q puede haber varios. miraklStatus es ACKNOWLEDGED, PENDING o FAILED, y esta consulta no reenvía nada. ⚠️ ACKNOWLEDGED dice que Mirakl recibió el envío, no que ya acepte ofertas. ⚠️ Ilustrativo: la envoltura query.

Paso 9POST/partner/v1/proposals

Mandar un producto con sus variantes. El atributo y la variante viajan como en Mercado Libre; el sobre (business, externalRef, logística) es de Liverpool. Responde en el momento: la propuesta creada y en aprobación, o el motivo por el que no se creó nada. Precio e inventario no entran: la oferta es de Mirakl.

Request, producto nuevo
POST /partner/v1/proposals
Authorization: Bearer eyJ...ejemplo
Idempotency-Key: 00000000-0000-4000-8000-000000000002
X-Request-Id: REQ-DEMO-0002
Content-Type: application/json
{
  "business": "MKP",
  "externalRef": "DEMO-PB20",
  "categoryId": "CAT-DEMO-0412",
  "categoryVersion": 7,
  "acceptedSuggestionId": "SUG-DEMO-0001",
  "title": "PB-DEMO-20 Marca Demo",
  "description": "Batería portátil de 20,000 mAh con carga rápida USB-C y dos puertos",
  "attributes": [
    { "id": "BRAND", "value_id": "MARCADEMO", "value_name": "Marca Demo" },
    { "id": "MODEL", "value_name": "PB-DEMO-20" },
    { "id": "CAPACIDAD", "value_name": "20000 mAh", "value_struct": { "number": 20000, "unit": "MAH" } },
    { "id": "TIPO_CONECTOR", "value_id": "USB_C", "value_name": "USB-C" },
    { "id": "TIPO_CONECTOR", "value_id": "USB_A", "value_name": "USB-A" }
  ],
  "variations": [
    {
      "externalRef": "DEMO-PB20-NEG",
      "attribute_combinations": [ { "id": "COLOR", "value_id": "NEGRO", "value_name": "Negro" } ],
      "attributes": [ { "id": "GTIN", "value_name": "2000000000015" } ],
      "pictures": [ { "source": "https://{host-del-seller}/imagenes/pb-demo-20-neg-1.jpg" } ]
    },
    {
      "externalRef": "DEMO-PB20-BLA",
      "attribute_combinations": [ { "id": "COLOR", "value_id": "BLANCO", "value_name": "Blanco" } ],
      "attributes": [ { "id": "GTIN", "value_name": "2000000000022" } ]
    }
  ],
  "logistics": {
    "package": { "heightCm": 3, "widthCm": 8, "lengthCm": 15, "grossWeightKg": 0.45, "netWeightKg": 0.4 }
  }
}
Request, si la búsqueda ya devolvió un id
{
  "business": "MKP",
  "externalRef": "DEMO-PB20-OFERTA",
  "categoryId": "CAT-DEMO-0412",
  "catalogProductId": "PRD-DEMO-0009"
}

Con catalogProductId no se manda la ficha. No se crea propuesta: la respuesta trae el SKU y allowedAction = MULTI_OFFER, y la oferta se crea en Mirakl. Es el mismo papel que catalog_product_id en Mercado Libre.

Response 201
{
  "success": true,
  "data": {
    "proposalId": "PROP-000123",
    "externalRef": "DEMO-PB20",
    "status": "IN_APPROVAL",
    "correctionDueAt": null,
    "links": { "self": "/partner/v1/proposals/PROP-000123" }
  }
}

Según la arquitectura: 201 en línea, con IN_APPROVAL (la propuesta funcional decía 202 con VALIDATING); ⚠️ si la prueba de carga da tiempos altos, pasaría a 202. Precio, inventario y fecha de disponibilidad no entran por esta API.

Paso 10POST/partner/v1/batches

Mandar hasta 1,000 productos de un negocio en una llamada. Acusa en el momento y procesa en segundo plano; un producto malo no detiene a los demás.

Request
POST /partner/v1/batches
Authorization: Bearer eyJ...ejemplo
Idempotency-Key: 00000000-0000-4000-8000-000000000003
X-Request-Id: REQ-DEMO-0003
Content-Type: application/json
{
  "business": "MKP",
  "items": [
    {
      "externalRef": "DEMO-PB20",
      "categoryId": "CAT-DEMO-0412",
      "categoryVersion": 7,
      "title": "PB-DEMO-20 Marca Demo",
      "attributes": [
        { "id": "BRAND", "value_id": "MARCADEMO", "value_name": "Marca Demo" },
        { "id": "CAPACIDAD", "value_name": "20000 mAh", "value_struct": { "number": 20000, "unit": "MAH" } },
        { "id": "TIPO_CONECTOR", "value_id": "USB_C", "value_name": "USB-C" },
        { "id": "TIPO_CONECTOR", "value_id": "USB_A", "value_name": "USB-A" }
      ],
      "variations": [
        {
          "externalRef": "DEMO-PB20-NEG",
          "attribute_combinations": [ { "id": "COLOR", "value_id": "NEGRO", "value_name": "Negro" } ],
          "attributes": [ { "id": "GTIN", "value_name": "2000000000015" } ]
        },
        {
          "externalRef": "DEMO-PB20-BLA",
          "attribute_combinations": [ { "id": "COLOR", "value_id": "BLANCO", "value_name": "Blanco" } ],
          "attributes": [ { "id": "GTIN", "value_name": "2000000000022" } ]
        }
      ]
    },
    {
      "externalRef": "DEMO-PB10",
      "categoryId": "CAT-DEMO-0412",
      "categoryVersion": 7,
      "title": "Batería externa de 10,000 mAh",
      "attributes": [
        { "id": "BRAND", "value_id": "MARCADEMO", "value_name": "Marca Demo" },
        { "id": "TIPO_CONECTOR", "value_id": "USB_C", "value_name": "USB-C" }
      ],
      "variations": [
        {
          "externalRef": "DEMO-PB10-NEG",
          "attribute_combinations": [ { "id": "COLOR", "value_id": "NEGRO", "value_name": "Negro" } ],
          "attributes": [ { "id": "GTIN", "value_name": "2000000000046" } ]
        }
      ]
    }
  ]
}
Response 202
{
  "success": true,
  "data": {
    "batchId": "BATCH-000045",
    "received": 2,
    "status": "RECEIVED",
    "links": { "self": "/partner/v1/batches/BATCH-000045" }
  }
}

Paso 11AVISOproposal.receivedel PIM llama al webhook del tercero

El PIM avisa que recibió el producto del lote. Todo aviso llega firmado (HMAC-SHA256 de «timestamp.cuerpo» con el secreto de la suscripción) y al menos una vez: el repetido se descarta por eventId y el orden lo da version.

Request que manda el PIM
POST https://{host-del-seller}/avisos-pim
Content-Type: application/json
X-Liverpool-Event-Id: EVT-000101
X-Liverpool-Event-Type: proposal.received
X-Liverpool-Timestamp: 1793635560
X-Liverpool-Signature: v1=ejemplo-de-firma-hmac
{
  "eventId": "EVT-000101",
  "type": "proposal.received",
  "occurredAt": "2026-11-02T16:06:00Z",
  "partnerId": "SELLER-DEMO-01",
  "business": "MKP",
  "batchId": "BATCH-000045",
  "externalRef": "DEMO-PB20",
  "status": "RECEIVED",
  "links": { "batch": "/partner/v1/batches/BATCH-000045" }
}
Response que espera el PIM, en 10 segundos o menos
HTTP/1.1 200 OK

⚠️ Ilustrativo: batchId y links.batch. El catálogo no dice si este aviso sale uno por producto o uno por lote.

Paso 13GET/partner/v1/batches/{batchId}/items

Ver el resultado de cada producto del lote, ligado a su referencia y nunca a su posición. Lo que requiere corrección no creó nada y se reenvía con la misma referencia.

Request
GET /partner/v1/batches/BATCH-000045/items?page=1&size=50
Authorization: Bearer eyJ...ejemplo
Response 200
{
  "success": true,
  "data": {
    "items": [
      {
        "externalRef": "DEMO-PB20",
        "result": "CREATED",
        "proposalId": "PROP-000123",
        "status": "IN_APPROVAL"
      },
      {
        "externalRef": "DEMO-PB10",
        "result": "CORRECTION_REQUIRED",
        "proposalId": null,
        "errors": [
          {
            "itemRef": "DEMO-PB10",
            "path": "/items/1/attributes",
            "attribute": "CAPACIDAD",
            "code": "REQUIRED",
            "message": "Falta la capacidad, obligatoria en esta plantilla",
            "value": null
          }
        ]
      }
    ],
    "page": 1,
    "size": 50,
    "total": 2
  }
}

⚠️ Ilustrativo: items.

Paso 14AVISOproposal.correction_requiredel PIM llama al webhook del tercero

Hay algo que corregir: trae los motivos por campo, la acción que toca y hasta cuándo puede hacerlo.

Request que manda el PIM
POST https://{host-del-seller}/avisos-pim
Content-Type: application/json
X-Liverpool-Event-Id: EVT-000104
X-Liverpool-Event-Type: proposal.correction_required
X-Liverpool-Timestamp: 1793639112
X-Liverpool-Signature: v1=ejemplo-de-firma-hmac
{
  "eventId": "EVT-000104",
  "type": "proposal.correction_required",
  "occurredAt": "2026-11-02T17:05:12Z",
  "partnerId": "SELLER-DEMO-01",
  "business": "MKP",
  "proposalId": "PROP-000123",
  "externalRef": "DEMO-PB20",
  "status": "CORRECTION_REQUIRED",
  "previousStatus": "IN_APPROVAL",
  "version": 4,
  "correctionDueAt": null,
  "reasons": [
    {
      "attribute": "TITULO",
      "code": "SCORE_BELOW_THRESHOLD",
      "area": "IA_MARKETPLACE",
      "message": "El título no dice qué es el producto; incluye el tipo y la capacidad"
    }
  ],
  "requiredActions": ["FIX_FIELDS"],
  "links": { "proposal": "/partner/v1/proposals/PROP-000123" }
}
Response que espera el PIM
HTTP/1.1 200 OK

Según la arquitectura: correctionDueAt vale null mientras el negocio no tenga plazo configurado. ⚠️ Ilustrativo: el valor de area, que fija el motor de estados.

Paso 15PATCH/partner/v1/proposals/{proposalId}

Corregir sólo lo que se pidió, sobre la versión que leyó; si alguien la cambió antes, responde 409.

Request
PATCH /partner/v1/proposals/PROP-000123
Authorization: Bearer eyJ...ejemplo
If-Match: "4"
Idempotency-Key: 00000000-0000-4000-8000-000000000004
Content-Type: application/json
{
  "title": "Batería externa de 20,000 mAh con carga rápida USB-C"
}
Response 200
{
  "success": true,
  "data": {
    "proposalId": "PROP-000123",
    "status": "IN_APPROVAL",
    "version": 5,
    "correctionDueAt": null
  }
}

⚠️ Ilustrativo: el valor de If-Match, version y el estado de la respuesta: la propuesta funcional dice VALIDATING, pero la arquitectura deja ese estado sólo para los lotes.

Paso 17AVISOproduct.sku_createdel PIM llama al webhook del tercero

SAP asignó el SKU: trae el SKU y el EAN de cada variante.

Request que manda el PIM
POST https://{host-del-seller}/avisos-pim
Content-Type: application/json
X-Liverpool-Event-Id: EVT-000106
X-Liverpool-Event-Type: product.sku_created
X-Liverpool-Timestamp: 1793643160
X-Liverpool-Signature: v1=ejemplo-de-firma-hmac
{
  "eventId": "EVT-000106",
  "type": "product.sku_created",
  "occurredAt": "2026-11-02T18:12:40Z",
  "partnerId": "SELLER-DEMO-01",
  "business": "MKP",
  "proposalId": "PROP-000123",
  "externalRef": "DEMO-PB20",
  "status": "SKU_CREATED",
  "previousStatus": "ACCEPTED",
  "version": 7,
  "variants": [
    { "externalRef": "DEMO-PB20-NEG", "sku": "1000000001", "ean": "2000000000015" },
    { "externalRef": "DEMO-PB20-BLA", "sku": "1000000002", "ean": "2000000000022" }
  ],
  "links": { "proposal": "/partner/v1/proposals/PROP-000123" }
}
Response que espera el PIM
HTTP/1.1 200 OK

⚠️ Ilustrativo: variants; el catálogo sólo dice «SKU y EAN».

Paso 18GET/partner/v1/proposals/{proposalId}

Consultar el estado cuando quiera: motivos, acciones pendientes, SKU por variante y canales. En el ejemplo, falta la imagen de una variante.

Request
GET /partner/v1/proposals/PROP-000123
Authorization: Bearer eyJ...ejemplo
Response 200
{
  "success": true,
  "data": {
    "proposalId": "PROP-000123",
    "externalRef": "DEMO-PB20",
    "business": "MKP",
    "status": "CORRECTION_REQUIRED",
    "version": 8,
    "correctionDueAt": null,
    "reasons": [
      { "variantRef": "DEMO-PB20-BLA", "code": "REQUIRED", "message": "Falta la imagen principal de esta variante" }
    ],
    "requiredActions": ["UPLOAD_IMAGES"],
    "variants": [
      { "externalRef": "DEMO-PB20-NEG", "sku": "1000000001", "ean": "2000000000015" },
      { "externalRef": "DEMO-PB20-BLA", "sku": "1000000002", "ean": "2000000000022" }
    ],
    "channels": []
  }
}

⚠️ Ilustrativo: version y variants.

Paso 19POST/partner/v1/media/uploads

Pedir una dirección de subida por archivo, cuando la foto no tiene URL pública. Si la tiene, va en pictures.source del alta y esta llamada no se hace. El archivo se sube con PUT antes de 15 minutos, y después se usa su mediaId.

Request
POST /partner/v1/media/uploads
Authorization: Bearer eyJ...ejemplo
Idempotency-Key: 00000000-0000-4000-8000-000000000005
Content-Type: application/json
{
  "files": [
    {
      "fileName": "pb-demo-20-bla-1.jpg",
      "contentType": "image/jpeg",
      "size": 482133,
      "sha256": "0000000000000000000000000000000000000000000000000000000000000000"
    }
  ]
}
Response 200
{
  "success": true,
  "data": {
    "files": [
      {
        "fileName": "pb-demo-20-bla-1.jpg",
        "mediaId": "MED-DEMO-0002",
        "uploadUrl": "https://{host-de-almacenamiento}/subidas/MED-DEMO-0002?firma=ejemplo",
        "expiresAt": "2026-11-02T18:45:00Z"
      }
    ]
  }
}

⚠️ Ilustrativo: files en la respuesta, fileName y expiresAt; el catálogo dice «vence en 15 min».

Paso 20PUT/partner/v1/proposals/{id}/media

Fijar el juego de imágenes de cada variante o color: cuál es la principal, en qué orden van y cuáles se publican.

Request
PUT /partner/v1/proposals/PROP-000123/media
Authorization: Bearer eyJ...ejemplo
Idempotency-Key: 00000000-0000-4000-8000-000000000006
Content-Type: application/json
{
  "items": [
    { "url": "https://{host-del-seller}/imagenes/pb-demo-20-neg-1.jpg", "type": "PRINCIPAL", "order": 1, "variantRef": "DEMO-PB20-NEG", "publish": true },
    { "mediaId": "MED-DEMO-0002", "type": "PRINCIPAL", "order": 1, "variantRef": "DEMO-PB20-BLA", "publish": true }
  ]
}
Response 200
{
  "success": true,
  "data": {
    "proposalId": "PROP-000123",
    "items": [
      { "variantRef": "DEMO-PB20-NEG", "type": "PRINCIPAL", "order": 1, "publish": true },
      { "variantRef": "DEMO-PB20-BLA", "mediaId": "MED-DEMO-0002", "type": "PRINCIPAL", "order": 1, "publish": true }
    ]
  }
}

⚠️ Ilustrativo: la forma de la respuesta; el catálogo dice «juego guardado y validado».

Paso 22AVISOproduct.publishedel PIM llama al webhook del tercero

Un canal confirmó; aquí fue el último, así que la propuesta queda publicada. Trae el detalle por canal; el acuse dice que el canal recibió el producto, no que ya se vea.

Request que manda el PIM
POST https://{host-del-seller}/avisos-pim
Content-Type: application/json
X-Liverpool-Event-Id: EVT-000110
X-Liverpool-Event-Type: product.published
X-Liverpool-Timestamp: 1793701200
X-Liverpool-Signature: v1=ejemplo-de-firma-hmac
{
  "eventId": "EVT-000110",
  "type": "product.published",
  "occurredAt": "2026-11-03T10:20:00Z",
  "partnerId": "SELLER-DEMO-01",
  "business": "MKP",
  "proposalId": "PROP-000123",
  "externalRef": "DEMO-PB20",
  "status": "PUBLISHED",
  "previousStatus": "PUBLISHING",
  "version": 12,
  "channel": "MIRAKL",
  "channels": [
    { "channel": "STORE", "status": "PUBLISHED", "publishedAt": "2026-11-03T10:12:00Z" },
    { "channel": "MIRAKL", "status": "PUBLISHED", "publishedAt": "2026-11-03T10:20:00Z" }
  ],
  "links": { "proposal": "/partner/v1/proposals/PROP-000123" }
}
Response que espera el PIM
HTTP/1.1 200 OK

⚠️ Ilustrativo: channel, channels, los códigos de canal y publishedAt. Los estados por canal sí son del catálogo: PENDING, SENT, PUBLISHED, FAILED y RETIRED.

Paso 23GET/partner/v1/events?cursor=

Recuperar en orden, desde donde se quedó, los avisos de los últimos 30 días. Es el respaldo de los webhooks.

Request
GET /partner/v1/events?cursor=EVT-000104&types=proposal.accepted,product.sku_created
Authorization: Bearer eyJ...ejemplo
Response 200
{
  "success": true,
  "data": {
    "events": [
      {
        "eventId": "EVT-000105",
        "type": "proposal.accepted",
        "occurredAt": "2026-11-02T18:00:00Z",
        "partnerId": "SELLER-DEMO-01",
        "business": "MKP",
        "proposalId": "PROP-000123",
        "externalRef": "DEMO-PB20",
        "status": "ACCEPTED",
        "previousStatus": "IN_APPROVAL",
        "version": 6
      },
      {
        "eventId": "EVT-000106",
        "type": "product.sku_created",
        "occurredAt": "2026-11-02T18:12:40Z",
        "partnerId": "SELLER-DEMO-01",
        "business": "MKP",
        "proposalId": "PROP-000123",
        "externalRef": "DEMO-PB20",
        "status": "SKU_CREATED",
        "previousStatus": "ACCEPTED",
        "version": 7
      }
    ],
    "nextCursor": "EVT-000106"
  }
}

⚠️ Ilustrativo: events, nextCursor y el formato de types. Los 30 días son un supuesto de arranque.

Paso 24GET/partner/v1/products/{id}/permissions

Saber qué puede cambiar de un producto publicado: qué es editable, qué pide aprobación y qué es de sólo lectura. Lo que no esté definido sale de sólo lectura.

Request
GET /partner/v1/products/PROP-000123/permissions
Authorization: Bearer eyJ...ejemplo
Response 200
{
  "success": true,
  "data": {
    "isContentOwner": true,
    "fieldGroups": [
      { "group": "TEXTOS", "access": "editable" },
      { "group": "ATRIBUTOS", "access": "requiresApproval" },
      { "group": "SKU_EAN_NEGOCIO", "access": "readOnly" }
    ],
    "actions": ["ADD_VARIANT", "CHANGE_MEDIA"]
  }
}

⚠️ Ilustrativo: fieldGroups, group, access y los nombres de grupo, y el {id} de /products, que aquí es el de la propuesta.

Paso 25POST/partner/v1/products/{id}/modifications

Mandar un cambio sobre un producto publicado: atributos, textos, imágenes o variantes nuevas. SKU, EAN y negocio son de sólo lectura.

Request
POST /partner/v1/products/PROP-000123/modifications
Authorization: Bearer eyJ...ejemplo
If-Match: "12"
Idempotency-Key: 00000000-0000-4000-8000-000000000007
Content-Type: application/json
{
  "changes": [
    { "id": "MATERIAL", "value_id": "ALUMINIO", "value_name": "Aluminio" }
  ],
  "addVariants": [
    {
      "externalRef": "DEMO-PB20-AZU",
      "attribute_combinations": [ { "id": "COLOR", "value_id": "AZUL", "value_name": "Azul" } ],
      "attributes": [ { "id": "GTIN", "value_name": "2000000000053" } ]
    }
  ]
}
Response 202
{
  "success": true,
  "data": {
    "modificationId": "MOD-DEMO-0001",
    "status": "IN_MODIFICATION",
    "links": { "self": "/partner/v1/modifications/MOD-DEMO-0001" }
  }
}

⚠️ Ilustrativo: la forma de cada cambio en changes y el valor de If-Match.

5. ¿Está robusta? Qué le falta para quedar al centavo

Veredicto. La base es sólida y ya está escrita: una sola puerta para sistemas, límites por consumidor que fallan cerrado, idempotencia en toda escritura, resultado por producto ligado a la referencia del tercero, avisos firmados con reintentos y un feed de respaldo, versionado con convivencia y un sandbox con el mismo contrato. Lo que falta no es de diseño sino de cierre: el contrato todavía describe en palabras muchos campos (varias fichas de arriba llevan alguno ilustrativo), no tiene un catálogo único de errores, deja abiertos algunos casos de borde y casi ningún número está medido. Siete recomendaciones bloquean el piloto, once deben quedar antes de producción y una es mejora.

Lo que ya está cubierto

TemaLo que ya tieneDónde
Seguridad y alcances del tokenOAuth 2.0 con credenciales de cliente, token de 15 minutos y siete alcances. Cada servicio vuelve a validar firma, emisor y audiencia, y el estado del consumidor se lee en cada llamada, así que una suspensión vale en la siguiente.Propuesta funcional §5.1; decisiones PD-26 y T-4
Idempotencia y reintentosIdempotency-Key en toda escritura: la misma llave da el mismo recibo, y con otro contenido, 409 IDEMPOTENCY_MISMATCH. externalRef único por tercero, If-Match al corregir y modificar, y recibos que se consultan.Propuesta funcional §5.0 y §5.9; modelo de datos
Lotes y fallas parcialesUn producto malo no tumba el lote; el resultado va por producto, ligado a la referencia y nunca a la posición; un lote que pasa el límite se rechaza completo; un negocio por lote.Propuesta funcional §5.0 y §5.5; contratos §2.2; decisiones T-2
WebhooksFirma HMAC-SHA256 de «timestamp.cuerpo» que vence a los 5 minutos, respuesta en 10 segundos, reintentos de 1 minuto a 24 horas, entrega al menos una vez con eventId y version, historial de entregas con reintento manual, aviso de prueba, rotación del secreto con 24 horas de traslape y feed de respaldo.Propuesta funcional §5.11; decisiones T-6 y Alertas
Versionado/v1 en la ruta; agregar no rompe; lo que rompe va a /v2 con 6 meses de convivencia y las cabeceras Deprecation y Sunset. Plantillas con versión, fecha de vigencia, aviso template.changed y error TEMPLATE_VERSION_OUTDATED.Propuesta funcional §5.0 y §5.2; contratos §1
Límites de usoPor consumidor y sin valor por omisión: un límite sin configurar vale null y no deja escribir. Cabeceras RateLimit-* y Retry-After, y los errores RATE_LIMITED y DAILY_QUOTA_EXCEEDED.Propuesta funcional §5.0; decisiones RF-GEN-08; contratos §1
Paginación y consultaspage, size de 1 a 200 y total; cursor en el feed; filtros por estado, fecha de cambio y referencia.Propuesta funcional §5.0, §5.5 y §5.9
Catálogo de erroresUn solo sobre con código, mensaje, correlationId y detalle por producto, variante y campo, con su ruta, el valor recibido y los valores permitidos; nueve códigos nuevos.Propuesta funcional §5.0; contratos §1
Ambiente de pruebas y pruebas de contratoSandbox con el mismo contrato, plantillas reales y SAP, Compras y canales simulados; OpenAPI 3.1, JSON Schema por plantilla y colección de Postman; pruebas de contrato de la tabla de estados.Propuesta funcional §5.12; decisiones T-7 y T-3
ObservabilidadcorrelationId en cada error, recibo por cada escritura, entrega por canal e historial de entregas de avisos.Propuesta funcional §5.0, §5.9 y §5.11
Lo que no está medidoSe dice «sin dato» en lugar de inventar, y cada número tiene cómo medirse.Decisiones RF-GEN-08

Lo que falta

#PrioridadQué hacerPor qué y fuente
1Bloquea el pilotoCerrar el contrato en un OpenAPI 3.1, con el nombre exacto de cada campo, un ejemplo por operación y por aviso, el alcance que pide cada operación y el identificador que lleva /products/{id}.El catálogo describe en palabras muchas salidas y sólo un aviso tiene cuerpo de ejemplo; arriba, varias fichas llevan campos ilustrativos. Quedan abiertos el estado tras PATCH (la propuesta funcional dice VALIDATING, que la arquitectura deja sólo para lotes) y el {id} de /products. Fuente: propuesta funcional §5.1 a §5.11 (columna «Salida») y §5.12, que ya promete el OpenAPI; contratos §1.
2Bloquea el pilotoUn catálogo único de errores: código, HTTP, cuándo sale, si se reintenta y qué hacer, con los códigos por campo.Los códigos están repartidos entre §5.0, §5.4, §5.5 y §5.10, los contratos de la arquitectura y los contratos internos; los de campo sólo aparecen en ejemplos, y varios 4xx no tienen código (imágenes, URL de webhook, sugerencias y lote demasiado grande). Que el error diga dónde está el problema es uno de los pedidos del seller. Fuente: propuesta funcional §5.0 y principio 6 (§2); contratos §1.
3Bloquea el pilotoDecir de dónde sale el valor de If-Match: devolver ETag o version en cada lectura de propuesta y de producto.Corregir y modificar exigen If-Match y responden 409 VERSION_CONFLICT, pero ninguna lectura documenta ETag ni version; sólo los avisos traen version. Fuente: propuesta funcional §5.0 (cabeceras), §5.5 y §5.10.
4Bloquea el pilotoFijar 201 o 202 para el envío individual antes de publicar el contrato: correr antes la prueba de carga, o documentar desde el primer día que el cliente acepta las dos respuestas.La arquitectura responde 201 en línea, pero si los tiempos salen altos pasa a 202; cambiarlo después rompe a quien ya integró. Fuente: contratos §2.1 ⚠️; decisiones T-2; DUD-38.
5Bloquea el pilotoUna tabla de escenarios del sandbox: qué mandar para provocar cada desenlace (Compras acepta o rechaza, marca bloqueada, ya existe, falla de SAP, faltan imágenes, publicación fallida) y qué aviso llega.El sandbox simula SAP, Compras y canales, pero la marca en externalRef que decide a Compras no está definida, ni cómo provocar los demás avisos. Fuente: propuesta funcional §5.12; decisiones T-7.
6Bloquea el pilotoMedir desde el primer día del piloto, por consumidor y operación: llamadas, errores por código, latencia p95, respuestas 429, entregas de avisos y su demora; y ligar X-Request-Id, correlationId y el aviso que resulte.La arquitectura no compromete límites ni nivel de servicio hasta medir, y esa medición sale del piloto y de la prueba de carga. Tampoco dice si correlationId es el mismo X-Request-Id. Fuente: decisiones RF-GEN-08 («Cómo medirlo»); propuesta funcional §5.0.
7Bloquea el pilotoElegir el proveedor de identidad y su plan para dar las credenciales del piloto.Sin él no hay token. Ya está registrado como duda crítica. Fuente: dudas de la arquitectura, 1; PD-26.
8Antes de producciónEl tope diario dentro de un lote: rechazar el lote completo al recibirlo si no cabe en lo que queda del día, o sumar un resultado por producto para lo que no entró.El resultado por producto tiene cuatro valores y ninguno dice «pasó el tope»; DAILY_QUOTA_EXCEEDED sólo existe como error de una llamada. Rechazar completo es coherente con «nunca se trunca». Fuente: contratos §1 y §2.2; decisiones T-5; propuesta funcional §5.0.
9Antes de producciónCerrar los casos de borde del lote: si proposal.received sale uno por producto o uno por lote (recomiendo uno por lote, o ninguno, porque el 202 ya acusa), qué quiere decir que un lote quede FAILED y qué responde una referencia repetida dentro del mismo lote.Con 1,000 productos por llamada serían 1,000 avisos de recibido. El modelo de datos ya tiene el estado FAILED y una referencia única por lote, pero el contrato no dice qué ve el tercero. Fuente: propuesta funcional §5.11 (tabla de eventos); modelo de datos, lotes.
10Antes de producciónIdempotencia completa: publicar cuánto tiempo vale una llave y qué recibe un reintento mientras la primera petición sigue en proceso.La ventana de 24 horas es un supuesto ⚠️ y el contrato no cubre el reintento simultáneo. Fuente: modelo de datos, recibos de envío; propuesta funcional §5.0.
11Antes de producciónSuscripciones que fallan siempre: cuándo se desactiva una suscripción, cómo se avisa y cómo se reactiva.Tras unos 10 intentos en 24 horas la entrega queda FAILED y el evento sigue en el feed, pero no hay regla para la suscripción, aunque el modelo ya tiene ACTIVE y DISABLED. Fuente: propuesta funcional §5.11; modelo de datos, suscripciones.
12Antes de producciónPaginación estable para sincronizar: fijar el orden de cada lista y usar cursor también en GET /proposals?updatedSince=.Las listas por página no fijan orden; si los datos cambian entre páginas, una sincronización puede saltar o repetir registros. El feed ya usa cursor. Fuente: propuesta funcional §5.0 (paginación) y §5.5.
13Antes de producciónQué versión de plantilla vale para lo que ya está en curso, la del alta o la vigente, y avisar los cambios con anticipación con su fecha de vigencia.Sin plazo de gracia en la v1, una corrección contra la versión anterior puede fallar con TEMPLATE_VERSION_OUTDATED; la propuesta funcional decía que un obligatorio nuevo aplica a las propuestas nuevas después del aviso. Fuente: decisiones T-8; contratos §1; propuesta funcional, principio 11 (§2); dudas de la arquitectura, 12.
14Antes de producciónCabeceras de límites por tipo de límite: decir qué límite reporta cada RateLimit-* (lecturas, escrituras, sugerencias o tope diario) y probar que un cliente que las respeta no recibe 429.Hay tres límites por minuto y un tope diario, y un solo juego de cabeceras. Fuente: propuesta funcional §5.0 (cabeceras); decisiones RF-GEN-08.
15Antes de producciónPruebas de contrato contra el OpenAPI en el CI, y los ejemplos públicos generados desde ese OpenAPI.Hoy sólo la tabla de estados tiene prueba de contrato, y estos ejemplos son una copia a mano que nadie compara. Fuente: decisiones T-3; propuesta funcional §5.12.
16Antes de producciónRevisión de Seguridad de la API completa antes de abrirla: alcances mínimos por operación, validación de las URL de webhook al registrarlas y en cada entrega, secretos cifrados que se muestran una sola vez y su rotación.El diseño ya prevé estas protecciones y falta verificarlas; PD-26 y las alertas los ratifica Arquitectura con Seguridad. Fuente: decisiones PD-26 y Alertas; modelo de datos, suscripciones.
17Antes de producciónMedir y publicar lo que hoy es supuesto: límites, retención del feed, de las entregas y de los recibos, plazo de corrección, máximo diario de SAP y umbral del score.Son «sin dato» o valores de arranque ⚠️; correctionDueAt vale null mientras el plazo no se decida. Fuente: decisiones RF-GEN-08; dudas de la arquitectura 2, 5, 11 y 14.
18Antes de producciónDescarga de archivos por URL: quién baja el fileUrl de los lotes y las URL de imágenes, con qué reintentos, tamaño máximo y vigencia, y cómo se reporta una URL caída.Ya está abierta para las imágenes, y el lote por archivo, de hasta 20,000 productos, tiene el mismo hueco. Fuente: propuesta funcional §5.5 y §5.8 («Abierto»); dudas de la arquitectura, 13.
19MejoraQuitar de la v1 lo que la v1 no trae y alinear las fuentes: el alcance credentials.manage sin rutas, el mode=VALIDATE_ONLY de los lotes, el tope de 50 de la validación (la arquitectura dice 100) y la notación {id} frente a {proposalId}.La propuesta funcional los sigue listando, y un integrador lee las dos. Fuente: propuesta funcional §5.1, §5.4, §5.5 y §5.8 a §5.10; decisiones T-8; contratos §1.

«Propuesta funcional» es docs/catalogacion-por-api/propuesta-flujo-catalogacion-api.md; «contratos», «decisiones», «modelo de datos» y «dudas de la arquitectura» son los archivos de docs/arquitectura/catalogacion-por-api/, en el repositorio del discovery.