Subir tu stock sin cargar el mismo auto dos veces
La regla de duplicados por patente y VIN, y qué pasa en cada choque.
AutomotorasSolo en los workspaces de automotoras.
Una automotora carga autos desde más de un lugar: a mano, desde un ERP propio, desde un portal que sincroniza solo. Tarde o temprano el mismo auto llega dos veces. Esta receta resuelve la carga completa (crear, cargar fotos, revisar y publicar) y, sobre todo, qué hace la API cuando dos cargas describen el mismo auto.
Trampa
La patente bloquea de inmediato. El VIN no
POST /vehicles rechaza una patente que ya tiene otro vehículo activo, con
409 de inmediato. Un VIN repetido con una patente distinta no se rechaza:
el segundo auto se crea igual, y el duplicado queda esperando a que corras el
motor de deduplicación. Si integras un ERP que reimporta por VIN, corre
POST /vehicles/dedup-backfill/report de forma periódica; no asumas que la
creación te avisa.
Antes de empezar
- Una key con
marketplace:writepara crear y publicar, ymarketplace:readpara leer. stock_dedup:readystock_dedup:writepara revisar y aplicar fusiones.- El
location_idde la sucursal, desde Sucursales.
1. Crea el vehículo
curl -X POST https://api.vitrinadev.com/api/v1/vehicles \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"make": "Toyota",
"model": "Yaris",
"version": "XLS 1.5",
"year": 2022,
"price_amount": 11990000,
"price_currency": "CLP",
"odometer_value": 28500,
"odometer_unit": "KM",
"type": "Car",
"listing_type": "Usado",
"registration_number": "LBXR21",
"location_id": "6812d9f0-9bed-44b5-91df-4e5b90941f6b",
"photos": [
{ "url": "https://cdn.vitrinadev.com/demo/yaris-1.jpg", "order": 0 },
{ "url": "https://cdn.vitrinadev.com/demo/yaris-2.jpg", "order": 1 }
]
}'{
"data": {
"id": "26a00eb8-62ce-4a02-a9a7-72204ed8a392",
"tenant_id": "00000000-0000-4000-8000-000000000001",
"make": "Toyota",
"model": "Yaris",
"version": "XLS 1.5",
"year": 2022,
"price_amount": 11990000,
"price_clp": 11990000,
"floor_price_clp": null,
"price_currency": "CLP",
"odometer_value": 28500,
"odometer_unit": "KM",
"registration_number": "LBXR21",
"vin": null,
"plate_normalized": "LBXR21",
"listing_type": "Usado",
"type": "Car",
"vehicle_type": "auto",
"status": "disponible",
"reserved_at": null,
"sold_at": null,
"location_id": "6812d9f0-9bed-44b5-91df-4e5b90941f6b",
"photos": [
{ "url": "https://cdn.vitrinadev.com/demo/yaris-1.jpg", "order": 0 },
{ "url": "https://cdn.vitrinadev.com/demo/yaris-2.jpg", "order": 1 }
],
"attributes": {},
"tenencia": "propio",
"published_at": null,
"created_at": "2026-09-23T11:41:56.594Z",
"updated_at": "2026-09-23T11:41:56.594Z"
}
}photos es parte del mismo objeto, no un endpoint aparte: un array de { url, order }. plate_normalized es la patente que compara la regla de duplicados. El servidor la deriva de registration_number; nunca la envías tú.
2. Cuando la patente ya está en uso
La misma llamada, con la misma patente, contra un vehículo activo:
{
"error": {
"code": "CONFLICT",
"message": "Otro vehículo activo ya tiene esta patente (conflicto de patente).",
"details": { "field": "registration_number", "reason": "plate_backstop" },
"requestId": "a94d2767-68a0-46c8-aa1a-a8b488a6463f"
}
}409 de inmediato. La regla es por patente y VIN normalizados, y solo mira vehículos activos. Un auto vendido no bloquea a uno que vuelve a entrar con la misma patente: esa es una Estadía nueva.
3. Cuando la patente no se puede leer
Una patente ilegible no se rechaza: se guarda tal cual y se marca para revisión.
curl -X POST https://api.vitrinadev.com/api/v1/vehicles \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"make": "Toyota", "model": "Yaris", "version": "XLE", "year": 2020,
"price_amount": 9500000, "price_currency": "CLP",
"type": "Car", "listing_type": "Usado",
"registration_number": "SIN PATENTE VISIBLE",
"location_id": "6812d9f0-9bed-44b5-91df-4e5b90941f6b"
}'{
"data": {
"id": "fe0c3c18-8ee4-4377-b651-5377ae9360a1",
"registration_number": "SIN PATENTE VISIBLE",
"plate_normalized": null,
"attributes": {
"curated": {
"plateRaw": "SIN PATENTE VISIBLE",
"needsReview": ["plate"],
"plateReason": "plate_unrecognized"
}
},
"status": "disponible",
"created_at": "2026-09-23T11:42:06.599Z"
}
}plate_normalized queda en null, pero Vitrina igual compara el texto crudo contra el stock activo. Una segunda carga con el mismo texto ilegible recibe el mismo 409 de arriba. El auto entra, y attributes.curated.needsReview dice qué revisar.
Para encontrar todo lo pendiente, filtra la lista:
curl "https://api.vitrinadev.com/api/v1/vehicles?needs_review=true" \
-H "Authorization: Bearer $VITRINA_KEY"4. Revisa antes de fusionar
El VIN no se compara al crear, así que dos cargas con el mismo VIN y patentes distintas conviven hasta que corres el motor de deduplicación. POST /vehicles/dedup-backfill/report no cambia nada: es la lista de fusiones propuestas.
curl -X POST https://api.vitrinadev.com/api/v1/vehicles/dedup-backfill/report \
-H "Authorization: Bearer $VITRINA_KEY"{
"data": {
"tenant_id": "00000000-0000-4000-8000-000000000001",
"scanned_vehicles": 18,
"proposals": [
{
"survivor_id": "427e1ca4-e832-4bb7-95f6-fd33870b6cb8",
"loser_id": "f3469336-31e4-4088-94ba-55bee1675241",
"matched_on": ["vin"],
"vin": "9BGKS48X0KG123456",
"survivor": { "make": "Chevrolet", "model": "Sail", "year": 2019 },
"loser": { "make": "Chevrolet", "model": "Sail", "year": 2019 }
}
],
"conflicts": [],
"summary": { "proposed_merges": 1, "clusters": 1, "conflicts": 0 }
}
}5. Aplica solo lo que revisaste
POST /vehicles/dedup-backfill/execute toma approve como filtro, no como orden: vuelve a escanear y solo fusiona un par si el escaneo fresco todavía lo propone.
curl -X POST https://api.vitrinadev.com/api/v1/vehicles/dedup-backfill/execute \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{ "approve": [{ "survivor_id": "427e1ca4-e832-4bb7-95f6-fd33870b6cb8", "loser_id": "f3469336-31e4-4088-94ba-55bee1675241" }] }'{
"data": {
"applied": [
{
"survivor_id": "427e1ca4-e832-4bb7-95f6-fd33870b6cb8",
"loser_id": "f3469336-31e4-4088-94ba-55bee1675241",
"superseded_alerts": 0
}
],
"skipped": [],
"summary": { "requested": 1, "applied": 1, "skipped": 0 }
}
}Trampa
Omitir approve y mandar approve: [] no es lo mismo
Omitir el campo aplica cada fusión propuesta en este escaneo. Mandar un array vacío aprueba cero y fusiona cero. Confundir «sin filtro» con «filtro vacío» es el error caro: uno fusiona todo tu stock de una pasada.
El vehículo perdedor no se borra: sus publicaciones, leads, cotizaciones y demás quedan re-apuntados al sobreviviente, y un vehículo fusionado nunca dispara vehicle.created de nuevo.
6. Publícalo
curl -X POST https://api.vitrinadev.com/api/v1/vehicles/26a00eb8-62ce-4a02-a9a7-72204ed8a392/publish \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{ "integration_ids": ["e6038419-209e-42e2-80ed-2a8451156a8a"] }'{
"data": [
{
"integration_id": "e6038419-209e-42e2-80ed-2a8451156a8a",
"provider": "facebook_marketplace",
"status": "pending",
"publication": {
"id": "62730397-a18e-41d0-a341-c7a344959fba",
"vehicle_id": "26a00eb8-62ce-4a02-a9a7-72204ed8a392",
"status": "pending",
"last_published_at": "2026-09-23T11:42:58.459Z"
}
}
]
}integration_ids nombra las conexiones a portales que ya configuraste en la aplicación (Automotora → Marketplaces). Conectar un portal nuevo no está en la API pública, así que ese id lo tomas de la pantalla. La publicación queda pending hasta que el portal la confirma, y vehicle.published dispara cuando eso pasa.
Crear y publicar en una sola llamada
Si tu ERP ya sabe dónde va cada auto, manda publish_to en el mismo POST /vehicles. Acepta el nombre del portal (chileautos, mercadolibre, yapo, facebook) o el id de la integración. chileautos llega a la cuenta que tengas conectada, sea la API o el panel.
curl -X POST https://api.vitrinadev.com/api/v1/vehicles \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"make": "Toyota", "model": "Yaris", "year": 2022,
"price_amount": 11990000, "registration_number": "PTZZ11",
"publish_to": ["chileautos"]
}'{
"data": {
"id": "dee55372-9db2-4cd8-b5e6-5b715ea90642",
"make": "Toyota",
"model": "Yaris",
"publications": [
{
"integration_id": "7d336584-4931-4c51-8c68-10e34e66b673",
"provider": "chileautos",
"status": "published",
"external_id": "sandbox-captured:79df5f5b-86cc-4ec5-82ca-64a6662e6328"
}
]
}
}Esta respuesta sale de un workspace de demostración, por eso el external_id es una captura y no un aviso real. Sin publish_to, o con [], el auto se guarda y no se publica en ningún lado. publications solo aparece cuando mandas el campo.
Cada entrada se valida antes de guardar. Un portal sin cuenta conectada, un nombre que coincide con dos cuentas o un id de otro workspace responden 422, y el auto no se crea:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "No hay una cuenta de Mercado Libre conectada.",
"details": {
"reason": "publish_to_invalid",
"problems": [
{ "entry": "mercadolibre", "reason": "no_connected_account", "message": "No hay una cuenta de Mercado Libre conectada." }
]
},
"field_errors": { "publish_to": "No hay una cuenta de Mercado Libre conectada." }
}
}Si el portal falla después de guardar, la respuesta sigue siendo 201: esa entrada llega con status: "error" y el motivo en error. En PUT /vehicles/{id}, publish_to solo agrega portales. Donde el auto ya está publicado o en cola responde already_published y no crea un segundo aviso, así que tu ERP puede reintentar. Un portal que no nombras no se despublica: para eso está DELETE /vehicles/{id}/publications/{pubId}.
Cuando falla
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Request validation failed",
"details": { "body": [{ "path": "integration_ids", "message": "Required", "code": "invalid_type" }] },
"field_errors": { "integration_ids": "Required" },
"requestId": "6c43dafb-07ed-4220-a3ba-47f579ebce94"
}
}Publicar sin integration_ids es el error más común: no hay un portal por defecto, así que cada llamada dice a cuáles publicar.
En la aplicación: lo mismo se hace desde Stock → Agregar vehículo. El formulario no avisa de una patente repetida mientras escribes: el conflicto aparece al guardar.

Con el stock cargado y publicado, Mostrar tu stock en tu sitio web lo saca al público, y Sacar la lista de interesados en un auto responde quién preguntó por cada unidad.