VitrinaAPI

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.

Beta
Puede cambiar en cualquier momento, con una entrada en el changelog y aviso a quienes la llamaron recientemente — ver versionado.

Descarga la proyección completa de la API pública: openapi.json.

MétodoRutaQué hace
GET/vehiclesSearch the tenant vehicle stock (filters + free text)
POST/vehiclesCreate 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}/activityThis car’s lead timeline
GET/vehicles/{id}/busquedasWho is waiting for this car (ADR 0109 §4)
POST/vehicles/{id}/close-adsClose all of a vehicle’s ads
GET/vehicles/{id}/document-discrepanciesWhat this car’s official papers disagree with
POST/vehicles/{id}/document-discrepanciesRecord that an official document disagrees with stock
POST/vehicles/{id}/document-discrepancies/{discrepancyId}/applyApply the document — the document wins
POST/vehicles/{id}/document-discrepancies/{discrepancyId}/dismissalDismiss the document — stock stands
GET/vehicles/{id}/documentsEvery document of one car
GET/vehicles/{id}/duplicate-candidatesPossible duplicates of this car
GET/vehicles/{id}/ficha-tecnicaThe printable spec sheet for one unit
POST/vehicles/{id}/fields/protectProtect (lock) vehicle fields against sync
POST/vehicles/{id}/fields/releaseRelease Override local protection on vehicle fields
GET/vehicles/{id}/historyA vehicle's change history (Historial)
GET/vehicles/{id}/interestWho is asking about this car
POST/vehicles/{id}/mergeMerge two cars into one
PUT/vehicles/{id}/pipelinePlace a vehicle on a reconditioning board, move boards, or remove it
PUT/vehicles/{id}/prep-assigneeName (or un-name) the person driving a vehicle's preparación
GET/vehicles/{id}/publicationsList marketplace publications of a vehicle
DELETE/vehicles/{id}/publications/{pubId}Unpublish a vehicle from a marketplace
POST/vehicles/{id}/publishPublish a vehicle to one or more connected marketplaces
PUT/vehicles/{id}/stageMove a vehicle to another column of its board
PUT/vehicles/{id}/stage-blockRecord (or clear) why a vehicle is stuck in its current column
GET/vehicles/{id}/suggestionsPaperwork this hand-moved unit is still missing
POST/vehicles/{id}/suggestions/{suggestionId}/dismissDismiss the paperwork nudge
GET/vehicles/catalog/brandsList car makes (catalog)
GET/vehicles/catalog/modelsList the models of a make (catalog)
GET/vehicles/catalog/versionsList the versions of a make + model (catalog)
POST/vehicles/close-ads-bulkClose N vehicles' ads on every connected marketplace
POST/vehicles/dedup-backfill/executeApply the reviewed dedup merges
POST/vehicles/dedup-backfill/reportPreview the existing-stock dedup
GET/vehicles/document-discrepanciesThe workspace queue of document-vs-stock disagreements
POST/vehicles/duplicate-candidates/{candidateId}/dismissDismiss a possible duplicate
PATCH/vehicles/estacionamientosBulk-set vehicles' Estacionamiento (patio position)
GET/vehicles/exportExport the Stock list as a PDF hoja de patio or an Excel table
GET/vehicles/field-visibilityWhere each vehicle field publishes (the editor’s marker source)
GET/vehicles/kanbanThe whole reconditioning board for one vehicle pipeline
POST/vehicles/parity-reportCompare native stock against the live source
GET/vehicles/photo-labelsWhat each stock photo shows (exterior, interior, motor…)
PUT/vehicles/photo-labelsCorrect what a stock photo shows
POST/vehicles/photo-labels/labelLabel new uploads now
POST/vehicles/photo-mirror/executeMirror a batch of photos into Vitrina storage
POST/vehicles/photo-mirror/reportPreview the photo-mirror backfill
POST/vehicles/photo-mirror/sweepMirror every remaining hotlink in one call
GET/vehicles/publicationsPublications for many vehicles at once
POST/vehicles/publish-bulkPublish many vehicles to one portal
POST/vehicles/source-retirement/executeRetire a source
POST/vehicles/source-retirement/reportPreview retiring a source
GET/vehicles/statsAggregate counts for the whole lot
POST/vehicles/tenenciaDeclare 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ámetroEnTipoObligatorioRestricciones
qquerystringno
makequerystringno
modelquerystringno
min_pricequerynumber | nullno
max_pricequerynumber | nullno
year_fromqueryinteger | nullno
year_toqueryinteger | nullno
min_kmqueryintegerno
max_kmqueryintegerno
transmissionquerystringno
fuel_typequerystringno
body_stylequerystringno
colorquerystringno
statusquerydisponible \reservado \vendido
location_idqueryuuidno
vehicle_typequeryauto \camion \maquinaria \
has_errorqueryanyno
needs_reviewqueryanyno
sin_estacionamientoqueryanyno
sin_sucursalqueryanyno
papers_expiredquerybooleanno
papers_expiringquerybooleanno
tenenciaquerypropio \consignacionno
consignacion_modalidadqueryen_local \virtual \sin_contrato
tenencia_sourcequerydocument \declared \default \
without_acquisitionqueryanyno
viewquerypublished \unpublished \error \
activequeryanyno
sortqueryprice_asc \price_desc \year_desc \
limitqueryintegerno≥ 1, ≤ 100
offsetqueryinteger | nullno≥ 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

CampoTipoObligatorioRestricciones
makestring | null—máx. 120
modelstring | null—máx. 120
versionstring | null—máx. 120
yearinteger | null—≥ 1900, ≤ 2100
titlestring | null—máx. 255
short_titlestring | null—máx. 255
price_amountnumber | null—≥ 0
price_currencystring—máx. 8
odometer_valueinteger | null—≥ 0
odometer_unitstring—máx. 8
fuel_typestring | null—máx. 60
gear_typestring | null—máx. 60
body_stylestring | null—máx. 60
colorstring | null—máx. 60
doorsinteger | null—≥ 0, ≤ 10
registration_numberstring | null—máx. 60
vinstring | null—máx. 32
listing_typestring | null—máx. 30
record_typestring | null—máx. 120
typestring | null—máx. 30
merch_labeloferta \liquidacion \poco_km \
is_featuredboolean—
sold_outside_vitrinaboolean—
vehicle_typeauto \camion \maquinaria \
sale_statusstring | null—máx. 60
statusdisponible \reservado \vendido
location_idstring | null—
estacionamientostring | null—
watch_enabledboolean—
floor_price_clpinteger | null—> 0
target_positioninteger | null—≥ 25, ≤ 75
abono_amount_clpinteger—≥ 1, ≤ 999999999
descriptionstring | null—
photosobject[]—
attributesobject—
tagsstring[]—
activeboolean—
keys_countinteger | null—≥ 0
municipalidad_comuna_codestring | null—patrón ^[0-9]{5}$
last_service_datestring | null—patrón ^\d{4}-\d{2}-\d{2}$
service_odometer_kminteger | null—≥ 0
responsible_user_idstring | null—
publish_tostring[]—
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ámetroEnTipoObligatorioRestricciones
idpathuuidsí
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ámetroEnTipoObligatorioRestricciones
idpathuuidsí
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.&lt;field&gt; 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ámetroEnTipoObligatorioRestricciones
idpathuuidsí

Cuerpo

CampoTipoObligatorioRestricciones
makestring | null—máx. 120
modelstring | null—máx. 120
versionstring | null—máx. 120
yearinteger | null—≥ 1900, ≤ 2100
titlestring | null—máx. 255
short_titlestring | null—máx. 255
price_amountnumber | null—≥ 0
price_currencystring—máx. 8
odometer_valueinteger | null—≥ 0
odometer_unitstring—máx. 8
fuel_typestring | null—máx. 60
gear_typestring | null—máx. 60
body_stylestring | null—máx. 60
colorstring | null—máx. 60
doorsinteger | null—≥ 0, ≤ 10
registration_numberstring | null—máx. 60
vinstring | null—máx. 32
listing_typestring | null—máx. 30
record_typestring | null—máx. 120
typestring | null—máx. 30
merch_labeloferta \liquidacion \poco_km \
is_featuredboolean—
sold_outside_vitrinaboolean—
vehicle_typeauto \camion \maquinaria \
sale_statusstring | null—máx. 60
statusdisponible \reservado \vendido
location_idstring | null—
estacionamientostring | null—
watch_enabledboolean—
floor_price_clpinteger | null—> 0
target_positioninteger | null—≥ 25, ≤ 75
abono_amount_clpinteger—≥ 1, ≤ 999999999
descriptionstring | null—
photosobject[]—
attributesobject—
tagsstring[]—
activeboolean—
keys_countinteger | null—≥ 0
municipalidad_comuna_codestring | null—patrón ^[0-9]{5}$
last_service_datestring | null—patrón ^\d{4}-\d{2}-\d{2}$
service_odometer_kminteger | null—≥ 0
responsible_user_idstring | null—
publish_tostring[]—
confirm_large_price_changeboolean—
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ámetroEnTipoObligatorioRestricciones
idpathuuidsí
limitqueryintegerno≥ 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ámetroEnTipoObligatorioRestricciones
idpathuuidsí
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ámetroEnTipoObligatorioRestricciones
idpathuuidsí
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ámetroEnTipoObligatorioRestricciones
idpathuuidsí
standingqueryopen \resolvedno
limitqueryintegerno≥ 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ámetroEnTipoObligatorioRestricciones
idpathuuidsí

Cuerpo

CampoTipoObligatorioRestricciones
attachment_iduuidsí
fieldregistration_number \vin \year \
document_valuestringsímín. 1, máx. 120
notestring—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ámetroEnTipoObligatorioRestricciones
idpathuuidsí
discrepancyIdpathuuidsí

Cuerpo

CampoTipoObligatorioRestricciones
reasonstringsí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ámetroEnTipoObligatorioRestricciones
idpathuuidsí
discrepancyIdpathuuidsí

Cuerpo

CampoTipoObligatorioRestricciones
reasonstringsí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ámetroEnTipoObligatorioRestricciones
idpathuuidsí
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ámetroEnTipoObligatorioRestricciones
idpathuuidsí
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ámetroEnTipoObligatorioRestricciones
idpathuuidsí
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ámetroEnTipoObligatorioRestricciones
idpathuuidsí

Cuerpo

CampoTipoObligatorioRestricciones
fieldsstring[]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ámetroEnTipoObligatorioRestricciones
idpathuuidsí

Cuerpo

CampoTipoObligatorioRestricciones
fieldsstring[]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ámetroEnTipoObligatorioRestricciones
idpathuuidsí
limitqueryintegerno≥ 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ámetroEnTipoObligatorioRestricciones
idpathuuidsí
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ámetroEnTipoObligatorioRestricciones
idpathuuidsí

Cuerpo

CampoTipoObligatorioRestricciones
other_vehicle_iduuidsí
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ámetroEnTipoObligatorioRestricciones
idpathuuidsí

Cuerpo

CampoTipoObligatorioRestricciones
pipeline_idstring | nullsí
stage_iduuid—
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ámetroEnTipoObligatorioRestricciones
idpathuuidsí

Cuerpo

CampoTipoObligatorioRestricciones
user_idstring | nullsí
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ámetroEnTipoObligatorioRestricciones
idpathuuidsí
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ámetroEnTipoObligatorioRestricciones
idpathuuidsí
pubIdpathuuidsí
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ámetroEnTipoObligatorioRestricciones
idpathuuidsí

Cuerpo

CampoTipoObligatorioRestricciones
integration_idsuuid[]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ámetroEnTipoObligatorioRestricciones
idpathuuidsí

Cuerpo

CampoTipoObligatorioRestricciones
stage_iduuidsí
reasonstring | 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ámetroEnTipoObligatorioRestricciones
idpathuuidsí

Cuerpo

CampoTipoObligatorioRestricciones
reasonstring | nullsí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ámetroEnTipoObligatorioRestricciones
idpathuuidsí
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ámetroEnTipoObligatorioRestricciones
idpathuuidsí
suggestionIdpathuuidsí
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ámetroEnTipoObligatorioRestricciones
brandquerystringsí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ámetroEnTipoObligatorioRestricciones
brandquerystringsímín. 1, máx. 120
modelquerystringsí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

CampoTipoObligatorioRestricciones
vehicle_idsuuid[]sí
intentsold \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

CampoTipoObligatorioRestricciones
approveobject[]—
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ámetroEnTipoObligatorioRestricciones
standingqueryopen \resolvedno
limitqueryintegerno≥ 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ámetroEnTipoObligatorioRestricciones
candidateIdpathuuidsí
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

CampoTipoObligatorioRestricciones
assignmentsobject[]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ámetroEnTipoObligatorioRestricciones
formatqueryxlsx \pdfno
qquerystringno
viewquerypublished \unpublished \error \
location_idqueryuuidno
statusquerydisponible \reservado \vendido
vehicle_typequeryauto \camion \maquinaria \
has_errorqueryboolean | nullno
needs_reviewqueryboolean | nullno
sin_estacionamientoqueryboolean | nullno
sin_sucursalqueryboolean | nullno
papers_expiredqueryanyno
papers_expiringqueryanyno
tenenciaquerypropio \consignacionno
consignacion_modalidadqueryen_local \virtual \sin_contrato
tenencia_sourcequerydocument \declared \default \
sortqueryprice_asc \price_desc \year_desc \
idsquerystringnomín. 1
include_inactivequeryboolean | nullno
include_internal_pricesqueryboolean | nullno
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ámetroEnTipoObligatorioRestricciones
pipeline_idqueryuuidsí
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

CampoTipoObligatorioRestricciones
integration_iduuidsí
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ámetroEnTipoObligatorioRestricciones
vehicle_idqueryuuidno
urlqueryanyno
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

CampoTipoObligatorioRestricciones
urlstringsímín. 1, máx. 2048
viewexterior \interior \motor \
descriptionstring | null—máx. 80
vehicle_iduuid—
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

CampoTipoObligatorioRestricciones
urlsstring[]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

CampoTipoObligatorioRestricciones
limitinteger—≥ 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

CampoTipoObligatorioRestricciones
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ámetroEnTipoObligatorioRestricciones
idsquerystringsí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

CampoTipoObligatorioRestricciones
vehicle_idsuuid[]sí
integration_iduuidsí
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

CampoTipoObligatorioRestricciones
integration_iduuidsí
reviveuuid[]—
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

CampoTipoObligatorioRestricciones
integration_iduuidsí
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 with GET /vehicles?active=false&needs_review=true.
  • sin_sucursal — «Sin sucursal»: no sucursal assigned. Opens with GET /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 with GET /vehicles?active=false&sin_estacionamiento=true.
  • source_deactivated — «Retirados del portal»: the portal withdrew the car, it is deactivated and still disponible, so nobody has said whether it was sold. Opens with GET /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 with GET /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 with GET /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

CampoTipoObligatorioRestricciones
vehicle_idsuuid[]sí
tenenciapropiosí
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

En esta página

GET /vehiclesPOST /vehiclesDELETE /vehicles/{id}GET /vehicles/{id}PUT /vehicles/{id}GET /vehicles/{id}/activityGET /vehicles/{id}/busquedasPOST /vehicles/{id}/close-adsGET /vehicles/{id}/document-discrepanciesPOST /vehicles/{id}/document-discrepanciesPOST /vehicles/{id}/document-discrepancies/{discrepancyId}/applyPOST /vehicles/{id}/document-discrepancies/{discrepancyId}/dismissalGET /vehicles/{id}/documentsGET /vehicles/{id}/duplicate-candidatesGET /vehicles/{id}/ficha-tecnicaPOST /vehicles/{id}/fields/protectPOST /vehicles/{id}/fields/releaseGET /vehicles/{id}/historyGET /vehicles/{id}/interestPOST /vehicles/{id}/mergePUT /vehicles/{id}/pipelinePUT /vehicles/{id}/prep-assigneeGET /vehicles/{id}/publicationsDELETE /vehicles/{id}/publications/{pubId}POST /vehicles/{id}/publishPUT /vehicles/{id}/stagePUT /vehicles/{id}/stage-blockGET /vehicles/{id}/suggestionsPOST /vehicles/{id}/suggestions/{suggestionId}/dismissGET /vehicles/catalog/brandsGET /vehicles/catalog/modelsGET /vehicles/catalog/versionsPOST /vehicles/close-ads-bulkPOST /vehicles/dedup-backfill/executePOST /vehicles/dedup-backfill/reportGET /vehicles/document-discrepanciesPOST /vehicles/duplicate-candidates/{candidateId}/dismissPATCH /vehicles/estacionamientosGET /vehicles/exportGET /vehicles/field-visibilityGET /vehicles/kanbanPOST /vehicles/parity-reportGET /vehicles/photo-labelsPUT /vehicles/photo-labelsPOST /vehicles/photo-labels/labelPOST /vehicles/photo-mirror/executePOST /vehicles/photo-mirror/reportPOST /vehicles/photo-mirror/sweepGET /vehicles/publicationsPOST /vehicles/publish-bulkPOST /vehicles/source-retirement/executePOST /vehicles/source-retirement/reportGET /vehicles/statsPOST /vehicles/tenencia