Vehicles
Stock from the inside: create, edit, stages, portal publications and the inventory maintenance tools. Needs wider scopes than public stock.
Stock from the inside: create, edit, stages, portal publications and the inventory maintenance tools. Needs wider scopes than public stock.
| Method | Path | What it does |
|---|---|---|
GET | /vehicles | Search the tenant vehicle stock (filters + free text) |
POST | /vehicles | Create a vehicle (manual stock) |
DELETE | /vehicles/{id} | Delete a vehicle |
GET | /vehicles/{id} | Get a vehicle |
PUT | /vehicles/{id} | Update a vehicle |
GET | /vehicles/{id}/activity | This car’s lead timeline |
POST | /vehicles/{id}/close-ads | Close all of a vehicle’s ads |
GET | /vehicles/{id}/document-discrepancies | What this car’s official papers disagree with |
POST | /vehicles/{id}/document-discrepancies | Record that an official document disagrees with stock |
POST | /vehicles/{id}/document-discrepancies/{discrepancyId}/apply | Apply the document — the document wins |
POST | /vehicles/{id}/document-discrepancies/{discrepancyId}/dismissal | Dismiss the document — stock stands |
GET | /vehicles/{id}/ficha-tecnica | The printable spec sheet for one unit |
POST | /vehicles/{id}/fields/protect | Protect (lock) vehicle fields against sync |
POST | /vehicles/{id}/fields/release | Release Override local protection on vehicle fields |
GET | /vehicles/{id}/history | A vehicle's change history (Historial) |
GET | /vehicles/{id}/interest | Who is asking about this car |
PUT | /vehicles/{id}/pipeline | Place a vehicle on a reconditioning board, move boards, or remove it |
PUT | /vehicles/{id}/prep-assignee | Name (or un-name) the person driving a vehicle's preparación |
GET | /vehicles/{id}/publications | List marketplace publications of a vehicle |
DELETE | /vehicles/{id}/publications/{pubId} | Unpublish a vehicle from a marketplace |
POST | /vehicles/{id}/publish | Publish a vehicle to one or more connected marketplaces |
PUT | /vehicles/{id}/stage | Move a vehicle to another column of its board |
PUT | /vehicles/{id}/stage-block | Record (or clear) why a vehicle is stuck in its current column |
GET | /vehicles/{id}/suggestions | Paperwork this hand-moved unit is still missing |
POST | /vehicles/{id}/suggestions/{suggestionId}/dismiss | Dismiss the paperwork nudge |
GET | /vehicles/catalog/brands | List canonical vehicle brands (autocomplete) |
GET | /vehicles/catalog/models | List canonical models for a brand (autocomplete) |
GET | /vehicles/catalog/versions | List versions/trims for a brand+model (autocomplete suggestions) |
POST | /vehicles/close-ads-bulk | Close N vehicles' ads on every connected marketplace |
POST | /vehicles/dedup-backfill/execute | Apply the reviewed dedup merges |
POST | /vehicles/dedup-backfill/report | Preview the existing-stock dedup |
GET | /vehicles/document-discrepancies | The workspace queue of document-vs-stock disagreements |
PATCH | /vehicles/estacionamientos | Bulk-set vehicles' Estacionamiento (patio position) |
GET | /vehicles/export | Export the Stock list as a PDF hoja de patio or an Excel table |
GET | /vehicles/field-visibility | Where each vehicle field publishes (the editor’s marker source) |
GET | /vehicles/kanban | The whole reconditioning board for one vehicle pipeline |
POST | /vehicles/parity-report | Compare native stock against the live source |
POST | /vehicles/photo-mirror/execute | Mirror a batch of photos into Vitrina storage |
POST | /vehicles/photo-mirror/report | Preview the photo-mirror backfill |
POST | /vehicles/photo-mirror/sweep | Mirror every remaining hotlink in one call |
GET | /vehicles/publications | Publications for many vehicles at once |
POST | /vehicles/publish-bulk | Publish many vehicles to one portal |
POST | /vehicles/source-retirement/execute | Retire a source |
POST | /vehicles/source-retirement/report | Preview retiring a source |
GET | /vehicles/stats | Aggregate counts for the whole lot |
POST | /vehicles/tenencia | Declare a car propio — no paperwork required |
GET /vehicles
Search the tenant vehicle stock (filters + free text)
FIELD-LEVEL COST OMISSION (ERP S3-6): the per-unit money position — floor_price_clp (precio piso) — is withheld unless the caller holds dealership_economics:read. The key is ABSENT from the response object, not null: a caller who checks 'floor_price_clp' in body sees it was never offered. price_clp (the asking price) is unaffected — it is the one money figure about a unit that is meant to be seen. The scope is NOT an alternative to marketplace:read (requireScope ORs its varargs, which would widen this route rather than narrow the field); it is an addition.
TENENCIA (ADR 0025). tenencia filters the vehicle-level discriminator (propio | consignacion); consignacion_modalidad narrows a consignación by its ACTIVE contract — en_local, virtual (the car stays with its dueño) or sin_contrato (consigned with no contract in estado=activo on file). consignacion_modalidad is REFUSED with 400 unless tenencia=consignacion came with it: it names a kind of consignación, so it is only answerable about one.
Every row carries tenencia and not_on_lot — the latter true iff an ACTIVE contract with modalidad=virtual exists, resolved in ONE batched query for the page (never per row). not_on_lot is the flag a test-drive or walking-sheet surface acts on; tenencia alone cannot answer it, because the modalidad lives on the contract.
⚠ 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.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
q | query | string | no | |
make | query | string | no | |
model | query | string | no | |
min_price | query | number | null | no | |
max_price | query | number | null | no | |
year_from | query | integer | null | no | |
year_to | query | integer | null | no | |
transmission | query | string | no | |
fuel_type | query | string | no | |
body_style | query | string | no | |
color | query | string | no | |
status | query | disponible \ | reservado \ | vendido |
location_id | query | uuid | no | |
vehicle_type | query | auto \ | camion \ | maquinaria \ |
has_error | query | any | no | |
needs_review | query | any | no | |
sin_estacionamiento | query | any | no | |
sin_sucursal | query | any | no | |
tenencia | query | propio \ | consignacion | no |
consignacion_modalidad | query | en_local \ | virtual \ | sin_contrato |
tenencia_source | query | document \ | declared \ | default \ |
without_acquisition | query | any | no | |
view | query | published \ | unpublished \ | error \ |
active | query | any | no | |
sort | query | price_asc \ | price_desc \ | year_desc \ |
limit | query | integer | no | ≥ 1, ≤ 100 |
offset | query | integer | null | no | ≥ 0 |
curl https://api.vitrinadev.com/api/v1/vehicles \
-H "Authorization: Bearer $VITRINA_KEY"Answers: 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). A plate the Chilean grammar cannot read derives plate_normalized = null, so the DB backstop cannot see it: the service then compares the RAW value (upper/trim) against the tenant active stock and answers the SAME 409, and flags the unit under attributes.curated.needsReview = ["plate"] with curated.plateReason = "plate_unrecognized". The plate is never refused on its own merits (allow-and-flag, ADR 0022) — only a duplicate is.
Body
| Field | Type | Required | Constraints |
|---|---|---|---|
make | string | null | — | máx. 120 |
model | string | null | — | máx. 120 |
version | string | null | — | máx. 120 |
year | integer | null | — | ≥ 1900, ≤ 2100 |
title | string | null | — | máx. 255 |
short_title | string | null | — | máx. 255 |
price_amount | number | null | — | ≥ 0 |
price_currency | string | — | máx. 8 |
odometer_value | integer | null | — | ≥ 0 |
odometer_unit | string | — | máx. 8 |
fuel_type | string | null | — | máx. 60 |
gear_type | string | null | — | máx. 60 |
body_style | string | null | — | máx. 60 |
color | string | null | — | máx. 60 |
doors | integer | null | — | ≥ 0, ≤ 10 |
registration_number | string | null | — | máx. 60 |
vin | string | null | — | máx. 32 |
listing_type | string | null | — | máx. 30 |
record_type | string | null | — | máx. 120 |
type | string | null | — | máx. 30 |
merch_label | oferta \ | liquidacion \ | poco_km \ |
is_featured | boolean | — | |
vehicle_type | auto \ | camion \ | maquinaria \ |
sale_status | string | null | — | máx. 60 |
status | disponible \ | reservado \ | vendido |
location_id | string | null | — | |
estacionamiento | string | null | — | |
watch_enabled | boolean | — | |
floor_price_clp | integer | null | — | > 0 |
target_position | integer | null | — | ≥ 25, ≤ 75 |
abono_amount_clp | integer | — | ≥ 1, ≤ 999999999 |
description | string | null | — | |
photos | object[] | — | |
attributes | object | — | |
tags | string[] | — | |
active | boolean | — | |
keys_count | integer | null | — | ≥ 0 |
municipalidad_comuna_code | string | null | — | patrón ^[0-9]{5}$ |
last_service_date | string | null | — | patrón ^\d{4}-\d{2}-\d{2}$ |
service_odometer_km | integer | null | — | ≥ 0 |
curl -X POST https://api.vitrinadev.com/api/v1/vehicles \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{ … }'Answers: 201 · 400 · 401 · 403 · 404 · 409 · 429
DELETE /vehicles/{id}
Delete a vehicle
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
id | path | uuid | yes |
curl -X DELETE https://api.vitrinadev.com/api/v1/vehicles/<id> \
-H "Authorization: Bearer $VITRINA_KEY"Answers: 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.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
id | path | uuid | yes |
curl https://api.vitrinadev.com/api/v1/vehicles/<id> \
-H "Authorization: Bearer $VITRINA_KEY"Answers: 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).
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
id | path | uuid | yes |
Body
| Field | Type | Required | Constraints |
|---|---|---|---|
make | string | null | — | máx. 120 |
model | string | null | — | máx. 120 |
version | string | null | — | máx. 120 |
year | integer | null | — | ≥ 1900, ≤ 2100 |
title | string | null | — | máx. 255 |
short_title | string | null | — | máx. 255 |
price_amount | number | null | — | ≥ 0 |
price_currency | string | — | máx. 8 |
odometer_value | integer | null | — | ≥ 0 |
odometer_unit | string | — | máx. 8 |
fuel_type | string | null | — | máx. 60 |
gear_type | string | null | — | máx. 60 |
body_style | string | null | — | máx. 60 |
color | string | null | — | máx. 60 |
doors | integer | null | — | ≥ 0, ≤ 10 |
registration_number | string | null | — | máx. 60 |
vin | string | null | — | máx. 32 |
listing_type | string | null | — | máx. 30 |
record_type | string | null | — | máx. 120 |
type | string | null | — | máx. 30 |
merch_label | oferta \ | liquidacion \ | poco_km \ |
is_featured | boolean | — | |
vehicle_type | auto \ | camion \ | maquinaria \ |
sale_status | string | null | — | máx. 60 |
status | disponible \ | reservado \ | vendido |
location_id | string | null | — | |
estacionamiento | string | null | — | |
watch_enabled | boolean | — | |
floor_price_clp | integer | null | — | > 0 |
target_position | integer | null | — | ≥ 25, ≤ 75 |
abono_amount_clp | integer | — | ≥ 1, ≤ 999999999 |
description | string | null | — | |
photos | object[] | — | |
attributes | object | — | |
tags | string[] | — | |
active | boolean | — | |
keys_count | integer | null | — | ≥ 0 |
municipalidad_comuna_code | string | null | — | patrón ^[0-9]{5}$ |
last_service_date | string | null | — | patrón ^\d{4}-\d{2}-\d{2}$ |
service_odometer_km | integer | null | — | ≥ 0 |
curl -X PUT https://api.vitrinadev.com/api/v1/vehicles/<id> \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{ … }'Answers: 200 · 400 · 401 · 403 · 404 · 409 · 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.
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.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
id | path | uuid | yes | |
limit | query | integer | no | ≥ 1, ≤ 50 |
curl https://api.vitrinadev.com/api/v1/vehicles/<id>/activity \
-H "Authorization: Bearer $VITRINA_KEY"Answers: 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.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
id | path | uuid | yes |
curl -X POST https://api.vitrinadev.com/api/v1/vehicles/<id>/close-ads \
-H "Authorization: Bearer $VITRINA_KEY"Answers: 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).
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
id | path | uuid | yes | |
standing | query | open \ | resolved | no |
limit | query | integer | no | ≥ 1, ≤ 500 |
curl https://api.vitrinadev.com/api/v1/vehicles/<id>/document-discrepancies \
-H "Authorization: Bearer $VITRINA_KEY"Answers: 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.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
id | path | uuid | yes |
Body
| Field | Type | Required | Constraints |
|---|---|---|---|
attachment_id | uuid | yes | |
field | registration_number \ | vin \ | year \ |
document_value | string | yes | mín. 1, máx. 120 |
note | string | — | mín. 1, máx. 500 |
curl -X POST https://api.vitrinadev.com/api/v1/vehicles/<id>/document-discrepancies \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{ … }'Answers: 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.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
id | path | uuid | yes | |
discrepancyId | path | uuid | yes |
Body
| Field | Type | Required | Constraints |
|---|---|---|---|
reason | string | yes | 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 '{ … }'Answers: 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.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
id | path | uuid | yes | |
discrepancyId | path | uuid | yes |
Body
| Field | Type | Required | Constraints |
|---|---|---|---|
reason | string | yes | 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 '{ … }'Answers: 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.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
id | path | uuid | yes |
curl https://api.vitrinadev.com/api/v1/vehicles/<id>/ficha-tecnica \
-H "Authorization: Bearer $VITRINA_KEY"Answers: 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.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
id | path | uuid | yes |
Body
| Field | Type | Required | Constraints |
|---|---|---|---|
fields | string[] | yes |
curl -X POST https://api.vitrinadev.com/api/v1/vehicles/<id>/fields/protect \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{ … }'Answers: 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.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
id | path | uuid | yes |
Body
| Field | Type | Required | Constraints |
|---|---|---|---|
fields | string[] | yes |
curl -X POST https://api.vitrinadev.com/api/v1/vehicles/<id>/fields/release \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{ … }'Answers: 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. The UI merges it with /activity (leads) into one "Leads y cambios" timeline.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
id | path | uuid | yes | |
limit | query | integer | no | ≥ 1, ≤ 100 |
curl https://api.vitrinadev.com/api/v1/vehicles/<id>/history \
-H "Authorization: Bearer $VITRINA_KEY"Answers: 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.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
id | path | uuid | yes |
curl https://api.vitrinadev.com/api/v1/vehicles/<id>/interest \
-H "Authorization: Bearer $VITRINA_KEY"Answers: 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.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
id | path | uuid | yes |
Body
| Field | Type | Required | Constraints |
|---|---|---|---|
pipeline_id | string | null | yes | |
stage_id | uuid | — |
curl -X PUT https://api.vitrinadev.com/api/v1/vehicles/<id>/pipeline \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{ … }'Answers: 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.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
id | path | uuid | yes |
Body
| Field | Type | Required | Constraints |
|---|---|---|---|
user_id | string | null | yes |
curl -X PUT https://api.vitrinadev.com/api/v1/vehicles/<id>/prep-assignee \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{ … }'Answers: 200 · 400 · 401 · 403 · 404 · 409 · 429
GET /vehicles/{id}/publications
List marketplace publications of a vehicle
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
id | path | uuid | yes |
curl https://api.vitrinadev.com/api/v1/vehicles/<id>/publications \
-H "Authorization: Bearer $VITRINA_KEY"Answers: 200 · 400 · 401 · 403 · 404 · 409 · 429
DELETE /vehicles/{id}/publications/{pubId}
Unpublish a vehicle from a marketplace
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
id | path | uuid | yes | |
pubId | path | uuid | yes |
curl -X DELETE https://api.vitrinadev.com/api/v1/vehicles/<id>/publications/<id> \
-H "Authorization: Bearer $VITRINA_KEY"Answers: 200 · 400 · 401 · 403 · 404 · 409 · 429
POST /vehicles/{id}/publish
Publish a vehicle to one or more connected marketplaces
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
id | path | uuid | yes |
Body
| Field | Type | Required | Constraints |
|---|---|---|---|
integration_ids | uuid[] | yes |
curl -X POST https://api.vitrinadev.com/api/v1/vehicles/<id>/publish \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{ … }'Answers: 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.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
id | path | uuid | yes |
Body
| Field | Type | Required | Constraints |
|---|---|---|---|
stage_id | uuid | yes | |
reason | string | null | — | máx. 500 |
curl -X PUT https://api.vitrinadev.com/api/v1/vehicles/<id>/stage \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{ … }'Answers: 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().
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
id | path | uuid | yes |
Body
| Field | Type | Required | Constraints |
|---|---|---|---|
reason | string | null | yes | 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 '{ … }'Answers: 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.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
id | path | uuid | yes |
curl https://api.vitrinadev.com/api/v1/vehicles/<id>/suggestions \
-H "Authorization: Bearer $VITRINA_KEY"Answers: 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.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
id | path | uuid | yes | |
suggestionId | path | uuid | yes |
curl -X POST https://api.vitrinadev.com/api/v1/vehicles/<id>/suggestions/<id>/dismiss \
-H "Authorization: Bearer $VITRINA_KEY"Answers: 200 · 400 · 401 · 403 · 404 · 409 · 429
GET /vehicles/catalog/brands
List canonical vehicle brands (autocomplete)
Distinct canonical brands unioned from vehicle_catalog + vehicle_taxonomy, reconciled to the ML_BRANDS spelling. Global reference data (no tenant scope); cacheable.
curl https://api.vitrinadev.com/api/v1/vehicles/catalog/brands \
-H "Authorization: Bearer $VITRINA_KEY"Answers: 200 · 400 · 401 · 403 · 404 · 409 · 429
GET /vehicles/catalog/models
List canonical models for a brand (autocomplete)
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
brand | query | string | yes | mín. 1, máx. 120 |
curl https://api.vitrinadev.com/api/v1/vehicles/catalog/models \
-H "Authorization: Bearer $VITRINA_KEY"Answers: 200 · 400 · 401 · 403 · 404 · 409 · 429
GET /vehicles/catalog/versions
List versions/trims for a brand+model (autocomplete suggestions)
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
brand | query | string | yes | mín. 1, máx. 120 |
model | query | string | yes | mín. 1, máx. 120 |
curl https://api.vitrinadev.com/api/v1/vehicles/catalog/versions \
-H "Authorization: Bearer $VITRINA_KEY"Answers: 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.
Body
| Field | Type | Required | Constraints |
|---|---|---|---|
vehicle_ids | uuid[] | yes | |
intent | sold \ | deactivated | — |
curl -X POST https://api.vitrinadev.com/api/v1/vehicles/close-ads-bulk \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{ … }'Answers: 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.
Body
| Field | Type | Required | Constraints |
|---|---|---|---|
approve | object[] | — |
curl -X POST https://api.vitrinadev.com/api/v1/vehicles/dedup-backfill/execute \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{ … }'Answers: 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"Answers: 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.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
standing | query | open \ | resolved | no |
limit | query | integer | no | ≥ 1, ≤ 500 |
curl https://api.vitrinadev.com/api/v1/vehicles/document-discrepancies \
-H "Authorization: Bearer $VITRINA_KEY"Answers: 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.
Body
| Field | Type | Required | Constraints |
|---|---|---|---|
assignments | object[] | yes |
curl -X PATCH https://api.vitrinadev.com/api/v1/vehicles/estacionamientos \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{ … }'Answers: 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.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
format | query | xlsx \ | pdf | no |
q | query | string | no | |
view | query | published \ | unpublished \ | error \ |
location_id | query | uuid | no | |
status | query | disponible \ | reservado \ | vendido |
vehicle_type | query | auto \ | camion \ | maquinaria \ |
has_error | query | boolean | null | no | |
needs_review | query | boolean | null | no | |
sin_estacionamiento | query | boolean | null | no | |
sin_sucursal | query | boolean | null | no | |
tenencia | query | propio \ | consignacion | no |
consignacion_modalidad | query | en_local \ | virtual \ | sin_contrato |
tenencia_source | query | document \ | declared \ | default \ |
sort | query | price_asc \ | price_desc \ | year_desc \ |
ids | query | string | no | mín. 1 |
include_inactive | query | boolean | null | no | |
include_internal_prices | query | boolean | null | no |
curl https://api.vitrinadev.com/api/v1/vehicles/export \
-H "Authorization: Bearer $VITRINA_KEY"Answers: 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"Answers: 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.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
pipeline_id | query | uuid | yes |
curl https://api.vitrinadev.com/api/v1/vehicles/kanban \
-H "Authorization: Bearer $VITRINA_KEY"Answers: 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.
Body
| Field | Type | Required | Constraints |
|---|---|---|---|
integration_id | uuid | yes |
curl -X POST https://api.vitrinadev.com/api/v1/vehicles/parity-report \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{ … }'Answers: 200 · 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.
Body
| Field | Type | Required | Constraints |
|---|---|---|---|
limit | integer | — | ≥ 1, ≤ 200 |
curl -X POST https://api.vitrinadev.com/api/v1/vehicles/photo-mirror/execute \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{ … }'Answers: 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"Answers: 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.
Body
| Field | Type | Required | Constraints |
|---|
curl -X POST https://api.vitrinadev.com/api/v1/vehicles/photo-mirror/sweep \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{ … }'Answers: 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". Automotive-vertical only: a workspace on another vertical gets 403 here however its scopes are set.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
ids | query | string | yes | mín. 1 |
curl https://api.vitrinadev.com/api/v1/vehicles/publications \
-H "Authorization: Bearer $VITRINA_KEY"Answers: 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 of published / skipped / error, and a 200 does not mean every vehicle published. Read the outcomes. 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.
Body
| Field | Type | Required | Constraints |
|---|---|---|---|
vehicle_ids | uuid[] | yes | |
integration_id | uuid | yes |
curl -X POST https://api.vitrinadev.com/api/v1/vehicles/publish-bulk \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{ … }'Answers: 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.
Body
| Field | Type | Required | Constraints |
|---|---|---|---|
integration_id | uuid | yes | |
revive | uuid[] | — |
curl -X POST https://api.vitrinadev.com/api/v1/vehicles/source-retirement/execute \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{ … }'Answers: 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.
Body
| Field | Type | Required | Constraints |
|---|---|---|---|
integration_id | uuid | yes |
curl -X POST https://api.vitrinadev.com/api/v1/vehicles/source-retirement/report \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{ … }'Answers: 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. 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"Answers: 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.
Body
| Field | Type | Required | Constraints |
|---|---|---|---|
vehicle_ids | uuid[] | yes | |
tenencia | propio | yes |
curl -X POST https://api.vitrinadev.com/api/v1/vehicles/tenencia \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{ … }'Answers: 200 · 400 · 401 · 403 · 404 · 409 · 429