Vehículos
El stock por dentro: alta, edición, etapas, publicaciones en portales y las herramientas de mantenimiento del inventario. Pide scopes más anchos que el stock público.
Descarga la proyección completa de la API pública: openapi.json.
| Método | Ruta | Qué hace |
|---|---|---|
GET | /vehicles | Search the tenant vehicle stock (filters + free text) |
POST | /vehicles | Create a vehicle (manual stock) |
DELETE | /vehicles/{id} | Delete a vehicle |
GET | /vehicles/{id} | Get a vehicle |
PUT | /vehicles/{id} | Update a vehicle |
GET | /vehicles/{id}/activity | This car’s lead timeline |
GET | /vehicles/{id}/busquedas | Who is waiting for this car (ADR 0109 §4) |
POST | /vehicles/{id}/close-ads | Close all of a vehicle’s ads |
GET | /vehicles/{id}/document-discrepancies | What this car’s official papers disagree with |
POST | /vehicles/{id}/document-discrepancies | Record that an official document disagrees with stock |
POST | /vehicles/{id}/document-discrepancies/{discrepancyId}/apply | Apply the document — the document wins |
POST | /vehicles/{id}/document-discrepancies/{discrepancyId}/dismissal | Dismiss the document — stock stands |
GET | /vehicles/{id}/documents | Every document of one car |
GET | /vehicles/{id}/duplicate-candidates | Possible duplicates of this car |
GET | /vehicles/{id}/ficha-tecnica | The printable spec sheet for one unit |
POST | /vehicles/{id}/fields/protect | Protect (lock) vehicle fields against sync |
POST | /vehicles/{id}/fields/release | Release Override local protection on vehicle fields |
GET | /vehicles/{id}/history | A vehicle's change history (Historial) |
GET | /vehicles/{id}/interest | Who is asking about this car |
POST | /vehicles/{id}/merge | Merge two cars into one |
PUT | /vehicles/{id}/pipeline | Place a vehicle on a reconditioning board, move boards, or remove it |
PUT | /vehicles/{id}/prep-assignee | Name (or un-name) the person driving a vehicle's preparación |
GET | /vehicles/{id}/publications | List marketplace publications of a vehicle |
DELETE | /vehicles/{id}/publications/{pubId} | Unpublish a vehicle from a marketplace |
POST | /vehicles/{id}/publish | Publish a vehicle to one or more connected marketplaces |
PUT | /vehicles/{id}/stage | Move a vehicle to another column of its board |
PUT | /vehicles/{id}/stage-block | Record (or clear) why a vehicle is stuck in its current column |
GET | /vehicles/{id}/suggestions | Paperwork this hand-moved unit is still missing |
POST | /vehicles/{id}/suggestions/{suggestionId}/dismiss | Dismiss the paperwork nudge |
GET | /vehicles/catalog/brands | List car makes (catalog) |
GET | /vehicles/catalog/models | List the models of a make (catalog) |
GET | /vehicles/catalog/versions | List the versions of a make + model (catalog) |
POST | /vehicles/close-ads-bulk | Close N vehicles' ads on every connected marketplace |
POST | /vehicles/dedup-backfill/execute | Apply the reviewed dedup merges |
POST | /vehicles/dedup-backfill/report | Preview the existing-stock dedup |
GET | /vehicles/document-discrepancies | The workspace queue of document-vs-stock disagreements |
POST | /vehicles/duplicate-candidates/{candidateId}/dismiss | Dismiss a possible duplicate |
PATCH | /vehicles/estacionamientos | Bulk-set vehicles' Estacionamiento (patio position) |
GET | /vehicles/export | Export the Stock list as a PDF hoja de patio or an Excel table |
GET | /vehicles/field-visibility | Where each vehicle field publishes (the editor’s marker source) |
GET | /vehicles/kanban | The whole reconditioning board for one vehicle pipeline |
POST | /vehicles/parity-report | Compare native stock against the live source |
GET | /vehicles/photo-labels | What each stock photo shows (exterior, interior, motor…) |
PUT | /vehicles/photo-labels | Correct what a stock photo shows |
POST | /vehicles/photo-labels/label | Label new uploads now |
POST | /vehicles/photo-mirror/execute | Mirror a batch of photos into Vitrina storage |
POST | /vehicles/photo-mirror/report | Preview the photo-mirror backfill |
POST | /vehicles/photo-mirror/sweep | Mirror every remaining hotlink in one call |
GET | /vehicles/publications | Publications for many vehicles at once |
POST | /vehicles/publish-bulk | Publish many vehicles to one portal |
POST | /vehicles/source-retirement/execute | Retire a source |
POST | /vehicles/source-retirement/report | Preview retiring a source |
GET | /vehicles/stats | Aggregate counts for the whole lot |
POST | /vehicles/tenencia | Declare a car propio — no paperwork required |
GET /vehicles
Search the tenant vehicle stock (filters + free text)
FIELD-LEVEL COST OMISSION (ERP S3-6): the per-unit money position — floor_price_clp (precio piso) — is withheld unless the caller holds dealership_economics:read. The key is ABSENT from the response object, not null: a caller who checks 'floor_price_clp' in body sees it was never offered. price_clp (the asking price) is unaffected — it is the one money figure about a unit that is meant to be seen. The scope is NOT an alternative to marketplace:read (requireScope ORs its varargs, which would widen this route rather than narrow the field); it is an addition.
TENENCIA (ADR 0025). tenencia filters the vehicle-level discriminator (propio | consignacion); consignacion_modalidad narrows a consignación by its ACTIVE contract — en_local, virtual (the car stays with its dueño) or sin_contrato (consigned with no contract in estado=activo on file). consignacion_modalidad is REFUSED with 400 unless tenencia=consignacion came with it: it names a kind of consignación, so it is only answerable about one.
Every row carries tenencia and not_on_lot — the latter true iff an ACTIVE contract with modalidad=virtual exists, resolved in ONE batched query for the page (never per row). not_on_lot is the flag a test-drive or walking-sheet surface acts on; tenencia alone cannot answer it, because the modalidad lives on the contract.
POSIBLE DUPLICADO. Every row carries possible_duplicate: true when an open duplicate candidate names the car (same photos and specs as another car, plates one character apart or an implausible VIN). The pairs are at GET /vehicles/\{id\}/duplicate-candidates.
⚠ tenencia=propio IS THE COLUMN DEFAULT, not a recorded decision: that cohort includes every unit nobody has classified yet. Do not word it as "comprado".
Phase 2 (tenencia_source): tenencia_source COMPOSES with tenencia rather than narrowing it — ?tenencia=propio&tenencia_source=decided is "propio, and a person or a document actually settled that" (decided ⇒ tenencia_source <> 'default'); document / declared / default match the column exactly. See POST /vehicles/tenencia for how a declared row is produced.
SIN COSTO (ADR 0114 §8). A car that entered without a compra (nota de compra), a consignación (contrato) or a parte de pago (nota de venta) has no acquisition, so nothing records what it cost. Every row carries sin_costo: true for such a unit (any status), and view=sin_costo lists the pile still on the floor — active, not vendido, no acquisition; the SAME predicate as the sin_costo count on GET /vehicles/stats and the stock_without_cost dealer alert. It is never a gate: bulk imports and migrations land cars here on purpose. sin_costo is a statement about the cost basis and follows the cost omission above: ABSENT without dealership_economics:read.
MANDATO SIN FIRMAR (ADR 0114 §5). view=mandato_sin_firmar lists the cars whose consignación (activa or vendida) has an emitted mandato and no signed copy filed in the expediente — the SAME predicate as the mandato_sin_firmar count on GET /vehicles/stats. Never a gate, and no longer a dealer alert (retired 2026-11-10: it is a filter you open, not a to-do).
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
q | query | string | no | |
make | query | string | no | |
model | query | string | no | |
min_price | query | number | null | no | |
max_price | query | number | null | no | |
year_from | query | integer | null | no | |
year_to | query | integer | null | no | |
min_km | query | integer | no | |
max_km | query | integer | no | |
transmission | query | string | no | |
fuel_type | query | string | no | |
body_style | query | string | no | |
color | query | string | no | |
status | query | disponible \ | reservado \ | vendido |
location_id | query | uuid | no | |
vehicle_type | query | auto \ | camion \ | maquinaria \ |
has_error | query | any | no | |
needs_review | query | any | no | |
sin_estacionamiento | query | any | no | |
sin_sucursal | query | any | no | |
papers_expired | query | boolean | no | |
papers_expiring | query | boolean | no | |
tenencia | query | propio \ | consignacion | no |
consignacion_modalidad | query | en_local \ | virtual \ | sin_contrato |
tenencia_source | query | document \ | declared \ | default \ |
without_acquisition | query | any | no | |
view | query | published \ | unpublished \ | error \ |
active | query | any | no | |
sort | query | price_asc \ | price_desc \ | year_desc \ |
limit | query | integer | no | ≥ 1, ≤ 100 |
offset | query | integer | null | no | ≥ 0 |
curl https://api.vitrinadev.com/api/v1/vehicles \
-H "Authorization: Bearer $VITRINA_KEY"Ejemplo de respuesta (200)
{
"data": [
{
"id": "e1e1e1e1-0000-4000-8000-000000000001",
"tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
"origin_integration_id": null,
"source": "manual",
"make": "Toyota",
"model": "Corolla",
"version": "1.8 XEI CVT",
"year": 2022,
"title": null,
"short_title": null,
"price_amount": 13990000,
"price_clp": 13990000,
"watch_enabled": false,
"floor_price_clp": null,
"target_position": null,
"abono_amount_clp": 300000,
"merged_into": null,
"deleted_at": null,
"price_currency": "CLP",
"odometer_value": 42500,
"odometer_unit": "KM",
"fuel_type": "Bencina",
"gear_type": "Automática",
"body_style": "Sedán",
"color": "Gris",
"doors": 4,
"registration_number": "FGXY21",
"vin": "9BWZZZ377NP004512",
"plate_normalized": "FGXY21",
"listing_type": "Usado",
"record_type": null,
"type": "Car",
"merch_label": null,
"featured_at": null,
"vehicle_type": "auto",
"sale_status": null,
"status": "disponible",
"reserved_at": null,
"reserved_by_name": null,
"sold_at": null,
"location_id": "b1b1b1b1-0000-4000-8000-000000000001",
"estacionamiento": null,
"description": "Mantenimientos al día en concesionario.",
"photos": [
{
"url": "https://images.example.cl/corolla-2022-1.jpg",
"order": 0
}
],
"attributes": {},
"tags": [],
"searchable_text": "toyota corolla 1.8 xei cvt sedán bencina automática gris 2022",
"active": true,
"protected_fields": [],
"deactivated_by": null,
"tenencia": "propio",
"tenencia_source": "default",
"tenencia_declared_by": null,
"tenencia_declared_at": null,
"iva_regime": null,
"keys_count": null,
"municipalidad_comuna_code": null,
"last_service_date": null,
"service_odometer_km": null,
"published_at": null,
"last_seen_at": null,
"created_at": "2026-09-15T18:34:48.575Z",
"updated_at": "2026-09-15T18:34:48.575Z",
"live_sale_note": null,
"active_reservation": null,
"sin_costo": true,
"not_on_lot": false
}
],
"meta": {
"pagination": {
"limit": 20,
"offset": 0
},
"total": 1
}
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
POST /vehicles
Create a vehicle (manual stock)
plate_normalized is server-derived from registration_number; a plate already claimed by another ACTIVE vehicle of the tenant returns 409 (conflicto de patente).
Plate format. For a road-registered type (auto, camion) a non-empty registration_number must be a Chilean plate: AB1234 (old series), BCDF12 (new series: consonants only, no M/N/Q), or for motorcycles AB123 / BCD12; separators, case and a trailing registry check digit are tolerated. Anything else is a 400 VALIDATION_ERROR with details.reason = "plate_format_invalid" and a Spanish fieldErrors.registration_number. An empty value and a pending declaration («en trámite») stay legal; maquinaria/nautico are not checked. Stock syncs from portals and feeds do not pass through this check.
Publish in the same call (publish_to). Optional. Name the marketplaces this vehicle should go to — portal slugs such as chileautos / mercadolibre, or integration ids from GET /marketplaces — and it is published right after it is saved, through the same path as POST /vehicles/{id}/publish. Omitted or [] saves only; there is no default. Every entry is validated first: a portal with no connected account, a slug matching several accounts, or an id from another workspace answers 422 and nothing is saved. Once saved, the vehicle stays saved: a portal that fails to publish is an error entry in publications, never a failed request (still 201). The response then carries publications, one PublishOutcome per account (published, pending for queued transports such as chileautos_panel, already_published, or error with the reason). publications is absent when publish_to was not sent.
Sanity range. A price fans out to every connected portal, so a typo is stopped here. price_amount must be between 100.000 and 2.000.000.000 CLP (a UF price is bounded at the day's UF; other currencies are not bounded) and odometer_value between 0 and 2.000.000; null clears either and is always accepted. Outside the range: 422 VEHICLE_VALUE_OUT_OF_RANGE, with a Spanish message, field_errors.<field> and details.violations[] (field, reason = price_out_of_range | km_out_of_range, value, min, max, currency). A price change beyond 10x up or down from the current price answers 422 PRICE_CHANGE_CONFIRMATION_REQUIRED (details.reason = large_price_change, current_price, requested_price, currency, factor, confirm_flag) unless the body carries confirm_large_price_change: true; ask the user, then resend the same body with it. Nothing is written on either 422.
Normalization and catalog_match. On create and update, make and model that equal an entry of GET /vehicles/catalog/brands and /catalog/models up to case, accents, spaces and hyphens are stored in the catalog's spelling ("toyota" → "Toyota", "mercedes benz" → "Mercedes-Benz", "rav 4" → "RAV4"); fuel_type, gear_type and body_style are stored as vocabulary codes ("Bencina" → gasolina, "Automática" → automatica, "Station Wagon" → wagon; codes: fuel gasolina|diesel|hibrido|electrico|gnc|glp|otro, gear mecanica|automatica|otro, body sedan|hatchback|suv|camioneta|coupe|wagon|furgon|convertible|minivan|otro). Nothing is invented: a value with no match is stored exactly as sent. The response (and GET /vehicles/{id}) carries catalog_match: make / model are matched | unmatched | missing (not_applicable for a non-auto vehicle_type); version adds approximate (word overlap with a catalog version, same displacement) and catalog_version names the catalog version it matched or most resembles; fuel_type / gear_type / body_style are matched | unmatched | missing; warnings[] lists \{ field, code, message \} for everything not matched, with a Spanish message ready to show (codes: make_missing, make_not_in_catalog, model_missing, model_not_in_catalog, version_approximate, version_not_in_catalog, fuel_type_not_recognized, gear_type_not_recognized, body_style_not_recognized). A make or model that is not matched still saves, but Chileautos publishes it without a technical sheet (ficha). catalog_match is null only when the catalog could not be loaded. Example of an unmatched push: {"make":"matched","model":"unmatched","version":"unmatched","catalog_version":null,"fuel_type":"unmatched","gear_type":"matched","body_style":"missing","warnings":[{"field":"model","code":"model_not_in_catalog","message":"Modelo 'Rav-4 xx' no está en el catálogo de Toyota (GET /vehicles/catalog/models); Chileautos lo publicará sin ficha."},{"field":"fuel_type","code":"fuel_type_not_recognized","message":"Combustible 'Hidrógeno' no se reconoce; valores aceptados: gasolina, diesel, hibrido, electrico, gnc, glp, otro."}]}
Photos are re-hosted. A photos[].url that is not already a Vitrina media url is copied, shortly AFTER the response, onto the Vitrina media host and the stored url is rewritten to that copy, in the same order with every other photo field kept. The copy is normalized: EXIF orientation applied to the pixels, EXIF/GPS/XMP removed, long edge capped at 2560 px, re-encoded only when it had to change. Only public https urls are fetched (private/internal addresses are refused) and only image bodies up to 12 MB; a photo that cannot be fetched keeps the url you sent. Re-sending a url that was already copied is stored as the copy straight away, so pushing the same gallery again is not a change. The response of THIS call still shows the urls as sent; read the vehicle again to see the rewritten ones. The rewrite advances updated_at but writes no history entry, triggers no portal republish and emits no webhook.
Cuerpo
| Campo | Tipo | Obligatorio | Restricciones |
|---|---|---|---|
make | string | null | — | máx. 120 |
model | string | null | — | máx. 120 |
version | string | null | — | máx. 120 |
year | integer | null | — | ≥ 1900, ≤ 2100 |
title | string | null | — | máx. 255 |
short_title | string | null | — | máx. 255 |
price_amount | number | null | — | ≥ 0 |
price_currency | string | — | máx. 8 |
odometer_value | integer | null | — | ≥ 0 |
odometer_unit | string | — | máx. 8 |
fuel_type | string | null | — | máx. 60 |
gear_type | string | null | — | máx. 60 |
body_style | string | null | — | máx. 60 |
color | string | null | — | máx. 60 |
doors | integer | null | — | ≥ 0, ≤ 10 |
registration_number | string | null | — | máx. 60 |
vin | string | null | — | máx. 32 |
listing_type | string | null | — | máx. 30 |
record_type | string | null | — | máx. 120 |
type | string | null | — | máx. 30 |
merch_label | oferta \ | liquidacion \ | poco_km \ |
is_featured | boolean | — | |
sold_outside_vitrina | boolean | — | |
vehicle_type | auto \ | camion \ | maquinaria \ |
sale_status | string | null | — | máx. 60 |
status | disponible \ | reservado \ | vendido |
location_id | string | null | — | |
estacionamiento | string | null | — | |
watch_enabled | boolean | — | |
floor_price_clp | integer | null | — | > 0 |
target_position | integer | null | — | ≥ 25, ≤ 75 |
abono_amount_clp | integer | — | ≥ 1, ≤ 999999999 |
description | string | null | — | |
photos | object[] | — | |
attributes | object | — | |
tags | string[] | — | |
active | boolean | — | |
keys_count | integer | null | — | ≥ 0 |
municipalidad_comuna_code | string | null | — | patrón ^[0-9]{5}$ |
last_service_date | string | null | — | patrón ^\d{4}-\d{2}-\d{2}$ |
service_odometer_km | integer | null | — | ≥ 0 |
responsible_user_id | string | null | — | |
publish_to | string[] | — |
curl -X POST https://api.vitrinadev.com/api/v1/vehicles \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"make": "Toyota",
"model": "Corolla",
"version": "1.8 XEI CVT",
"year": 2022,
"price_amount": 13990000,
"odometer_value": 42500,
"fuel_type": "Bencina",
"gear_type": "Automática",
"body_style": "Sedán",
"color": "Gris",
"registration_number": "FGXY21",
"vin": "9BWZZZ377NP004512",
"location_id": "b1b1b1b1-0000-4000-8000-000000000001",
"publish_to": [
"chileautos",
"mercadolibre"
]
}'Ejemplo de respuesta (201)
{
"data": {
"id": "e1e1e1e1-0000-4000-8000-000000000001",
"tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
"origin_integration_id": null,
"source": "manual",
"make": "Toyota",
"model": "Corolla",
"version": "1.8 XEI CVT",
"year": 2022,
"title": null,
"short_title": null,
"price_amount": 13990000,
"price_clp": 13990000,
"watch_enabled": false,
"floor_price_clp": null,
"target_position": null,
"abono_amount_clp": 300000,
"merged_into": null,
"deleted_at": null,
"price_currency": "CLP",
"odometer_value": 42500,
"odometer_unit": "KM",
"fuel_type": "gasolina",
"gear_type": "automatica",
"body_style": "sedan",
"color": "Gris",
"doors": 4,
"registration_number": "FGXY21",
"vin": "9BWZZZ377NP004512",
"plate_normalized": "FGXY21",
"listing_type": "Usado",
"record_type": null,
"type": "Car",
"merch_label": null,
"featured_at": null,
"vehicle_type": "auto",
"sale_status": null,
"status": "disponible",
"reserved_at": null,
"reserved_by_name": null,
"sold_at": null,
"location_id": "b1b1b1b1-0000-4000-8000-000000000001",
"estacionamiento": null,
"description": "Mantenimientos al día en concesionario.",
"photos": [
{
"url": "https://images.example.cl/corolla-2022-1.jpg",
"order": 0
}
],
"attributes": {},
"tags": [],
"searchable_text": "toyota corolla 1.8 xei cvt sedán bencina automática gris 2022",
"active": true,
"protected_fields": [],
"deactivated_by": null,
"tenencia": "propio",
"tenencia_source": "default",
"tenencia_declared_by": null,
"tenencia_declared_at": null,
"iva_regime": null,
"keys_count": null,
"municipalidad_comuna_code": null,
"last_service_date": null,
"service_odometer_km": null,
"published_at": null,
"last_seen_at": null,
"created_at": "2026-09-15T18:34:48.575Z",
"updated_at": "2026-09-15T18:34:48.575Z",
"live_sale_note": null,
"active_reservation": null,
"sin_costo": true,
"not_on_lot": false,
"catalog_match": {
"make": "matched",
"model": "matched",
"version": "approximate",
"catalog_version": "1.8 XEI CVT",
"fuel_type": "matched",
"gear_type": "matched",
"body_style": "matched",
"warnings": [
{
"field": "version",
"code": "version_approximate",
"message": "Versión '1.8 XEI CVT full' no coincide exactamente con el catálogo; la más parecida es '1.8 XEI CVT'."
}
]
},
"publications": [
{
"integration_id": "33333333-0000-4000-8000-000000000003",
"provider": "chileautos_panel",
"status": "pending",
"external_id": null,
"publication": {
"id": "88888888-0000-4000-8000-000000000003",
"tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
"vehicle_id": "e1e1e1e1-0000-4000-8000-000000000001",
"marketplace_account_id": "33333333-0000-4000-8000-000000000003",
"provider": "chileautos_panel",
"external_id": null,
"status": "pending",
"origin": "vitrina",
"permalink": null,
"last_synced_at": null,
"last_published_at": null,
"last_error": null,
"close_retry_count": 0,
"portal_state": null,
"portal_state_at": null,
"last_push_status": "updated",
"last_push_at": "2026-09-19T09:58:00.000Z",
"last_push_error": null,
"last_push_fields": [
"priceAmount"
],
"push_delivery": "api",
"raw_provider_payload": null,
"created_at": "2026-09-16T09:00:00.000Z",
"updated_at": "2026-09-19T10:00:00.000Z"
}
},
{
"integration_id": "33333333-0000-4000-8000-000000000001",
"provider": "mercadolibre",
"status": "published",
"external_id": "MLC-1234567890",
"publication": {
"id": "88888888-0000-4000-8000-000000000001",
"tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
"vehicle_id": "e1e1e1e1-0000-4000-8000-000000000001",
"marketplace_account_id": "33333333-0000-4000-8000-000000000001",
"provider": "mercadolibre",
"external_id": "MLC-1234567890",
"status": "published",
"origin": "vitrina",
"permalink": "https://articulo.mercadolibre.cl/MLC-1234567890",
"last_synced_at": "2026-09-19T10:00:00.000Z",
"last_published_at": "2026-09-16T09:00:00.000Z",
"last_error": null,
"close_retry_count": 0,
"portal_state": "active",
"portal_state_at": "2026-09-19T10:00:00.000Z",
"last_push_status": "updated",
"last_push_at": "2026-09-19T09:58:00.000Z",
"last_push_error": null,
"last_push_fields": [
"priceAmount"
],
"push_delivery": "api",
"raw_provider_payload": null,
"created_at": "2026-09-16T09:00:00.000Z",
"updated_at": "2026-09-19T10:00:00.000Z"
}
}
]
}
}Responde: 201 · 400 · 401 · 403 · 404 · 409 · 422 · 429
DELETE /vehicles/{id}
Delete a vehicle
Soft-delete: the row and its history remain, deleted_at is stamped, and it drops out of every default list and out of the public stock API. Before the soft-delete it schedules the close of every live ad on every connected portal (the same retry-backed pipeline as POST /vehicles/\{id\}/close-ads). A vehicle a negocio holds (reservado, vendido, or carrying a live nota de venta) cannot be deleted: 409 with details.code: "vehicle_held_by_negocio". Deleting an already deleted vehicle is a 204 no-op.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
id | path | uuid | sí |
curl -X DELETE https://api.vitrinadev.com/api/v1/vehicles/<id> \
-H "Authorization: Bearer $VITRINA_KEY"Responde: 204 · 400 · 401 · 403 · 404 · 409 · 429
GET /vehicles/{id}
Get a vehicle
FIELD-LEVEL COST OMISSION (ERP S3-6): the per-unit money position — floor_price_clp (precio piso) — is withheld unless the caller holds dealership_economics:read. The key is ABSENT from the response object, not null: a caller who checks 'floor_price_clp' in body sees it was never offered. price_clp (the asking price) is unaffected — it is the one money figure about a unit that is meant to be seen. The scope is NOT an alternative to marketplace:read (requireScope ORs its varargs, which would widen this route rather than narrow the field); it is an addition.
TENENCIA (ADR 0025): the response carries tenencia and not_on_lot — the SAME two signals the Stock list row carries (composeStockRows), resolved the SAME way (consignmentRepo.listActiveModalidadByVehicleIds + isOffLot), so the ficha can never disagree with the list about which cars are off the lot. not_on_lot is true iff an ACTIVE contract with modalidad=virtual exists; tenencia alone cannot answer it, because the modalidad lives on the contract.
Normalization and catalog_match. On create and update, make and model that equal an entry of GET /vehicles/catalog/brands and /catalog/models up to case, accents, spaces and hyphens are stored in the catalog's spelling ("toyota" → "Toyota", "mercedes benz" → "Mercedes-Benz", "rav 4" → "RAV4"); fuel_type, gear_type and body_style are stored as vocabulary codes ("Bencina" → gasolina, "Automática" → automatica, "Station Wagon" → wagon; codes: fuel gasolina|diesel|hibrido|electrico|gnc|glp|otro, gear mecanica|automatica|otro, body sedan|hatchback|suv|camioneta|coupe|wagon|furgon|convertible|minivan|otro). Nothing is invented: a value with no match is stored exactly as sent. The response (and GET /vehicles/{id}) carries catalog_match: make / model are matched | unmatched | missing (not_applicable for a non-auto vehicle_type); version adds approximate (word overlap with a catalog version, same displacement) and catalog_version names the catalog version it matched or most resembles; fuel_type / gear_type / body_style are matched | unmatched | missing; warnings[] lists \{ field, code, message \} for everything not matched, with a Spanish message ready to show (codes: make_missing, make_not_in_catalog, model_missing, model_not_in_catalog, version_approximate, version_not_in_catalog, fuel_type_not_recognized, gear_type_not_recognized, body_style_not_recognized). A make or model that is not matched still saves, but Chileautos publishes it without a technical sheet (ficha). catalog_match is null only when the catalog could not be loaded. Example of an unmatched push: {"make":"matched","model":"unmatched","version":"unmatched","catalog_version":null,"fuel_type":"unmatched","gear_type":"matched","body_style":"missing","warnings":[{"field":"model","code":"model_not_in_catalog","message":"Modelo 'Rav-4 xx' no está en el catálogo de Toyota (GET /vehicles/catalog/models); Chileautos lo publicará sin ficha."},{"field":"fuel_type","code":"fuel_type_not_recognized","message":"Combustible 'Hidrógeno' no se reconoce; valores aceptados: gasolina, diesel, hibrido, electrico, gnc, glp, otro."}]}
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
id | path | uuid | sí |
curl https://api.vitrinadev.com/api/v1/vehicles/<id> \
-H "Authorization: Bearer $VITRINA_KEY"Ejemplo de respuesta (200)
{
"data": {
"id": "e1e1e1e1-0000-4000-8000-000000000001",
"tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
"origin_integration_id": null,
"source": "manual",
"make": "Toyota",
"model": "Corolla",
"version": "1.8 XEI CVT",
"year": 2022,
"title": null,
"short_title": null,
"price_amount": 13990000,
"price_clp": 13990000,
"watch_enabled": false,
"floor_price_clp": null,
"target_position": null,
"abono_amount_clp": 300000,
"merged_into": null,
"deleted_at": null,
"price_currency": "CLP",
"odometer_value": 42500,
"odometer_unit": "KM",
"fuel_type": "gasolina",
"gear_type": "automatica",
"body_style": "sedan",
"color": "Gris",
"doors": 4,
"registration_number": "FGXY21",
"vin": "9BWZZZ377NP004512",
"plate_normalized": "FGXY21",
"listing_type": "Usado",
"record_type": null,
"type": "Car",
"merch_label": null,
"featured_at": null,
"vehicle_type": "auto",
"sale_status": null,
"status": "disponible",
"reserved_at": null,
"reserved_by_name": null,
"sold_at": null,
"location_id": "b1b1b1b1-0000-4000-8000-000000000001",
"estacionamiento": null,
"description": "Mantenimientos al día en concesionario.",
"photos": [
{
"url": "https://images.example.cl/corolla-2022-1.jpg",
"order": 0
}
],
"attributes": {},
"tags": [],
"searchable_text": "toyota corolla 1.8 xei cvt sedán bencina automática gris 2022",
"active": true,
"protected_fields": [],
"deactivated_by": null,
"tenencia": "propio",
"tenencia_source": "default",
"tenencia_declared_by": null,
"tenencia_declared_at": null,
"iva_regime": null,
"keys_count": null,
"municipalidad_comuna_code": null,
"last_service_date": null,
"service_odometer_km": null,
"published_at": null,
"last_seen_at": null,
"created_at": "2026-09-15T18:34:48.575Z",
"updated_at": "2026-09-15T18:34:48.575Z",
"live_sale_note": null,
"active_reservation": null,
"sin_costo": true,
"not_on_lot": false,
"catalog_match": {
"make": "matched",
"model": "matched",
"version": "approximate",
"catalog_version": "1.8 XEI CVT",
"fuel_type": "matched",
"gear_type": "matched",
"body_style": "matched",
"warnings": [
{
"field": "version",
"code": "version_approximate",
"message": "Versión '1.8 XEI CVT full' no coincide exactamente con el catálogo; la más parecida es '1.8 XEI CVT'."
}
]
}
}
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
PUT /vehicles/{id}
Update a vehicle
Updating registration_number re-derives plate_normalized; a collision with another ACTIVE vehicle of the tenant returns 409 (conflicto de patente). A CHANGED plate is held to the same Chilean formats as on POST /vehicles (400 plate_format_invalid); re-sending the stored value unchanged is never refused.
publish_to only ADDS portals. Same contract as on POST /vehicles (validated before the edit is written; 422 writes nothing; a portal failure is an error entry in publications, never a failed request). A portal where the vehicle is already live or has a queued publish is a no-op (already_published) — never a second ad, so an ERP may safely retry. A portal LEFT OUT of publish_to is NOT unpublished: removal is DELETE /vehicles/{id}/publications/{pubId}. A body with only publish_to publishes without editing the vehicle.
Sanity range. A price fans out to every connected portal, so a typo is stopped here. price_amount must be between 100.000 and 2.000.000.000 CLP (a UF price is bounded at the day's UF; other currencies are not bounded) and odometer_value between 0 and 2.000.000; null clears either and is always accepted. Outside the range: 422 VEHICLE_VALUE_OUT_OF_RANGE, with a Spanish message, field_errors.<field> and details.violations[] (field, reason = price_out_of_range | km_out_of_range, value, min, max, currency). A price change beyond 10x up or down from the current price answers 422 PRICE_CHANGE_CONFIRMATION_REQUIRED (details.reason = large_price_change, current_price, requested_price, currency, factor, confirm_flag) unless the body carries confirm_large_price_change: true; ask the user, then resend the same body with it. Nothing is written on either 422.
Normalization and catalog_match. On create and update, make and model that equal an entry of GET /vehicles/catalog/brands and /catalog/models up to case, accents, spaces and hyphens are stored in the catalog's spelling ("toyota" → "Toyota", "mercedes benz" → "Mercedes-Benz", "rav 4" → "RAV4"); fuel_type, gear_type and body_style are stored as vocabulary codes ("Bencina" → gasolina, "Automática" → automatica, "Station Wagon" → wagon; codes: fuel gasolina|diesel|hibrido|electrico|gnc|glp|otro, gear mecanica|automatica|otro, body sedan|hatchback|suv|camioneta|coupe|wagon|furgon|convertible|minivan|otro). Nothing is invented: a value with no match is stored exactly as sent. The response (and GET /vehicles/{id}) carries catalog_match: make / model are matched | unmatched | missing (not_applicable for a non-auto vehicle_type); version adds approximate (word overlap with a catalog version, same displacement) and catalog_version names the catalog version it matched or most resembles; fuel_type / gear_type / body_style are matched | unmatched | missing; warnings[] lists \{ field, code, message \} for everything not matched, with a Spanish message ready to show (codes: make_missing, make_not_in_catalog, model_missing, model_not_in_catalog, version_approximate, version_not_in_catalog, fuel_type_not_recognized, gear_type_not_recognized, body_style_not_recognized). A make or model that is not matched still saves, but Chileautos publishes it without a technical sheet (ficha). catalog_match is null only when the catalog could not be loaded. Example of an unmatched push: {"make":"matched","model":"unmatched","version":"unmatched","catalog_version":null,"fuel_type":"unmatched","gear_type":"matched","body_style":"missing","warnings":[{"field":"model","code":"model_not_in_catalog","message":"Modelo 'Rav-4 xx' no está en el catálogo de Toyota (GET /vehicles/catalog/models); Chileautos lo publicará sin ficha."},{"field":"fuel_type","code":"fuel_type_not_recognized","message":"Combustible 'Hidrógeno' no se reconoce; valores aceptados: gasolina, diesel, hibrido, electrico, gnc, glp, otro."}]}
Photos are re-hosted. A photos[].url that is not already a Vitrina media url is copied, shortly AFTER the response, onto the Vitrina media host and the stored url is rewritten to that copy, in the same order with every other photo field kept. The copy is normalized: EXIF orientation applied to the pixels, EXIF/GPS/XMP removed, long edge capped at 2560 px, re-encoded only when it had to change. Only public https urls are fetched (private/internal addresses are refused) and only image bodies up to 12 MB; a photo that cannot be fetched keeps the url you sent. Re-sending a url that was already copied is stored as the copy straight away, so pushing the same gallery again is not a change. The response of THIS call still shows the urls as sent; read the vehicle again to see the rewritten ones. The rewrite advances updated_at but writes no history entry, triggers no portal republish and emits no webhook.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
id | path | uuid | sí |
Cuerpo
| Campo | Tipo | Obligatorio | Restricciones |
|---|---|---|---|
make | string | null | — | máx. 120 |
model | string | null | — | máx. 120 |
version | string | null | — | máx. 120 |
year | integer | null | — | ≥ 1900, ≤ 2100 |
title | string | null | — | máx. 255 |
short_title | string | null | — | máx. 255 |
price_amount | number | null | — | ≥ 0 |
price_currency | string | — | máx. 8 |
odometer_value | integer | null | — | ≥ 0 |
odometer_unit | string | — | máx. 8 |
fuel_type | string | null | — | máx. 60 |
gear_type | string | null | — | máx. 60 |
body_style | string | null | — | máx. 60 |
color | string | null | — | máx. 60 |
doors | integer | null | — | ≥ 0, ≤ 10 |
registration_number | string | null | — | máx. 60 |
vin | string | null | — | máx. 32 |
listing_type | string | null | — | máx. 30 |
record_type | string | null | — | máx. 120 |
type | string | null | — | máx. 30 |
merch_label | oferta \ | liquidacion \ | poco_km \ |
is_featured | boolean | — | |
sold_outside_vitrina | boolean | — | |
vehicle_type | auto \ | camion \ | maquinaria \ |
sale_status | string | null | — | máx. 60 |
status | disponible \ | reservado \ | vendido |
location_id | string | null | — | |
estacionamiento | string | null | — | |
watch_enabled | boolean | — | |
floor_price_clp | integer | null | — | > 0 |
target_position | integer | null | — | ≥ 25, ≤ 75 |
abono_amount_clp | integer | — | ≥ 1, ≤ 999999999 |
description | string | null | — | |
photos | object[] | — | |
attributes | object | — | |
tags | string[] | — | |
active | boolean | — | |
keys_count | integer | null | — | ≥ 0 |
municipalidad_comuna_code | string | null | — | patrón ^[0-9]{5}$ |
last_service_date | string | null | — | patrón ^\d{4}-\d{2}-\d{2}$ |
service_odometer_km | integer | null | — | ≥ 0 |
responsible_user_id | string | null | — | |
publish_to | string[] | — | |
confirm_large_price_change | boolean | — |
curl -X PUT https://api.vitrinadev.com/api/v1/vehicles/<id> \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"price_amount": 13490000
}'Ejemplo de respuesta (200)
{
"data": {
"id": "e1e1e1e1-0000-4000-8000-000000000001",
"tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
"origin_integration_id": null,
"source": "manual",
"make": "Toyota",
"model": "Corolla",
"version": "1.8 XEI CVT",
"year": 2022,
"title": null,
"short_title": null,
"price_amount": 13490000,
"price_clp": 13490000,
"watch_enabled": false,
"floor_price_clp": null,
"target_position": null,
"abono_amount_clp": 300000,
"merged_into": null,
"deleted_at": null,
"price_currency": "CLP",
"odometer_value": 42500,
"odometer_unit": "KM",
"fuel_type": "gasolina",
"gear_type": "automatica",
"body_style": "sedan",
"color": "Gris",
"doors": 4,
"registration_number": "FGXY21",
"vin": "9BWZZZ377NP004512",
"plate_normalized": "FGXY21",
"listing_type": "Usado",
"record_type": null,
"type": "Car",
"merch_label": null,
"featured_at": null,
"vehicle_type": "auto",
"sale_status": null,
"status": "disponible",
"reserved_at": null,
"reserved_by_name": null,
"sold_at": null,
"location_id": "b1b1b1b1-0000-4000-8000-000000000001",
"estacionamiento": null,
"description": "Mantenimientos al día en concesionario.",
"photos": [
{
"url": "https://images.example.cl/corolla-2022-1.jpg",
"order": 0
}
],
"attributes": {},
"tags": [],
"searchable_text": "toyota corolla 1.8 xei cvt sedán bencina automática gris 2022",
"active": true,
"protected_fields": [],
"deactivated_by": null,
"tenencia": "propio",
"tenencia_source": "default",
"tenencia_declared_by": null,
"tenencia_declared_at": null,
"iva_regime": null,
"keys_count": null,
"municipalidad_comuna_code": null,
"last_service_date": null,
"service_odometer_km": null,
"published_at": null,
"last_seen_at": null,
"created_at": "2026-09-15T18:34:48.575Z",
"updated_at": "2026-09-15T18:34:48.575Z",
"live_sale_note": null,
"active_reservation": null,
"sin_costo": true,
"not_on_lot": false,
"catalog_match": {
"make": "matched",
"model": "matched",
"version": "approximate",
"catalog_version": "1.8 XEI CVT",
"fuel_type": "matched",
"gear_type": "matched",
"body_style": "matched",
"warnings": [
{
"field": "version",
"code": "version_approximate",
"message": "Versión '1.8 XEI CVT full' no coincide exactamente con el catálogo; la más parecida es '1.8 XEI CVT'."
}
]
}
}
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 422 · 429
GET /vehicles/{id}/activity
This car’s lead timeline
Every named lead that asked about this vehicle, merged with its anonymous Yapo phone-reveals, newest first. This is the car’s HISTORY, so unlike /interest a sold vehicle keeps its timeline.
Each lead row carries conversation_id: the chat to open for that person (the lead's own conversation, else the contact's most recent one, e.g. the first-touch WhatsApp after a portal inquiry). Null when there is none.
The Stock detail screen merges this with /vehicles/\{id\}/history (the change log) into one "Leads y cambios" feed — they are two endpoints because they are two different kinds of event. Automotive-vertical only: a workspace on another vertical gets 403 here however its scopes are set.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
id | path | uuid | sí | |
limit | query | integer | no | ≥ 1, ≤ 50 |
curl https://api.vitrinadev.com/api/v1/vehicles/<id>/activity \
-H "Authorization: Bearer $VITRINA_KEY"Ejemplo de respuesta (200)
{
"data": [
{
"kind": "lead",
"contact_id": "22222222-0000-4000-8000-000000000001",
"lead_id": "99999999-0000-4000-8000-000000000002",
"conversation_id": "99999999-0000-4000-8000-000000000003",
"portal": "website",
"at": "2026-09-19T15:30:00.000Z"
}
]
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
GET /vehicles/{id}/busquedas
Who is waiting for this car (ADR 0109 §4)
Which open Búsquedas (customer wants) does THIS unit answer, graded live — calza (every bound holds) or calza_parcial (the name and years hold; the budget or the kilometres are over by no more than the workspace tolerance, with the difference stated). A want recorded AFTER the car arrived is never missed — this grades the CURRENT row on every call, not an activity log from intake.
notice is what became of the notice about THIS car, or null when the customer has not heard about it yet. Búsqueda notices go out automatically. notice.state is notified or dismissed (the pair was considered and deliberately not sent); notice.at is when; notice.channel is the channel it went out on (whatsapp, email, instagram, voice, or human_self when the want was reserved by a member and only that member was told). notice.conversation_id + notice.message_id open the exact thread and message the notice went out in — both null when the notice has no thread of its own. notice.replied_at is the customer’s first message in that thread after the notice (null = no reply yet, or no thread to read). already_notified is notice != null.
⚠ SCOPE IS followups:read, not marketplace:read — this reports CRM demand, not stock. ⚠ TENANT-WIDE, not row-visibility-gated: a shared review sheet about a unit in the shared stock. contact.name / contact.phone are further gated on contacts:read — a caller without it still sees the full worklist, minus the customer’s name and phone. Automotive-vertical only: a workspace on another vertical gets 403 here however its scopes are set.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
id | path | uuid | sí |
curl https://api.vitrinadev.com/api/v1/vehicles/<id>/busquedas \
-H "Authorization: Bearer $VITRINA_KEY"Ejemplo de respuesta (200)
{
"data": [
{
"subscription_id": "5b5b5b5b-0000-4000-8000-000000000001",
"contact_id": "22222222-0000-4000-8000-000000000001",
"contact": {
"id": "22222222-0000-4000-8000-000000000001",
"name": "Camila Rojas",
"phone": "+56912345678"
},
"lead_id": "99999999-0000-4000-8000-000000000001",
"owner": {
"user_id": "33333333-0000-4000-8000-000000000001",
"name": "Valentina Soto",
"avatar_url": null
},
"status": "active",
"trigger_key": "vehicle_in_stock",
"criteria": {
"make": "Mazda",
"model": "CX-30",
"year_min": 2021,
"max_price": 18000000
},
"note": null,
"sought_label": "Mazda CX-30 · 2021 en adelante",
"expires_at": "2026-10-19T12:00:00.000Z",
"days_left": 27,
"notice_count": 0,
"verdict": "calza_parcial",
"differences": [
{
"bound": "budget",
"stated": 18000000,
"actual": 18900000,
"over": 900000,
"overFraction": 0.05,
"text": "Está $900.000 sobre el presupuesto de $18.000.000"
}
],
"already_notified": false,
"notice": null
},
{
"subscription_id": "5b5b5b5b-0000-4000-8000-000000000002",
"contact_id": "11111111-0000-4000-8000-000000000002",
"contact": {
"id": "11111111-0000-4000-8000-000000000002",
"name": "Tomás Herrera",
"phone": "+56911112222"
},
"lead_id": null,
"owner": null,
"status": "open",
"trigger_key": "vehicle_in_stock",
"criteria": {
"make": "Mazda",
"model": "CX-30"
},
"note": null,
"sought_label": "Mazda CX-30",
"expires_at": "2026-12-01T12:00:00.000Z",
"days_left": 70,
"notice_count": 1,
"verdict": "calza",
"differences": [],
"already_notified": true,
"notice": {
"state": "notified",
"at": "2026-09-29T17:02:00.000Z",
"channel": "whatsapp",
"conversation_id": "7c7c7c7c-0000-4000-8000-000000000001",
"message_id": "8d8d8d8d-0000-4000-8000-000000000001",
"replied_at": "2026-09-29T17:10:41.000Z"
}
}
],
"meta": {
"total": 2
}
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
POST /vehicles/{id}/close-ads
Close all of a vehicle’s ads
Cierre de avisos: removes the vehicle from EVERY connected portal at once. Contrast DELETE /vehicles/\{id\}/publications/\{pubId\}, which removes one publication synchronously — this closes the whole vehicle and is enqueued.
Answers \{ enqueued: true, vehicle_id \} immediately. That is an acknowledgement, not a result: the retry-backed pipeline does the portal work afterwards because portal removal fails often enough that doing it inline would strand ads. Poll the vehicle’s publications for the real state. It is enqueued with the sold intent. Automotive-vertical only: a workspace on another vertical gets 403 here however its scopes are set.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
id | path | uuid | sí |
curl -X POST https://api.vitrinadev.com/api/v1/vehicles/<id>/close-ads \
-H "Authorization: Bearer $VITRINA_KEY"Ejemplo de respuesta (200)
{
"data": {
"enqueued": true,
"vehicle_id": "e1e1e1e1-0000-4000-8000-000000000001"
}
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
GET /vehicles/{id}/document-discrepancies
What this car’s official papers disagree with
Each row carries what the document says, what stock said when it was read, and — once decided — who decided, why, and what stock held at that instant (applied_prior_value).
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
id | path | uuid | sí | |
standing | query | open \ | resolved | no |
limit | query | integer | no | ≥ 1, ≤ 500 |
curl https://api.vitrinadev.com/api/v1/vehicles/<id>/document-discrepancies \
-H "Authorization: Bearer $VITRINA_KEY"Ejemplo de respuesta (200)
{
"data": [
{
"id": "66666666-0000-4000-8000-000000000001",
"tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
"attachment_id": "f1f1f1f1-0000-4000-8000-000000000001",
"vehicle_id": "e1e1e1e1-0000-4000-8000-000000000001",
"attachment_kind": "padron",
"field": "registration_number",
"document_value": "FGXY22",
"observed_stock_value": "FGXY21",
"note": "El padrón trae una letra distinta a la patente registrada.",
"observed_at": "2026-09-18T14:00:00.000Z",
"observed_by": "11111111-0000-4000-8000-000000000001",
"resolution": null,
"resolution_reason": null,
"resolved_on": null,
"resolved_at": null,
"resolved_by": null,
"applied_prior_value": null,
"created_at": "2026-09-18T14:00:00.000Z",
"updated_at": "2026-09-18T14:00:00.000Z"
}
]
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
POST /vehicles/{id}/document-discrepancies
Record that an official document disagrees with stock
⚠ WRITES NOTHING BUT THE OBSERVATION — the vehicle is untouched, and stays untouched until a person applies it. attachment_id points into the ERP S15-20 vehicle expediente (never raw bytes), must belong to THIS car, and its kind must be one of padron, factura, certificado_anotaciones — all three issued OUTSIDE the dealership. A contrato is refused: a compraventa is dealer-asserted, and this is the one place an external fact may overrule a dealer-asserted one. field is bounded to the six a registry actually states — an unbounded list would let a factura "correct" the asking price. A reading that AGREES with stock is refused; it is not a discrepancy.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
id | path | uuid | sí |
Cuerpo
| Campo | Tipo | Obligatorio | Restricciones |
|---|---|---|---|
attachment_id | uuid | sí | |
field | registration_number \ | vin \ | year \ |
document_value | string | sí | mín. 1, máx. 120 |
note | string | — | mín. 1, máx. 500 |
curl -X POST https://api.vitrinadev.com/api/v1/vehicles/<id>/document-discrepancies \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"attachment_id": "f1f1f1f1-0000-4000-8000-000000000001",
"field": "registration_number",
"document_value": "FGXY22",
"note": "El padrón trae una letra distinta a la patente registrada."
}'Ejemplo de respuesta (201)
{
"data": {
"id": "66666666-0000-4000-8000-000000000001",
"tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
"attachment_id": "f1f1f1f1-0000-4000-8000-000000000001",
"vehicle_id": "e1e1e1e1-0000-4000-8000-000000000001",
"attachment_kind": "padron",
"field": "registration_number",
"document_value": "FGXY22",
"observed_stock_value": "FGXY21",
"note": "El padrón trae una letra distinta a la patente registrada.",
"observed_at": "2026-09-18T14:00:00.000Z",
"observed_by": "11111111-0000-4000-8000-000000000001",
"resolution": null,
"resolution_reason": null,
"resolved_on": null,
"resolved_at": null,
"resolved_by": null,
"applied_prior_value": null,
"created_at": "2026-09-18T14:00:00.000Z",
"updated_at": "2026-09-18T14:00:00.000Z"
}
}Responde: 201 · 400 · 401 · 403 · 404 · 409 · 429
POST /vehicles/{id}/document-discrepancies/{discrepancyId}/apply
Apply the document — the document wins
Rewrites the field to what the document says, THROUGH the same ownership-aware write path a hand edit takes: the corrected field joins the vehicle’s Override local set (ADR 0023), so the next portal sync does not undo it. What stock held at that instant is preserved on the discrepancy and stamped into the car’s Historial. reason is mandatory; a second decision is a 409, and there is deliberately no un-apply verb — release the field to hand it back to sync.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
id | path | uuid | sí | |
discrepancyId | path | uuid | sí |
Cuerpo
| Campo | Tipo | Obligatorio | Restricciones |
|---|---|---|---|
reason | string | sí | mín. 1, máx. 500 |
curl -X POST https://api.vitrinadev.com/api/v1/vehicles/<id>/document-discrepancies/<id>/apply \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"reason": "El padrón es el vigente; se corrige la patente en stock."
}'Ejemplo de respuesta (200)
{
"data": {
"id": "66666666-0000-4000-8000-000000000001",
"tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
"attachment_id": "f1f1f1f1-0000-4000-8000-000000000001",
"vehicle_id": "e1e1e1e1-0000-4000-8000-000000000001",
"attachment_kind": "padron",
"field": "registration_number",
"document_value": "FGXY22",
"observed_stock_value": "FGXY21",
"note": "El padrón trae una letra distinta a la patente registrada.",
"observed_at": "2026-09-18T14:00:00.000Z",
"observed_by": "11111111-0000-4000-8000-000000000001",
"resolution": "applied",
"resolution_reason": "El padrón es el vigente; se corrige la patente en stock.",
"resolved_on": "2026-09-19T09:00:00.000Z",
"resolved_at": "2026-09-19T09:00:00.000Z",
"resolved_by": "11111111-0000-4000-8000-000000000001",
"applied_prior_value": "FGXY21",
"created_at": "2026-09-18T14:00:00.000Z",
"updated_at": "2026-09-18T14:00:00.000Z"
}
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
POST /vehicles/{id}/document-discrepancies/{discrepancyId}/dismissal
Dismiss the document — stock stands
Records that the document does NOT overrule the stock record. ⚠ NEVER touches stock, and carries the same actor and the same MANDATORY reason as an apply: a dismissal nobody signed is not a decision, it is a discrepancy that quietly stopped being displayed.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
id | path | uuid | sí | |
discrepancyId | path | uuid | sí |
Cuerpo
| Campo | Tipo | Obligatorio | Restricciones |
|---|---|---|---|
reason | string | sí | mín. 1, máx. 500 |
curl -X POST https://api.vitrinadev.com/api/v1/vehicles/<id>/document-discrepancies/<id>/dismissal \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"reason": "El padrón es el vigente; se corrige la patente en stock."
}'Ejemplo de respuesta (200)
{
"data": {
"id": "66666666-0000-4000-8000-000000000001",
"tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
"attachment_id": "f1f1f1f1-0000-4000-8000-000000000001",
"vehicle_id": "e1e1e1e1-0000-4000-8000-000000000001",
"attachment_kind": "padron",
"field": "registration_number",
"document_value": "FGXY22",
"observed_stock_value": "FGXY21",
"note": "El padrón trae una letra distinta a la patente registrada.",
"observed_at": "2026-09-18T14:00:00.000Z",
"observed_by": "11111111-0000-4000-8000-000000000001",
"resolution": "dismissed",
"resolution_reason": "El padrón es del dueño anterior y es previo al regrabado.",
"resolved_on": "2026-09-19T09:00:00.000Z",
"resolved_at": "2026-09-19T09:00:00.000Z",
"resolved_by": "11111111-0000-4000-8000-000000000001",
"applied_prior_value": null,
"created_at": "2026-09-18T14:00:00.000Z",
"updated_at": "2026-09-18T14:00:00.000Z"
}
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
GET /vehicles/{id}/documents
Every document of one car
The car’s papers in one read (ADR 0114 §7), in the dealership’s order: how it entered (purchase_note, nota de compra — or the sale_note with role: parte_de_pago that took it as a trade-in), the consignment_contract (contrato de consignación, the mandato), each sale_note (nota de venta, voided ones included) followed by its compraventa (contrato de compraventa), the deal_settlement (Resumen del negocio; preview while the car is in stock) and the liquidacion (liquidación al consignante).
A paper the car is expected to have and does not is listed with status: missing (a nota de venta before the sale, a compraventa before approval, a liquidación before the consignor is paid). Other statuses: issued, approved, voided (notas), printed / unprinted (compraventa, mandato — frozen instruments), preview / final (Resumen del negocio), paid / withdrawal (liquidación).
pdf is where the PDF is read (method + path under /api/v1); print is the call that issues an unprinted instrument (needs the write scope). signable says whether a signed copy belongs on the document; signed_copy is that copy in the expediente (upload it with POST /vehicle-attachments and signed_document_type + signed_document_id), null when missing and ABSENT for a caller without vehicle_registry:read.
Requires marketplace:read. Each document type is listed only for a caller holding its read scope (purchase_notes:read, consignments:read, sale_notes:read, dealership_economics:read). No amounts.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
id | path | uuid | sí |
curl https://api.vitrinadev.com/api/v1/vehicles/<id>/documents \
-H "Authorization: Bearer $VITRINA_KEY"Ejemplo de respuesta (200)
{
"data": {
"vehicle_id": "e1e1e1e1-0000-4000-8000-000000000001",
"documents": [
{
"type": "purchase_note",
"id": "d5d5d5d5-0000-4000-8000-000000000001",
"folio": "P-34",
"issued_at": "2026-08-02T14:10:00.000Z",
"printed_at": null,
"status": "issued",
"pdf": {
"method": "GET",
"path": "/purchase-notes/d5d5d5d5-0000-4000-8000-000000000001/pdf"
},
"print": null,
"signable": true,
"signed_copy": null
},
{
"type": "sale_note",
"id": "d4d4d4d4-0000-4000-8000-000000000001",
"folio": "V-12",
"issued_at": "2026-09-20T15:02:00.000Z",
"printed_at": null,
"status": "approved",
"pdf": {
"method": "GET",
"path": "/sale-notes/d4d4d4d4-0000-4000-8000-000000000001/pdf"
},
"print": null,
"signable": true,
"signed_copy": null
},
{
"type": "compraventa",
"id": "d4d4d4d4-0000-4000-8000-000000000001",
"folio": "V-12",
"issued_at": "2026-09-20T16:40:00.000Z",
"printed_at": "2026-09-20T16:40:00.000Z",
"status": "printed",
"pdf": {
"method": "GET",
"path": "/sale-notes/d4d4d4d4-0000-4000-8000-000000000001/compraventa/pdf"
},
"print": null,
"signable": true,
"signed_copy": {
"attachment_id": "f1f1f1f1-0000-4000-8000-000000000003",
"filename": "compraventa-firmada.pdf",
"mime_type": "application/pdf",
"uploaded_at": "2026-09-21T10:05:00.000Z"
}
},
{
"type": "deal_settlement",
"id": null,
"folio": null,
"issued_at": "2026-09-20T15:30:00.000Z",
"printed_at": null,
"status": "final",
"pdf": {
"method": "GET",
"path": "/vehicles/e1e1e1e1-0000-4000-8000-000000000001/deal-settlement/pdf"
},
"print": null,
"signable": false
}
]
}
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
GET /vehicles/{id}/duplicate-candidates
Possible duplicates of this car
Posible duplicado: other cars of the workspace that share this car’s photos and specs but whose identifiers conflict in a way a person could have typed. reason is plate_typo (both plates present, one character apart) or invalid_vin (at least one VIN is implausible: not 17 characters, a non-alphanumeric character, an I/O/Q, or all digits). The nightly photo reconciliation records them; it never merges such a pair on its own.
Only OPEN candidates are listed: not dismissed, not resolved by a merge, and with both cars live (not merged, not deleted). The car may sit on either side of a pair; other_vehicle is always the OTHER car, with lead_count = the lead interests pointing at it. Act on a candidate with POST /vehicles/\{id\}/merge or POST /vehicles/duplicate-candidates/\{candidateId\}/dismiss. Automotive-vertical only: a workspace on another vertical gets 403 here however its scopes are set.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
id | path | uuid | sí |
curl https://api.vitrinadev.com/api/v1/vehicles/<id>/duplicate-candidates \
-H "Authorization: Bearer $VITRINA_KEY"Ejemplo de respuesta (200)
{
"data": [
{
"id": "0b7c4e2a-5d1f-4a8e-9c3b-2f6d8e1a4b70",
"reason": "plate_typo",
"detected_at": "2026-10-26T06:12:40.000Z",
"other_vehicle": {
"id": "88f2fa25-3c71-4e0b-9a4d-6b1e2c7d5f90",
"title": "MINI Cooper S 2017",
"plate_normalized": "JLGJ42",
"vin": "25423423423131313",
"status": "disponible",
"active": true,
"price_clp": 15990000,
"photo_url": "https://cdn.example.com/vehicles/88f2fa25/1.jpg",
"lead_count": 5,
"created_at": "2026-09-02T14:05:11.000Z"
}
}
]
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
GET /vehicles/{id}/ficha-tecnica
The printable spec sheet for one unit
Renders the "hoja de mesón" — the counter sheet a customer walking the lot takes home — and streams it as application/pdf. ⚠ IT IS NOT AN ISSUANCE. Nothing is stored: no rendered_document row, no folio, no supersede chain, and no immutability claim of any kind. Calling it twice with the vehicle edited in between returns two DIFFERENT sheets, by design — the sheet is a render of the row as it is right now, which is why there is no companion metadata route and no stored-bytes hash to verify against (contrast GET /consignments/\{id\}/contract/pdf, which REFUSES to serve bytes whose sha256 disagrees with its row).
Everything printed comes from the PUBLIC projection of the vehicle — the same DTO GET /stock/\{id\} serves to a dealer’s own website — plus the workspace’s branding and the unit’s branch. The plate, the VIN, the floor price, the target position, the IVA regime and the patio position are not expressible in that projection and never appear on the paper. Unlike /stock/\{id\}, there is NO status gate: an operator can print the sheet for a reservado or vendido unit they can already see. Scoped marketplace:read (a public-fields render, not dealer economics); 404 for another tenant’s vehicle, so the route is not an existence oracle.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
id | path | uuid | sí |
curl https://api.vitrinadev.com/api/v1/vehicles/<id>/ficha-tecnica \
-H "Authorization: Bearer $VITRINA_KEY"Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
POST /vehicles/{id}/fields/protect
Protect (lock) vehicle fields against sync
Explicitly pin the named canonical fields to their current value (ADR 0023, two-way candado) so no sync overwrites them — without editing the value first. Non-protectable names (price_amount, status, …) are a no-op. Symmetric to /fields/release.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
id | path | uuid | sí |
Cuerpo
| Campo | Tipo | Obligatorio | Restricciones |
|---|---|---|---|
fields | string[] | sí |
curl -X POST https://api.vitrinadev.com/api/v1/vehicles/<id>/fields/protect \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"fields": [
"price_amount"
]
}'Ejemplo de respuesta (200)
{
"data": {
"id": "e1e1e1e1-0000-4000-8000-000000000001",
"tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
"origin_integration_id": null,
"source": "manual",
"make": "Toyota",
"model": "Corolla",
"version": "1.8 XEI CVT",
"year": 2022,
"title": null,
"short_title": null,
"price_amount": 13990000,
"price_clp": 13990000,
"watch_enabled": false,
"floor_price_clp": null,
"target_position": null,
"abono_amount_clp": 300000,
"merged_into": null,
"deleted_at": null,
"price_currency": "CLP",
"odometer_value": 42500,
"odometer_unit": "KM",
"fuel_type": "Bencina",
"gear_type": "Automática",
"body_style": "Sedán",
"color": "Gris",
"doors": 4,
"registration_number": "FGXY21",
"vin": "9BWZZZ377NP004512",
"plate_normalized": "FGXY21",
"listing_type": "Usado",
"record_type": null,
"type": "Car",
"merch_label": null,
"featured_at": null,
"vehicle_type": "auto",
"sale_status": null,
"status": "disponible",
"reserved_at": null,
"reserved_by_name": null,
"sold_at": null,
"location_id": "b1b1b1b1-0000-4000-8000-000000000001",
"estacionamiento": null,
"description": "Mantenimientos al día en concesionario.",
"photos": [
{
"url": "https://images.example.cl/corolla-2022-1.jpg",
"order": 0
}
],
"attributes": {},
"tags": [],
"searchable_text": "toyota corolla 1.8 xei cvt sedán bencina automática gris 2022",
"active": true,
"protected_fields": [],
"deactivated_by": null,
"tenencia": "propio",
"tenencia_source": "default",
"tenencia_declared_by": null,
"tenencia_declared_at": null,
"iva_regime": null,
"keys_count": null,
"municipalidad_comuna_code": null,
"last_service_date": null,
"service_odometer_km": null,
"published_at": null,
"last_seen_at": null,
"created_at": "2026-09-15T18:34:48.575Z",
"updated_at": "2026-09-15T18:34:48.575Z",
"live_sale_note": null,
"active_reservation": null,
"sin_costo": true,
"not_on_lot": false
}
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
POST /vehicles/{id}/fields/release
Release Override local protection on vehicle fields
Drop protection (ADR 0023) on the named canonical fields so the next sync re-follows the source. Releasing an unprotected field is a no-op.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
id | path | uuid | sí |
Cuerpo
| Campo | Tipo | Obligatorio | Restricciones |
|---|---|---|---|
fields | string[] | sí |
curl -X POST https://api.vitrinadev.com/api/v1/vehicles/<id>/fields/release \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"fields": [
"color"
]
}'Ejemplo de respuesta (200)
{
"data": {
"id": "e1e1e1e1-0000-4000-8000-000000000001",
"tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
"origin_integration_id": null,
"source": "manual",
"make": "Toyota",
"model": "Corolla",
"version": "1.8 XEI CVT",
"year": 2022,
"title": null,
"short_title": null,
"price_amount": 13990000,
"price_clp": 13990000,
"watch_enabled": false,
"floor_price_clp": null,
"target_position": null,
"abono_amount_clp": 300000,
"merged_into": null,
"deleted_at": null,
"price_currency": "CLP",
"odometer_value": 42500,
"odometer_unit": "KM",
"fuel_type": "Bencina",
"gear_type": "Automática",
"body_style": "Sedán",
"color": "Gris",
"doors": 4,
"registration_number": "FGXY21",
"vin": "9BWZZZ377NP004512",
"plate_normalized": "FGXY21",
"listing_type": "Usado",
"record_type": null,
"type": "Car",
"merch_label": null,
"featured_at": null,
"vehicle_type": "auto",
"sale_status": null,
"status": "disponible",
"reserved_at": null,
"reserved_by_name": null,
"sold_at": null,
"location_id": "b1b1b1b1-0000-4000-8000-000000000001",
"estacionamiento": null,
"description": "Mantenimientos al día en concesionario.",
"photos": [
{
"url": "https://images.example.cl/corolla-2022-1.jpg",
"order": 0
}
],
"attributes": {},
"tags": [],
"searchable_text": "toyota corolla 1.8 xei cvt sedán bencina automática gris 2022",
"active": true,
"protected_fields": [],
"deactivated_by": null,
"tenencia": "propio",
"tenencia_source": "default",
"tenencia_declared_by": null,
"tenencia_declared_at": null,
"iva_regime": null,
"keys_count": null,
"municipalidad_comuna_code": null,
"last_service_date": null,
"service_odometer_km": null,
"published_at": null,
"last_seen_at": null,
"created_at": "2026-09-15T18:34:48.575Z",
"updated_at": "2026-09-15T18:34:48.575Z",
"live_sale_note": null,
"active_reservation": null,
"sin_costo": true,
"not_on_lot": false
}
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
GET /vehicles/{id}/history
A vehicle's change history (Historial)
Reverse-chronological audit trail for this vehicle — created, field edits (with a before→after diff), publish/unpublish/cierre, and Override local protect/release — with teammate actors resolved to display names. It also carries the deal's milestones, read from each document's own audit rows: nota de compra emitted/voided (purchase_note_issued, purchase_note_voided), nota de venta emitted/approved/voided (sale_note_issued, sale_note_approved, sale_note_voided), contrato de compraventa first printed (compraventa_printed), mandato de consignación first printed (mandato_issued), a signed copy filed (signed_copy_filed, with metadata.signed_document_type) and the liquidación paid to the consignor (liquidacion_paid). metadata.folio names the document when it has one. Each document type appears only for a caller holding its read scope (purchase_notes:read, sale_notes:read, consignments:read; signed copies also need vehicle_registry:read). A download of the Resumen del negocio is deal_settlement_downloaded. The car's papers (ADR 0114 §7 — permiso de circulación, revisión técnica, SOAP, padrón, CAV, any named paper) appear as paper_added (a file filed; metadata.replaced_attachment_id when it replaced one), paper_updated (renamed or its «Vence» day set, metadata.changed) and paper_removed, each with metadata.kind, metadata.label and metadata.expires_on — only for a caller holding vehicle_registry:read. A value changed directly on a connected system (price, km; Veekls also year and version) and taken into Vitrina is portal_edit (system actor, changes, metadata.provider, metadata.portal, metadata.summary); a portal value that was not taken in is portal_edit_ignored (metadata.summary; metadata.ignored: field, portal_value, kept_value, reason = pending_vitrina_edit when it lost to a Vitrina edit still waiting to be pushed, out_of_range when it fell outside the price/km sanity range). The UI merges it with /activity (leads) into one "Leads y cambios" timeline.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
id | path | uuid | sí | |
limit | query | integer | no | ≥ 1, ≤ 100 |
curl https://api.vitrinadev.com/api/v1/vehicles/<id>/history \
-H "Authorization: Bearer $VITRINA_KEY"Ejemplo de respuesta (200)
{
"data": [
{
"id": "99999999-0000-4000-8000-000000000005",
"action": "sale_notes.approve",
"kind": "sale_note_approved",
"actor": {
"kind": "user",
"id": "11111111-0000-4000-8000-000000000001",
"name": "Ana Torres"
},
"changes": [],
"metadata": {
"folio": "V-13",
"vehicle_id": "e1e1e1e1-0000-4000-8000-000000000001"
},
"at": "2026-09-18T15:02:11.000Z"
},
{
"id": "99999999-0000-4000-8000-000000000003",
"action": "vehicles.create",
"kind": "created",
"actor": {
"kind": "user",
"id": "11111111-0000-4000-8000-000000000001",
"name": "Ana Torres"
},
"changes": [],
"metadata": {},
"at": "2026-09-15T18:34:58.390Z"
}
]
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
GET /vehicles/{id}/interest
Who is asking about this car
The interest rollup: distinct contacts, how many became named leads, a per-portal breakdown of where they came from, first and last contact, and days in stock.
It answers "is anyone interested NOW", which is why a sold vehicle rolls up empty (ADR 0030) rather than showing its historical interest — use /activity for the history. No contact ids are echoed, deliberately: the endpoint reports demand without becoming a way to enumerate the contact table. Automotive-vertical only: a workspace on another vertical gets 403 here however its scopes are set.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
id | path | uuid | sí |
curl https://api.vitrinadev.com/api/v1/vehicles/<id>/interest \
-H "Authorization: Bearer $VITRINA_KEY"Ejemplo de respuesta (200)
{
"data": {
"distinct_contacts": 2,
"named_leads": 1,
"by_portal": [
{
"portal": "website",
"contacts": 2
}
],
"first_contact_at": "2026-09-16T12:00:00.000Z",
"last_contact_at": "2026-09-19T15:30:00.000Z",
"anonymous_interest": 1,
"in_stock_since": "2026-09-15T18:34:48.575Z",
"time_in_stock_days": 6,
"is_sold": false,
"lifetime": {
"distinct_contacts": 2,
"named_leads": 1,
"anonymous_interest": 1,
"first_contact_at": "2026-09-16T12:00:00.000Z",
"last_contact_at": "2026-09-19T15:30:00.000Z"
}
}
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
POST /vehicles/{id}/merge
Merge two cars into one
Fusionar: collapse this car and other_vehicle_id into ONE car. The survivor is chosen by the platform, never by the caller: an active car beats an inactive one, then the ERP stock-of-record row beats a portal row, then the car with more activity (leads, alerts, publications), then the older one. Every lead, reserva, documento, publicación, cita and cost line of the loser moves onto the survivor; the loser stops appearing in Stock. It cannot be undone through the API.
Any two live cars of the workspace may be merged; a duplicate candidate is not required. Merging resolves every candidate naming the loser. 404 when either car is unknown, merged or deleted; 409 when both ids are the same car. Answers \{ survivor_id, loser_id \}. Automotive-vertical only: a workspace on another vertical gets 403 here however its scopes are set.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
id | path | uuid | sí |
Cuerpo
| Campo | Tipo | Obligatorio | Restricciones |
|---|---|---|---|
other_vehicle_id | uuid | sí |
curl -X POST https://api.vitrinadev.com/api/v1/vehicles/<id>/merge \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"other_vehicle_id": "88f2fa25-3c71-4e0b-9a4d-6b1e2c7d5f90"
}'Ejemplo de respuesta (200)
{
"data": {
"survivor_id": "3b1f8a52-9d34-4a7e-8f2c-1a2b3c4d5e6f",
"loser_id": "88f2fa25-3c71-4e0b-9a4d-6b1e2c7d5f90"
}
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
PUT /vehicles/{id}/pipeline
Place a vehicle on a reconditioning board, move boards, or remove it
Places the car on a kind=vehicle pipeline, landing on that board's ENTRY column (lowest position, category open) unless stage_id names another column OF THAT SAME BOARD. A car already placed is RE-placed and the change is recorded in stage_transition_log. ⚠ pipeline_id: null TAKES THE CAR OFF ITS BOARD AND DELETES NOTHING — the car, its history and its past transitions all remain; only the placement is cleared. Placement is OPTIONAL by design: most cars are on no board at all, and that is not an error state. A pipeline of another kind is a 400; an unknown or foreign one is a 404.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
id | path | uuid | sí |
Cuerpo
| Campo | Tipo | Obligatorio | Restricciones |
|---|---|---|---|
pipeline_id | string | null | sí | |
stage_id | uuid | — |
curl -X PUT https://api.vitrinadev.com/api/v1/vehicles/<id>/pipeline \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"pipeline_id": "44444444-0000-4000-8000-000000000001"
}'Ejemplo de respuesta (200)
{
"data": {
"id": "e1e1e1e1-0000-4000-8000-000000000001",
"tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
"pipeline_id": "44444444-0000-4000-8000-000000000001",
"stage_id": "55555555-0000-4000-8000-000000000001",
"stage_entered_at": "2026-09-20T01:46:33.764Z",
"status": "disponible",
"stage_blocked_reason": null,
"prep_assignee_user_id": null,
"registration_number": null,
"title": null
},
"meta": {
"outcome": "moved"
}
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
PUT /vehicles/{id}/prep-assignee
Name (or un-name) the person driving a vehicle's preparación
Sets who owns getting this unit out of the column it is on. user_id: null un-assigns. The value is a RAW user id and every read returns it as one — the display name is resolved CLIENT-SIDE against GET /memberships, this platform's convention wherever a person appears on a card, because the column carries no FK (it outlives a membership delete). ⚠ ANY WELL-FORMED uuid IS ACCEPTED AND MEMBERSHIP IS NOT VALIDATED — deliberately identical to lead.owner_user_id and ticket.assignee_user_id, and recorded as a known cross-entity gap rather than a rule invented for one noun. UNLIKE THE BLOCK, THIS SURVIVES A STAGE MOVE: the person driving the unit does not change because it reached the next column. A car that is on NO board is a 409.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
id | path | uuid | sí |
Cuerpo
| Campo | Tipo | Obligatorio | Restricciones |
|---|---|---|---|
user_id | string | null | sí |
curl -X PUT https://api.vitrinadev.com/api/v1/vehicles/<id>/prep-assignee \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"user_id": "11111111-0000-4000-8000-000000000001"
}'Ejemplo de respuesta (200)
{
"data": {
"id": "e1e1e1e1-0000-4000-8000-000000000001",
"tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
"pipeline_id": "44444444-0000-4000-8000-000000000001",
"stage_id": "55555555-0000-4000-8000-000000000002",
"stage_entered_at": "2026-09-20T01:46:34.125Z",
"status": "disponible",
"stage_blocked_reason": "Esperando repuesto de importación",
"prep_assignee_user_id": "11111111-0000-4000-8000-000000000001",
"registration_number": null,
"title": null
},
"meta": {
"outcome": "moved"
}
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
GET /vehicles/{id}/publications
List marketplace publications of a vehicle
Every publication row for this vehicle, one per connected portal it has ever been sent to — including a deleted/error row from a past close/unpublish, since a row here is a tombstone rather than an absence. status is the real PublicationStatus (never a boolean): pending (device-agent transport, facebook_marketplace — enqueued but not yet posted), published/imported/updating/error (live on the portal — error too, since only a later refresh failed), removing (device-agent transport — a removal was enqueued but not yet confirmed, so the ad is presumptively still live, the opposite fact from pending), paused (Cierre de avisos reversible hide, portals that support pausing) or deleted (gone, or a portal-specific terminal state like Chileautos’ Withdrawn). external_id is the portal’s own id for the ad; it is never a key inside Vitrina.
Per-portal push status. After a dealer edit, each row reports the result of its last push: last_push_status is updating (the edit is queued, running or being retried), updated (the portal took it; last_push_at is when and last_push_fields lists what went out) or failed (the final retry failed: last_push_error is the portal’s reason in Spanish, last_push_at is when it gave up). A failed push retries automatically and the row stays failed until a later push succeeds; one portal failing never changes the others. null means never pushed. push_delivery says what updated means: api (the change is on the portal), feed (Yapo: the change is in the feed and shows when Yapo reads it) or extension (Facebook Marketplace: queued for the dealer’s browser extension). The same fields ride on GET /vehicles/publications.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
id | path | uuid | sí |
curl https://api.vitrinadev.com/api/v1/vehicles/<id>/publications \
-H "Authorization: Bearer $VITRINA_KEY"Ejemplo de respuesta (200)
{
"data": [
{
"id": "88888888-0000-4000-8000-000000000001",
"tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
"vehicle_id": "e1e1e1e1-0000-4000-8000-000000000001",
"marketplace_account_id": "33333333-0000-4000-8000-000000000001",
"provider": "mercadolibre",
"external_id": "MLC-1234567890",
"status": "published",
"origin": "vitrina",
"permalink": "https://articulo.mercadolibre.cl/MLC-1234567890",
"last_synced_at": "2026-09-19T10:00:00.000Z",
"last_published_at": "2026-09-16T09:00:00.000Z",
"last_error": null,
"close_retry_count": 0,
"portal_state": "active",
"portal_state_at": "2026-09-19T10:00:00.000Z",
"last_push_status": "updated",
"last_push_at": "2026-09-19T09:58:00.000Z",
"last_push_error": null,
"last_push_fields": [
"priceAmount"
],
"push_delivery": "api",
"raw_provider_payload": null,
"created_at": "2026-09-16T09:00:00.000Z",
"updated_at": "2026-09-19T10:00:00.000Z"
}
]
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
DELETE /vehicles/{id}/publications/{pubId}
Unpublish a vehicle from a marketplace
Removes ONE publication (contrast POST /vehicles/\{id\}/close-ads, which closes every portal at once and is always enqueued). Returns the publication row with status, the real PublicationStatus — whatever a portal calls its own removed state (Chileautos has no hard delete and writes Withdrawn on its side) still lands here as deleted. The row itself is kept as a tombstone, never removed from this table, so "where did publication pubId go?" always has an answer.
⚠ Facebook Marketplace has no HTTP API, so this call enqueues a device_task for the extension exactly as publish does, and answers honestly: status: "removing", never a fabricated deleted. The ad is presumptively still live on Facebook until the extension confirms the removal — poll GET /vehicles/\{id\}/publications for the row to turn deleted (or error, if the removal failed and the ad is still there), or subscribe to vehicle.unpublished / vehicle.publish_failed, fired only on that confirmation, never at enqueue. A chileautos_panel publication behaves the same way: the removal runs on Vitrina’s scraper workers, so the call answers removing and the row turns deleted (or error) when it completes.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
id | path | uuid | sí | |
pubId | path | uuid | sí |
curl -X DELETE https://api.vitrinadev.com/api/v1/vehicles/<id>/publications/<id> \
-H "Authorization: Bearer $VITRINA_KEY"Ejemplo de respuesta (200)
{
"data": {
"id": "88888888-0000-4000-8000-000000000001",
"tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
"vehicle_id": "e1e1e1e1-0000-4000-8000-000000000001",
"marketplace_account_id": "33333333-0000-4000-8000-000000000001",
"provider": "mercadolibre",
"external_id": "MLC-1234567890",
"status": "deleted",
"origin": "vitrina",
"permalink": null,
"last_synced_at": "2026-09-19T10:00:00.000Z",
"last_published_at": "2026-09-16T09:00:00.000Z",
"last_error": null,
"close_retry_count": 0,
"portal_state": "active",
"portal_state_at": "2026-09-19T10:00:00.000Z",
"last_push_status": "updated",
"last_push_at": "2026-09-19T09:58:00.000Z",
"last_push_error": null,
"last_push_fields": [
"priceAmount"
],
"push_delivery": "api",
"raw_provider_payload": null,
"created_at": "2026-09-16T09:00:00.000Z",
"updated_at": "2026-09-19T10:00:00.000Z"
}
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
POST /vehicles/{id}/publish
Publish a vehicle to one or more connected marketplaces
One round trip per portal named in integration_ids, and the response is a per-integration outcome list — a 200 does not mean every portal accepted the ad. status is published (Vitrina pushed or updated the ad), already_published (the double-publish guard found a live ad on this (vehicle, integration) that Vitrina did not itself push — nothing was sent, nothing was duplicated) or error (error names why).
Facebook Marketplace does not publish. Vitrina only answers its buyers; naming a facebook_marketplace integration here answers error, and facebook is not a publish_to slug.
Chileautos (Panel) (chileautos_panel) is asynchronous. It is driven through a browser session on the dealer’s control panel, which only Vitrina’s scraper workers can run, so this call answers status: "pending" the instant the job is queued (the publication row reads pending, or updating when the ad already exists). Same polling and webhooks as above; the row turns published or error (with last_error), usually within a few minutes.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
id | path | uuid | sí |
Cuerpo
| Campo | Tipo | Obligatorio | Restricciones |
|---|---|---|---|
integration_ids | uuid[] | sí |
curl -X POST https://api.vitrinadev.com/api/v1/vehicles/<id>/publish \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"integration_ids": [
"33333333-0000-4000-8000-000000000001"
]
}'Ejemplo de respuesta (200)
{
"data": [
{
"integration_id": "33333333-0000-4000-8000-000000000001",
"provider": "mercadolibre",
"status": "published",
"external_id": "MLC-1234567890",
"publication": {
"id": "88888888-0000-4000-8000-000000000001",
"tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
"vehicle_id": "e1e1e1e1-0000-4000-8000-000000000001",
"marketplace_account_id": "33333333-0000-4000-8000-000000000001",
"provider": "mercadolibre",
"external_id": "MLC-1234567890",
"status": "published",
"origin": "vitrina",
"permalink": "https://articulo.mercadolibre.cl/MLC-1234567890",
"last_synced_at": "2026-09-19T10:00:00.000Z",
"last_published_at": "2026-09-16T09:00:00.000Z",
"last_error": null,
"close_retry_count": 0,
"portal_state": "active",
"portal_state_at": "2026-09-19T10:00:00.000Z",
"last_push_status": "updated",
"last_push_at": "2026-09-19T09:58:00.000Z",
"last_push_error": null,
"last_push_fields": [
"priceAmount"
],
"push_delivery": "api",
"raw_provider_payload": null,
"created_at": "2026-09-16T09:00:00.000Z",
"updated_at": "2026-09-19T10:00:00.000Z"
}
},
{
"integration_id": "33333333-0000-4000-8000-000000000002",
"provider": "chileautos_panel",
"status": "pending",
"external_id": null,
"publication": {
"id": "88888888-0000-4000-8000-000000000002",
"tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
"vehicle_id": "e1e1e1e1-0000-4000-8000-000000000001",
"marketplace_account_id": "33333333-0000-4000-8000-000000000002",
"provider": "chileautos_panel",
"external_id": null,
"status": "pending",
"origin": "vitrina",
"permalink": null,
"last_synced_at": "2026-09-19T10:00:00.000Z",
"last_published_at": "2026-09-16T09:00:00.000Z",
"last_error": null,
"close_retry_count": 0,
"portal_state": null,
"portal_state_at": null,
"last_push_status": "updated",
"last_push_at": "2026-09-19T09:58:00.000Z",
"last_push_error": null,
"last_push_fields": [
"priceAmount"
],
"push_delivery": "api",
"raw_provider_payload": null,
"created_at": "2026-09-16T09:00:00.000Z",
"updated_at": "2026-09-19T10:00:00.000Z"
}
}
]
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
PUT /vehicles/{id}/stage
Move a vehicle to another column of its board
The card move. One transaction: the row is locked, the target is checked against the current column's transition whitelist (allowed_transitions_to; empty = unrestricted), the move lands in stage_transition_log with the actor, and stage_entered_at is re-stamped — except on a no-op re-drop onto the same column, which deliberately does NOT reset the ageing clock. ⚠ IT NEVER WRITES status, reserved_at OR sold_at: the operational stage and the commercial status (ADR 0030) are orthogonal, so a car in Mecánica may be reservado and a car on the terminal column may already be vendido. A car on NO board is a 409 ("place it first") and never an implicit placement; a stage belonging to another board is a 400.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
id | path | uuid | sí |
Cuerpo
| Campo | Tipo | Obligatorio | Restricciones |
|---|---|---|---|
stage_id | uuid | sí | |
reason | string | null | — | máx. 500 |
curl -X PUT https://api.vitrinadev.com/api/v1/vehicles/<id>/stage \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"stage_id": "55555555-0000-4000-8000-000000000002",
"reason": "Perito revisó el auto"
}'Ejemplo de respuesta (200)
{
"data": {
"id": "e1e1e1e1-0000-4000-8000-000000000001",
"tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
"pipeline_id": "44444444-0000-4000-8000-000000000001",
"stage_id": "55555555-0000-4000-8000-000000000002",
"stage_entered_at": "2026-09-20T01:46:34.125Z",
"status": "disponible",
"stage_blocked_reason": null,
"prep_assignee_user_id": null,
"registration_number": null,
"title": null
},
"meta": {
"outcome": "moved"
}
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
PUT /vehicles/{id}/stage-block
Record (or clear) why a vehicle is stuck in its current column
Sets the short label a board card shows when a car cannot progress ("esperando repuesto de Japón"). reason: null CLEARS it. ⚠ THE BLOCK IS SCOPED TO THE CURRENT COLUMN AND A STAGE MOVE CLEARS IT: it explains why the car is stuck HERE, so carrying it into the next column would claim the car is still waiting on a part it already received. A no-op re-drop onto the same column writes nothing and therefore clears nothing — this endpoint with a null reason is the verb for un-blocking. Capped at 120 characters because it renders ON A CARD; the move reason (500) is the place for a sentence, and it lands in stage_transition_log forever. Blank text is a 400. A car that is on NO board is a 409 — a block names the column the car is stuck in, and there is none. ⚠ IT NEVER WRITES status, reserved_at OR sold_at: a blocked car is not thereby unavailable (ADR 0030), and the body is .strict().
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
id | path | uuid | sí |
Cuerpo
| Campo | Tipo | Obligatorio | Restricciones |
|---|---|---|---|
reason | string | null | sí | máx. 120 |
curl -X PUT https://api.vitrinadev.com/api/v1/vehicles/<id>/stage-block \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"reason": "Esperando repuesto de importación"
}'Ejemplo de respuesta (200)
{
"data": {
"id": "e1e1e1e1-0000-4000-8000-000000000001",
"tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
"pipeline_id": "44444444-0000-4000-8000-000000000001",
"stage_id": "55555555-0000-4000-8000-000000000002",
"stage_entered_at": "2026-09-20T01:46:34.125Z",
"status": "disponible",
"stage_blocked_reason": "Esperando repuesto de importación",
"prep_assignee_user_id": null,
"registration_number": null,
"title": null
},
"meta": {
"outcome": "moved"
}
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
GET /vehicles/{id}/suggestions
Paperwork this hand-moved unit is still missing
PENDING suggestions only. A dealer who marks a car vendido or reservado directly — the quick status control on the Stock card — gets the state change immediately and unconditionally; what they also get is a nudge toward the document that normally moves it (crear_nota_venta / crear_reserva). ⚠ A SUGGESTION IS NEVER A PENDING WRITE: nothing here has touched the vehicle, the sale note or the reserva, and ignoring it forever is a supported outcome. The nudge is only raised when the document is GENUINELY ABSENT, so a vendido caused by approving a nota de venta — and a reservado caused by taking a reserva — produce nothing here. It disappears from this list when a person dismisses it or when the document it asked for is created; there is deliberately no create verb.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
id | path | uuid | sí |
curl https://api.vitrinadev.com/api/v1/vehicles/<id>/suggestions \
-H "Authorization: Bearer $VITRINA_KEY"Ejemplo de respuesta (200)
{
"data": [
{
"id": "77777777-0000-4000-8000-000000000001",
"tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
"vehicle_id": "e1e1e1e1-0000-4000-8000-000000000001",
"kind": "crear_nota_venta",
"created_at": "2026-09-19T16:00:00.000Z",
"dismissed_at": null,
"dismissed_by": null,
"resolved_at": null
}
]
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
POST /vehicles/{id}/suggestions/{suggestionId}/dismiss
Dismiss the paperwork nudge
"No, this one really is just a status change." Stamps the suggestion with the acting principal — a dismissal nobody signed is not a decision, it is a task that quietly stopped being displayed — and removes it from the pending list for good. Compare-and-swap: a second dismissal, or dismissing one the system already resolved because the document now exists, is a 404. The \{suggestionId\} must belong to \{id\}; one that belongs to another car of the same workspace is a 404 rather than a silent success. ⚠ NEVER TOUCHES THE VEHICLE: dismissing the nudge to write a nota de venta does not un-sell the car.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
id | path | uuid | sí | |
suggestionId | path | uuid | sí |
curl -X POST https://api.vitrinadev.com/api/v1/vehicles/<id>/suggestions/<id>/dismiss \
-H "Authorization: Bearer $VITRINA_KEY"Ejemplo de respuesta (200)
{
"data": {
"ok": true
}
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
GET /vehicles/catalog/brands
List car makes (catalog)
Every car make Vitrina recognises, one entry per make in its canonical spelling (the one Chileautos uses: "Mercedes-Benz", "SsangYong", "Citroën"). Only cars, SUVs, pickups and light commercial vehicles: no motorcycles, boats, machinery or accessories. Sources: the Chilean homologación catalog and the labels Chileautos publishes. Use it to normalize a vehicle's make / model / version BEFORE you create or update it: send the value exactly as this list spells it and Vitrina (and the portals it publishes to, Chileautos included) classifies the car. Matching is case-, accent-, space- and hyphen-insensitive ("mercedes benz" = "Mercedes-Benz", "rav 4" = "RAV4"). Global reference data (the same for every workspace), cached for an hour. Readable with marketplace:read or stock:read.
curl https://api.vitrinadev.com/api/v1/vehicles/catalog/brands \
-H "Authorization: Bearer $VITRINA_KEY"Ejemplo de respuesta (200)
{
"data": [
"Chevrolet",
"Mercedes-Benz",
"SsangYong",
"Toyota"
]
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
GET /vehicles/catalog/models
List the models of a make (catalog)
The models of brand (any spelling of the make), one entry per model: spelling variants are collapsed ("4 Runner" / "4runner" → "4Runner") and no entry carries a version, engine or body description. Unknown make → empty list. Use it to normalize a vehicle's make / model / version BEFORE you create or update it: send the value exactly as this list spells it and Vitrina (and the portals it publishes to, Chileautos included) classifies the car. Matching is case-, accent-, space- and hyphen-insensitive ("mercedes benz" = "Mercedes-Benz", "rav 4" = "RAV4"). Global reference data (the same for every workspace), cached for an hour. Readable with marketplace:read or stock:read.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
brand | query | string | sí | mín. 1, máx. 120 |
curl https://api.vitrinadev.com/api/v1/vehicles/catalog/models \
-H "Authorization: Bearer $VITRINA_KEY"Ejemplo de respuesta (200)
{
"data": [
"4Runner",
"Corolla",
"Corolla Cross",
"Hilux",
"RAV4",
"Yaris"
]
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
GET /vehicles/catalog/versions
List the versions of a make + model (catalog)
Known versions (trims) of brand + model, in trim style: displacement, trim level, drivetrain and gearbox ("2.0 LE 4X2 MT", "2.5 HEV AWD"), deduplicated. A version is matched approximately (word overlap, same displacement), so it is a guide rather than a closed list: a version that is not here is still accepted. Use it to normalize a vehicle's make / model / version BEFORE you create or update it: send the value exactly as this list spells it and Vitrina (and the portals it publishes to, Chileautos included) classifies the car. Matching is case-, accent-, space- and hyphen-insensitive ("mercedes benz" = "Mercedes-Benz", "rav 4" = "RAV4"). Global reference data (the same for every workspace), cached for an hour. Readable with marketplace:read or stock:read.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
brand | query | string | sí | mín. 1, máx. 120 |
model | query | string | sí | mín. 1, máx. 120 |
curl https://api.vitrinadev.com/api/v1/vehicles/catalog/versions \
-H "Authorization: Bearer $VITRINA_KEY"Ejemplo de respuesta (200)
{
"data": [
"2.0 4X2 CVT",
"2.0 LE 4X2 MT",
"2.5 AWD HEV",
"2.5 XLE 4X4 AT"
]
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
POST /vehicles/close-ads-bulk
Close N vehicles' ads on every connected marketplace
Bulk cierre de avisos (ADR 0024) — the Stock bulk-bar "despublicar". ASYNC: enqueues one retry-backed cierre job per eligible vehicle and returns { enqueued, skipped } (skip reasons: not_found, no_publications), never the portals’ answers. intent defaults to "deactivated" (reversible hide: pause where the portal supports it, remove where it doesn’t); "sold" is the terminal close. Does NOT mark the vehicles Sold.
Cuerpo
| Campo | Tipo | Obligatorio | Restricciones |
|---|---|---|---|
vehicle_ids | uuid[] | sí | |
intent | sold \ | deactivated | — |
curl -X POST https://api.vitrinadev.com/api/v1/vehicles/close-ads-bulk \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"vehicle_ids": [
"e1e1e1e1-0000-4000-8000-000000000001"
],
"intent": "deactivated"
}'Ejemplo de respuesta (200)
{
"data": {
"enqueued": [
"e1e1e1e1-0000-4000-8000-000000000001"
],
"skipped": []
}
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
POST /vehicles/dedup-backfill/execute
Apply the reviewed dedup merges
Merges duplicate vehicles through the standard merge machinery.
approve is an APPROVAL FILTER, not a merge command: execute re-runs the scan and applies only pairs the fresh scan still proposes on its own, so a stale pair from an old report is skipped rather than forced through.
Omitting approve and sending approve: [] are different. Omit it to apply every currently-proposed merge; send an empty array to approve nothing and merge zero. The dangerous mistake is sending [] meaning "no filter". Up to 1000 pairs. Scope: stock_dedup:write. Automotive-vertical only: a workspace on another vertical gets 403 here however its scopes are set.
Cuerpo
| Campo | Tipo | Obligatorio | Restricciones |
|---|---|---|---|
approve | object[] | — |
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": "e1e1e1e1-0000-4000-8000-000000000001",
"loser_id": "e1e1e1e1-0000-4000-8000-000000000002"
}
]
}'Ejemplo de respuesta (200)
{
"data": {
"tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
"merged": 1,
"skipped": []
}
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
POST /vehicles/dedup-backfill/report
Preview the existing-stock dedup
Dry run — modifies NOTHING. Returns the proposed merges (Fusiones), the pairs it will not decide (Conflictos), and a note on photo-net coverage. Takes no body.
This is the review artefact a human reads before approving: the execute endpoint takes its approve list in exactly the shape these proposals come back in. Scope: stock_dedup:read. Automotive-vertical only: a workspace on another vertical gets 403 here however its scopes are set.
curl -X POST https://api.vitrinadev.com/api/v1/vehicles/dedup-backfill/report \
-H "Authorization: Bearer $VITRINA_KEY"Ejemplo de respuesta (200)
{
"data": {
"tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
"scanned_vehicles": 7,
"proposals": [
{
"survivor_id": "e1e1e1e1-0000-4000-8000-000000000001",
"loser_id": "e1e1e1e1-0000-4000-8000-000000000002",
"reason": "same_vin"
}
],
"conflicts": [],
"photo_reconciliation": {
"included": true,
"deferred": false,
"engine_available": true,
"reason": "Same-photo duplicates were previewed inline because the pHash engine is present in this context.",
"proposals": []
},
"summary": {
"proposed_merges": 1,
"clusters": 1,
"conflicts": 0
}
}
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
GET /vehicles/document-discrepancies
The workspace queue of document-vs-stock disagreements
Every disagreement between an official document and this workspace’s stock still waiting on a person, newest first. standing defaults to open — a queue is what is OUTSTANDING.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
standing | query | open \ | resolved | no |
limit | query | integer | no | ≥ 1, ≤ 500 |
curl https://api.vitrinadev.com/api/v1/vehicles/document-discrepancies \
-H "Authorization: Bearer $VITRINA_KEY"Ejemplo de respuesta (200)
{
"data": [
{
"id": "66666666-0000-4000-8000-000000000001",
"tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
"attachment_id": "f1f1f1f1-0000-4000-8000-000000000001",
"vehicle_id": "e1e1e1e1-0000-4000-8000-000000000001",
"attachment_kind": "padron",
"field": "registration_number",
"document_value": "FGXY22",
"observed_stock_value": "FGXY21",
"note": "El padrón trae una letra distinta a la patente registrada.",
"observed_at": "2026-09-18T14:00:00.000Z",
"observed_by": "11111111-0000-4000-8000-000000000001",
"resolution": null,
"resolution_reason": null,
"resolved_on": null,
"resolved_at": null,
"resolved_by": null,
"applied_prior_value": null,
"created_at": "2026-09-18T14:00:00.000Z",
"updated_at": "2026-09-18T14:00:00.000Z"
}
]
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
POST /vehicles/duplicate-candidates/{candidateId}/dismiss
Dismiss a possible duplicate
«No es el mismo auto»: the two cars of the candidate are different cars. The pair leaves GET /vehicles/\{id\}/duplicate-candidates and the Stock list’s possible_duplicate flag, and the reconciliation never flags it again. Idempotent. 404 when the candidate is not in this workspace. Answers \{ id \}. Automotive-vertical only: a workspace on another vertical gets 403 here however its scopes are set.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
candidateId | path | uuid | sí |
curl -X POST https://api.vitrinadev.com/api/v1/vehicles/duplicate-candidates/<id>/dismiss \
-H "Authorization: Bearer $VITRINA_KEY"Ejemplo de respuesta (200)
{
"data": {
"id": "0b7c4e2a-5d1f-4a8e-9c3b-2f6d8e1a4b70"
}
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
PATCH /vehicles/estacionamientos
Bulk-set vehicles' Estacionamiento (patio position)
The Sucursal tab-through save (ADR 0060 bulk path). ALL-OR-NOTHING: every vehicle_id is validated against this tenant BEFORE anything is written, and the whole batch lands in ONE transaction — unlike publish-bulk/close-ads-bulk above, there is no per-row partial success. A blank/empty estacionamiento clears the position (stores null). Never checks for duplicate positions across rows — sharing a spot is a legal double-park.
Cuerpo
| Campo | Tipo | Obligatorio | Restricciones |
|---|---|---|---|
assignments | object[] | sí |
curl -X PATCH https://api.vitrinadev.com/api/v1/vehicles/estacionamientos \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"assignments": [
{
"vehicle_id": "e1e1e1e1-0000-4000-8000-000000000001",
"estacionamiento": "A-12"
}
]
}'Ejemplo de respuesta (200)
{
"data": {
"updated": 1
}
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
GET /vehicles/export
Export the Stock list as a PDF hoja de patio or an Excel table
Exports exactly what the Stock screen shows, under the SAME filters and the same marketplace:read scope as GET /vehicles — no new access path. format picks the artifact: pdf is the printed walking sheet (grouped by Sucursal, ordered by sucursal + estacionamiento so it follows the patio on foot), xlsx is the full table. The two differ in ONE default: include_inactive defaults to true for pdf (an inactivo is not published but is still parked in the patio, so a walking sheet must list it) and false for xlsx. Passing ids scopes the export to that selection and makes include_inactive moot — a selected vehicle is always exported. Internal prices (piso / mínimo del dueño / target) are opt-in via include_internal_prices and NEVER appear on the pdf. ⚠ SINCE ERP S3-6 THE OPT-IN IS NECESSARY BUT NOT SUFFICIENT: piso and mínimo del dueño are per-unit money figures that disclose the dealership cost position, so the whole internal-price block is withheld unless the recipient of the file also holds dealership_economics:read. The columns are ABSENT from the sheet, not blank. Above 2000 matching rows (STOCK_EXPORT_ROW_CAP in stock-export.service.ts — not imported here, since this file deliberately depends on schemas only and that service pulls in the pdf/xlsx renderers) the export is refused with 413 rather than truncated silently. tenencia / consignacion_modalidad (Tenencia, ADR 0025) narrow the sheet exactly as they narrow the screen, and carry the same 400 when the modalidad arrives without tenencia=consignacion.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
format | query | xlsx \ | pdf | no |
q | query | string | no | |
view | query | published \ | unpublished \ | error \ |
location_id | query | uuid | no | |
status | query | disponible \ | reservado \ | vendido |
vehicle_type | query | auto \ | camion \ | maquinaria \ |
has_error | query | boolean | null | no | |
needs_review | query | boolean | null | no | |
sin_estacionamiento | query | boolean | null | no | |
sin_sucursal | query | boolean | null | no | |
papers_expired | query | any | no | |
papers_expiring | query | any | no | |
tenencia | query | propio \ | consignacion | no |
consignacion_modalidad | query | en_local \ | virtual \ | sin_contrato |
tenencia_source | query | document \ | declared \ | default \ |
sort | query | price_asc \ | price_desc \ | year_desc \ |
ids | query | string | no | mín. 1 |
include_inactive | query | boolean | null | no | |
include_internal_prices | query | boolean | null | no |
curl https://api.vitrinadev.com/api/v1/vehicles/export \
-H "Authorization: Bearer $VITRINA_KEY"Responde: 200 · 400 · 401 · 403 · 404 · 409 · 413 · 429
GET /vehicles/field-visibility
Where each vehicle field publishes (the editor’s marker source)
Every vehicle column — plus the allow-listed attributes.* keys the editor renders as their own controls — classified against the three outbound audiences the backend enforces: public_listing (the dealer website, the ficha técnica, the widget card), portal_publish (Mercado Libre / Chileautos / Yapo) and agent_context (the AI agent). Each row carries the raw audiences list AND a server-derived visibility rollup — public | portal_only | internal — so a client never re-implements the decision. THREE states, not two: registration_number is withheld from the public listing and REQUIRED by the portals, so a binary public/internal marker would have to lie about it.
audiences[] echoes how each audience reaches THIS tenant. public_listing.detail is sitio_publicado | sitio_no_publicado | sin_complemento; note reaches stays true in all three, because that audience still feeds the ficha técnica and the widget card when the dealer website is unpublished or the dealer_site add-on is absent — only the copy softens.
READ-ONLY. Which fields publish is a compiled registry, not tenant data: there is no write counterpart, and a per-tenant override is the rejected alternative in ADR 0068. The editable per-field control is the ADR-0023 candado (/vehicles/\{id\}/fields/protect|release), which answers "who may WRITE it" rather than "who may SEE it".
curl https://api.vitrinadev.com/api/v1/vehicles/field-visibility \
-H "Authorization: Bearer $VITRINA_KEY"Ejemplo de respuesta (200)
{
"data": {
"audiences": [
{
"key": "public_listing",
"label": "Sitio web y ficha técnica",
"reaches": true,
"detail": "sitio_publicado"
},
{
"key": "portal_publish",
"label": "Portales",
"reaches": true,
"detail": null
},
{
"key": "agent_context",
"label": "Agente de IA",
"reaches": true,
"detail": null
}
],
"fields": [
{
"field": "make",
"audiences": [
"public_listing",
"portal_publish",
"agent_context"
],
"visibility": "public"
},
{
"field": "registration_number",
"audiences": [
"portal_publish"
],
"visibility": "portal_only"
},
{
"field": "tenant_id",
"audiences": [],
"visibility": "internal"
}
]
},
"meta": {
"total": 78
}
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
GET /vehicles/kanban
The whole reconditioning board for one vehicle pipeline
Every column in position order, each with the cars on it. A card is a MINIMAL projection (make/model/version/year, título, patente, one photo, photos_count, the ADR-0030 commercial status, and stage_entered_at) and deliberately carries NO pricing and no cost. Each column carries sla_days — the per-stage ageing threshold in days, null for no limit — which combined with stage_entered_at is what makes a card "atrasado"; the ageing itself is derived, never stored. Whole-board fetch capped at 500 cars total, and meta.capped says whether the cap was hit. A pipeline whose kind is not vehicle is a 400.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
pipeline_id | query | uuid | sí |
curl https://api.vitrinadev.com/api/v1/vehicles/kanban \
-H "Authorization: Bearer $VITRINA_KEY"Ejemplo de respuesta (200)
{
"data": {
"stages": [
{
"id": "55555555-0000-4000-8000-000000000001",
"name": "Recepción",
"slug": "reception",
"position": 0,
"category": "open",
"is_terminal": false,
"description": "El vehículo ya ingresó físicamente.",
"sla_days": 1,
"vehicles": []
},
{
"id": "55555555-0000-4000-8000-000000000002",
"name": "Peritaje",
"slug": "appraisal",
"position": 1,
"category": "open",
"is_terminal": false,
"description": "Revisión mecánica y estructural.",
"sla_days": 2,
"vehicles": [
{
"id": "e1e1e1e1-0000-4000-8000-000000000001",
"make": "Chevrolet",
"model": "Sail",
"version": null,
"year": 2020,
"title": null,
"registration_number": null,
"photo_url": null,
"photos_count": 0,
"status": "disponible",
"stage_entered_at": "2026-09-20T01:46:34.125Z"
}
]
}
]
},
"meta": {
"total": 1,
"limit": 500,
"capped": false
}
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
POST /vehicles/parity-report
Compare native stock against the live source
Read-only certification: compares the native vehicle table with the LIVE Veekls API for one integration — counts, matched and missing vehicles, and field-level diffs on price, year, odometer, photo count and plate.
This is the evidence step in the decommission sequence: mirror photos → parity → repoint consumers → disable sync → retire the source → cancel the vendor contract. Modifies nothing, and takes the read scope (marketplace:read) accordingly. Automotive-vertical only: a workspace on another vertical gets 403 here however its scopes are set.
Cuerpo
| Campo | Tipo | Obligatorio | Restricciones |
|---|---|---|---|
integration_id | uuid | sí |
curl -X POST https://api.vitrinadev.com/api/v1/vehicles/parity-report \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"integration_id": "33333333-0000-4000-8000-000000000001"
}'Ejemplo de respuesta (200)
{
"data": {
"tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
"integration_id": "33333333-0000-4000-8000-000000000001",
"generated_at": "2026-09-20T09:00:00.000Z",
"summary": {
"native_total": 40,
"native_active": 38,
"veekls_total": 41,
"matched": 37,
"native_only": 1,
"veekls_only": 4,
"vehicles_with_diffs": 2
},
"native_only_items": [
{
"vehicle_id": "e1e1e1e1-0000-4000-8000-000000000001",
"external_id": "VK-10021",
"plate_normalized": "FGXY21",
"active": true,
"deactivated_by": null
}
],
"native_only_truncated": false,
"veekls_only_items": [
{
"external_id": "VK-10099",
"plate": "HJKL34",
"price": 8990000,
"year": 2019,
"odometer": 60000
}
],
"veekls_only_truncated": false,
"diffs": [
{
"vehicle_id": "e1e1e1e1-0000-4000-8000-000000000002",
"external_id": "VK-10022",
"plate_normalized": "MNPQ56",
"diffs": [
{
"field": "price",
"native_value": 13990000,
"veekls_value": 13490000
}
],
"protected_diffs": []
}
]
}
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
GET /vehicles/photo-labels
What each stock photo shows (exterior, interior, motor…)
Labels for the photos of one vehicle (vehicle_id) and/or explicit urls (repeat the param, up to 40). A url with no row has not been labelled yet. source is ai (the vision model) or manual (a person corrected it; the AI never overwrites it). status: failed = the last vision call failed and the label is unknown.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
vehicle_id | query | uuid | no | |
url | query | any | no |
curl https://api.vitrinadev.com/api/v1/vehicles/photo-labels \
-H "Authorization: Bearer $VITRINA_KEY"Ejemplo de respuesta (200)
{
"labels": [
{
"url": "https://media.vitrinadev.com/vehicles/00000000-0000-4000-8000-000000000001/8a76.jpg",
"label": "Interior — tablero y volante",
"view": "interior",
"source": "ai",
"status": "labelled"
}
]
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
PUT /vehicles/photo-labels
Correct what a stock photo shows
Sets the view (and an optional short description) of one photo as a person's correction (source: manual). The AI agent uses it at once (get_vehicle shows, send_attachment view check) and no later AI pass overwrites it. The url must be one of this workspace's uploads, or a photo of vehicle_id.
Cuerpo
| Campo | Tipo | Obligatorio | Restricciones |
|---|---|---|---|
url | string | sí | mín. 1, máx. 2048 |
view | exterior \ | interior \ | motor \ |
description | string | null | — | máx. 80 |
vehicle_id | uuid | — |
curl -X PUT https://api.vitrinadev.com/api/v1/vehicles/photo-labels \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://media.vitrinadev.com/vehicles/00000000-0000-4000-8000-000000000001/8a76.jpg",
"view": "interior",
"description": "tablero y volante",
"vehicle_id": "9f4a2b7c-1111-4a2b-8c3d-000000000001"
}'Ejemplo de respuesta (200)
{
"url": "https://media.vitrinadev.com/vehicles/00000000-0000-4000-8000-000000000001/8a76.jpg",
"label": "Interior — tablero y volante",
"view": "interior",
"source": "manual",
"status": "labelled"
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
POST /vehicles/photo-labels/label
Label new uploads now
Queues the vision model for up to 30 of this workspace's uploaded photo urls that have no label yet (2–3 s each, 5 in parallel). Poll GET /vehicles/photo-labels?url=… for the result. Urls that are not this workspace's uploads are ignored.
Cuerpo
| Campo | Tipo | Obligatorio | Restricciones |
|---|---|---|---|
urls | string[] | sí |
curl -X POST https://api.vitrinadev.com/api/v1/vehicles/photo-labels/label \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"urls": [
"https://media.vitrinadev.com/vehicles/00000000-0000-4000-8000-000000000001/8a76.jpg"
]
}'Ejemplo de respuesta (202)
{
"queued": 1
}Responde: 202 · 400 · 401 · 403 · 404 · 409 · 429
POST /vehicles/photo-mirror/execute
Mirror a batch of photos into Vitrina storage
Copies up to limit vehicles’ photos (default 25, hard cap 200 per run). Deliberately bounded so a large lot is worked in chunks rather than one long request — call it repeatedly until the report comes back clean. Scope: stock_photo_mirror:write. Automotive-vertical only: a workspace on another vertical gets 403 here however its scopes are set.
Cuerpo
| Campo | Tipo | Obligatorio | Restricciones |
|---|---|---|---|
limit | integer | — | ≥ 1, ≤ 200 |
curl -X POST https://api.vitrinadev.com/api/v1/vehicles/photo-mirror/execute \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"limit": 25
}'Ejemplo de respuesta (200)
{
"data": {
"tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
"mirrored_vehicles": 2,
"mirrored_photos": 5,
"failures": []
}
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
POST /vehicles/photo-mirror/report
Preview the photo-mirror backfill
Dry run — modifies NOTHING. Counts the vehicles and photos still hotlinking a source CDN, grouped by origin integration.
Mirroring photos into Vitrina storage is the first precondition for retiring a source (ADR 0031): stock whose images still live on the source’s CDN breaks the moment that source is switched off. Takes no body. Scope: stock_photo_mirror:read. Automotive-vertical only: a workspace on another vertical gets 403 here however its scopes are set.
curl -X POST https://api.vitrinadev.com/api/v1/vehicles/photo-mirror/report \
-H "Authorization: Bearer $VITRINA_KEY"Ejemplo de respuesta (200)
{
"data": {
"tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
"scanned_vehicles": 7,
"vehicles_pending": 2,
"photos_pending": 5,
"by_integration": [
{
"origin_integration_id": null,
"source": "manual",
"vehicles": 2,
"photos": 5
}
]
}
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
POST /vehicles/photo-mirror/sweep
Mirror every remaining hotlink in one call
The unbounded sibling of /execute: no limit, and the scope is every ACTIVE vehicle in the workspace, so it is the call that drives the lot to zero rather than chipping at it. A large lot therefore makes this a LONG request — /execute is still the right tool when you need bounded runs.
Not necessarily complete in one pass: failures come back as an actionable per-vehicle/per-url list and all_mirrored says whether anything is left. Re-run until all_mirrored is true — repeating it is safe, because a photo already mirrored is reused rather than fetched again.
Takes an EMPTY body, strictly: the sweep is tenant-wide so there is nothing to parametrise, and an unknown field is a 400 rather than a silently ignored bound. Scope: stock_photo_mirror:write. Automotive-vertical only: a workspace on another vertical gets 403 here however its scopes are set.
Cuerpo
| Campo | Tipo | Obligatorio | Restricciones |
|---|
curl -X POST https://api.vitrinadev.com/api/v1/vehicles/photo-mirror/sweep \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{}'Ejemplo de respuesta (200)
{
"data": {
"tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
"mirrored_vehicles": 2,
"mirrored_photos": 5,
"failures": [],
"all_mirrored": true
}
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
GET /vehicles/publications
Publications for many vehicles at once
The batched read behind the inventory list: pass ids and get every vehicle’s publications in one call instead of N. Capped at 100 ids per request — page the list and batch per page.
Note deleted publications are tombstones rather than absences, so a vehicle that was unpublished still has rows here; read each row’s status rather than treating presence as "published". Each row also carries the per-portal push status of the last edit: last_push_status (updating | updated | failed | null), last_push_at, last_push_error (the portal’s reason when failed), last_push_fields and push_delivery (api | feed | extension; Yapo is feed: updated means the change is in the feed it reads). See GET /vehicles/\{id\}/publications. Automotive-vertical only: a workspace on another vertical gets 403 here however its scopes are set.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
ids | query | string | sí | mín. 1 |
curl https://api.vitrinadev.com/api/v1/vehicles/publications \
-H "Authorization: Bearer $VITRINA_KEY"Ejemplo de respuesta (200)
{
"data": {
"e1e1e1e1-0000-4000-8000-000000000001": [
{
"id": "88888888-0000-4000-8000-000000000001",
"tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
"vehicle_id": "e1e1e1e1-0000-4000-8000-000000000001",
"marketplace_account_id": "33333333-0000-4000-8000-000000000001",
"provider": "mercadolibre",
"external_id": "MLC-1234567890",
"status": "published",
"origin": "vitrina",
"permalink": "https://articulo.mercadolibre.cl/MLC-1234567890",
"last_synced_at": "2026-09-19T10:00:00.000Z",
"last_published_at": "2026-09-16T09:00:00.000Z",
"last_error": null,
"close_retry_count": 0,
"portal_state": "active",
"portal_state_at": "2026-09-19T10:00:00.000Z",
"last_push_status": "updated",
"last_push_at": "2026-09-19T09:58:00.000Z",
"last_push_error": null,
"last_push_fields": [
"priceAmount"
],
"push_delivery": "api",
"raw_provider_payload": null,
"created_at": "2026-09-16T09:00:00.000Z",
"updated_at": "2026-09-19T10:00:00.000Z"
}
]
}
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
POST /vehicles/publish-bulk
Publish many vehicles to one portal
N vehicles × ONE portal — integration_id is singular. To publish to several portals, call this once per portal.
Each vehicle is isolated: the response is a per-vehicle outcome list, and a 200 does not mean every vehicle published. status is published, already_published (a live ad already existed and nothing was sent), skipped (never reached the portal — vendido, inactive or gone; reason says which), error (error says why) or, for facebook_marketplace and chileautos_panel (see POST /vehicles/\{id\}/publish), pending: the portal call was queued and the real outcome arrives later via vehicle.published / vehicle.publish_failed. Up to 100 vehicles per call, and unlike the close-ads path this runs the portal round-trips synchronously, so a full batch is a slow request. Automotive-vertical only: a workspace on another vertical gets 403 here however its scopes are set.
Cuerpo
| Campo | Tipo | Obligatorio | Restricciones |
|---|---|---|---|
vehicle_ids | uuid[] | sí | |
integration_id | uuid | sí |
curl -X POST https://api.vitrinadev.com/api/v1/vehicles/publish-bulk \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"vehicle_ids": [
"e1e1e1e1-0000-4000-8000-000000000001",
"e1e1e1e1-0000-4000-8000-000000000002"
],
"integration_id": "33333333-0000-4000-8000-000000000001"
}'Ejemplo de respuesta (200)
{
"data": [
{
"vehicle_id": "e1e1e1e1-0000-4000-8000-000000000001",
"status": "published",
"external_id": "MLC-1234567890"
},
{
"vehicle_id": "e1e1e1e1-0000-4000-8000-000000000002",
"status": "skipped",
"reason": "missing_photos"
}
]
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
POST /vehicles/source-retirement/execute
Retire a source
A once-per-source, admin-level decommission — not a casual button. It severs the workspace’s stock from an integration for good, and it is what DELETE /marketplaces/\{id\} refuses to let you skip: deleting a marketplace account that still originates vehicles is blocked precisely so this runs first.
revive is the dealer’s decision from the review list — the vehicle ids to keep. Everything not named stays hidden. Up to 200 ids, and omitting the field revives nothing. Read the report and decide deliberately; there is no un-retire endpoint. Scope: stock_retirement:write. Automotive-vertical only: a workspace on another vertical gets 403 here however its scopes are set.
Cuerpo
| Campo | Tipo | Obligatorio | Restricciones |
|---|---|---|---|
integration_id | uuid | sí | |
revive | uuid[] | — |
curl -X POST https://api.vitrinadev.com/api/v1/vehicles/source-retirement/execute \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"integration_id": "33333333-0000-4000-8000-000000000001",
"revive": [
"e1e1e1e1-0000-4000-8000-000000000001"
]
}'Ejemplo de respuesta (200)
{
"data": {
"tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
"integration_id": "33333333-0000-4000-8000-000000000001",
"retired": 11,
"revived": 1,
"revive_failures": [],
"kept_hidden": [],
"already_retired": false
}
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
POST /vehicles/source-retirement/report
Preview retiring a source
Dry run for Retiro de fuente (ADR 0031): the status of each precondition plus the review list of what would be affected, for one integration_id. Modifies nothing. Scope: stock_retirement:read. Automotive-vertical only: a workspace on another vertical gets 403 here however its scopes are set.
Cuerpo
| Campo | Tipo | Obligatorio | Restricciones |
|---|---|---|---|
integration_id | uuid | sí |
curl -X POST https://api.vitrinadev.com/api/v1/vehicles/source-retirement/report \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"integration_id": "33333333-0000-4000-8000-000000000001"
}'Ejemplo de respuesta (200)
{
"data": {
"tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
"integration_id": "33333333-0000-4000-8000-000000000001",
"provider": "veekls",
"vehicles_with_origin": 12,
"photos": {
"all_mirrored": true,
"pending": 0
},
"sync_still_enabled": false,
"hidden_by_source": [
{
"vehicle_id": "e1e1e1e1-0000-4000-8000-000000000001",
"title": null,
"make": "Toyota",
"model": "Corolla",
"year": 2022,
"plate": "FGXY21",
"status": "disponible"
}
],
"already_retired": false
}
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
GET /vehicles/stats
Aggregate counts for the whole lot
The stock health header — counts across the workspace’s inventory, not a page of vehicles.
by_tenencia (Tenencia, ADR 0025) splits the lot four ways: propio, consignacion_en_local, consignacion_virtual and consignacion_sin_contrato (consigned on the vehicle row with NO contract in estado=activo). The four PARTITION the lot — they sum to total — and each equals the length of the list its chip opens: GET /vehicles?active=false&tenencia=…[&consignacion_modalidad=…], the same predicate builder serving both. Like by_location and total, they count the WHOLE lot rather than active stock only, which is what the Stock screen shows (it sends active=false).
propio_sin_clasificar (Phase 2, tenencia_source, 20270903000000) is a SUB-COUNT of propio, NOT a fifth bucket — the four above still sum to total. It is the cars nobody has ever classified (tenencia_source=default), provably coextensive with "sin clasificar" (a consignación can never be default). Measured on production 2026-08-31: 903 of 967 vehicles carried propio with nobody having said so — that is what this number answers, rather than leaving propio an unresolved warning. Opens with GET /vehicles?active=false&tenencia=propio&tenencia_source=default.
sin_costo (ADR 0114 §8, «Sin costo») counts the active, non-vendido cars with NO acquisition — no nota de compra, contrato de consignación or parte de pago — so their cost is unknown. Opens with GET /vehicles?view=sin_costo, the same predicate. A statement about the cost basis: ABSENT (not 0) for a caller without dealership_economics:read.
mandato_sin_firmar (ADR 0114 §5) counts the cars whose consignación (activa, or vendida) has an emitted mandato and no signed copy filed in the expediente. Opens with GET /vehicles?view=mandato_sin_firmar, the same predicate.
The «Datos por completar» and «Pendientes» facet counts of the Stock filters (ADR 0114) — each is the length of the list it opens, built from the same predicate, over the whole lot like total:
needs_review— «Revisar marca/modelo»: the marca or modelo did not match the catalogue on save and waits for a person. Opens withGET /vehicles?active=false&needs_review=true.sin_sucursal— «Sin sucursal»: no sucursal assigned. Opens withGET /vehicles?active=false&sin_sucursal=true. Always 0 for a member who only sees their own sucursales.sin_estacionamiento— «Sin estacionamiento»: no estacionamiento in the patio. Opens withGET /vehicles?active=false&sin_estacionamiento=true.source_deactivated— «Retirados del portal»: the portal withdrew the car, it is deactivated and stilldisponible, so nobody has said whether it was sold. Opens withGET /vehicles?view=source_deactivated.papers_expired— «Papeles vencidos»: cars in stock (active, not vendido) holding a paper (SOAP, permiso de circulación, revisión técnica or a named paper) whose «Vence» day has passed and that no later paper of the same kind replaces. Opens withGET /vehicles?papers_expired=true.papers_expiring— «Papeles por vencer»: cars in stock holding a paper whose «Vence» day is today or within the next 30 days. Opens withGET /vehicles?papers_expiring=true. A car can be in both.
Every count respects the caller’s sucursal scope (ADR 0102), the same ceiling the list applies. Automotive-vertical only: a workspace on another vertical gets 403 here however its scopes are set.
curl https://api.vitrinadev.com/api/v1/vehicles/stats \
-H "Authorization: Bearer $VITRINA_KEY"Ejemplo de respuesta (200)
{
"data": {
"total": 7,
"active": 7,
"by_status": {
"disponible": 6,
"reservado": 1
},
"published": 0,
"with_error": 0,
"unpublished": 7,
"stale": 0,
"sin_costo": 7,
"mandato_sin_firmar": 1,
"needs_review": 1,
"sin_sucursal": 2,
"sin_estacionamiento": 4,
"source_deactivated": 0,
"papers_expired": 1,
"papers_expiring": 2,
"by_location": {
"b1b1b1b1-0000-4000-8000-000000000001": 3,
"b1b1b1b1-0000-4000-8000-000000000002": 1,
"b1b1b1b1-0000-4000-8000-000000000003": 1
},
"by_tenencia": {
"propio": 7,
"consignacion_en_local": 0,
"consignacion_virtual": 0,
"consignacion_sin_contrato": 0,
"propio_sin_clasificar": 7
}
}
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
POST /vehicles/tenencia
Declare a car propio — no paperwork required
Phase 2 (tenencia_source): the half of the ownership model consignación already had a two-field path for (POST /consignments/bulk → contrato mínimo). tenencia accepts ONLY "propio" — consignación keeps going through its own contrato mínimo. Per-vehicle verdicts in request order, NEVER all-or-nothing: declared (now tenencia_source=declared), skipped_active_contract (an ACTIVE consignment contract stands — end it first via POST /consignments/{id}/return, a devolución, rather than flipping it here), or error (with the reason). Up to 200 vehicles per call, de-duplicated, sequential. Scoped on consignments:write — "the tenencia lifecycle" — not marketplace:write.
Cuerpo
| Campo | Tipo | Obligatorio | Restricciones |
|---|---|---|---|
vehicle_ids | uuid[] | sí | |
tenencia | propio | sí |
curl -X POST https://api.vitrinadev.com/api/v1/vehicles/tenencia \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"vehicle_ids": [
"e1e1e1e1-0000-4000-8000-000000000001"
],
"tenencia": "propio"
}'Ejemplo de respuesta (200)
{
"data": [
{
"vehicle_id": "e1e1e1e1-0000-4000-8000-000000000001",
"outcome": "declared"
}
]
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429