Vitrina API
Referencia

Vehículos

El stock por dentro: alta, edición, etapas, publicaciones en portales y las herramientas de mantención del inventario. Pide scopes más anchos que el stock público.

El stock por dentro: alta, edición, etapas, publicaciones en portales y las herramientas de mantención del inventario. Pide scopes más anchos que el stock público.

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
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}/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
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 canonical vehicle brands (autocomplete)
GET/vehicles/catalog/modelsList canonical models for a brand (autocomplete)
GET/vehicles/catalog/versionsList versions/trims for a brand+model (autocomplete suggestions)
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
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
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.

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" (decidedtenencia_source <> 'default'); document / declared / default match the column exactly. See POST /vehicles/tenencia for how a declared row is produced.

ParámetroEnTipoObligatorioRestricciones
qquerystringno
makequerystringno
modelquerystringno
min_pricequerynumber | nullno
max_pricequerynumber | nullno
year_fromqueryinteger | nullno
year_toqueryinteger | nullno
transmissionquerystringno
fuel_typequerystringno
body_stylequerystringno
colorquerystringno
statusquerydisponible \reservado \vendido
location_idqueryuuidno
vehicle_typequeryauto \camion \maquinaria \
has_errorqueryanyno
needs_reviewqueryanyno
sin_estacionamientoqueryanyno
sin_sucursalqueryanyno
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"

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). 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.

Cuerpo

CampoTipoObligatorioRestricciones
makestring | nullmáx. 120
modelstring | nullmáx. 120
versionstring | nullmáx. 120
yearinteger | null≥ 1900, ≤ 2100
titlestring | nullmáx. 255
short_titlestring | nullmáx. 255
price_amountnumber | null≥ 0
price_currencystringmáx. 8
odometer_valueinteger | null≥ 0
odometer_unitstringmáx. 8
fuel_typestring | nullmáx. 60
gear_typestring | nullmáx. 60
body_stylestring | nullmáx. 60
colorstring | nullmáx. 60
doorsinteger | null≥ 0, ≤ 10
registration_numberstring | nullmáx. 60
vinstring | nullmáx. 32
listing_typestring | nullmáx. 30
record_typestring | nullmáx. 120
typestring | nullmáx. 30
merch_labeloferta \liquidacion \poco_km \
is_featuredboolean
vehicle_typeauto \camion \maquinaria \
sale_statusstring | nullmá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 | nullpatrón ^[0-9]{5}$
last_service_datestring | nullpatrón ^\d{4}-\d{2}-\d{2}$
service_odometer_kminteger | null≥ 0
curl -X POST https://api.vitrinadev.com/api/v1/vehicles \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{ … }'

Responde: 201 · 400 · 401 · 403 · 404 · 409 · 429

DELETE /vehicles/{id}

Delete a vehicle

ParámetroEnTipoObligatorioRestricciones
idpathuuid
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.

ParámetroEnTipoObligatorioRestricciones
idpathuuid
curl https://api.vitrinadev.com/api/v1/vehicles/<id> \
  -H "Authorization: Bearer $VITRINA_KEY"

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).

ParámetroEnTipoObligatorioRestricciones
idpathuuid

Cuerpo

CampoTipoObligatorioRestricciones
makestring | nullmáx. 120
modelstring | nullmáx. 120
versionstring | nullmáx. 120
yearinteger | null≥ 1900, ≤ 2100
titlestring | nullmáx. 255
short_titlestring | nullmáx. 255
price_amountnumber | null≥ 0
price_currencystringmáx. 8
odometer_valueinteger | null≥ 0
odometer_unitstringmáx. 8
fuel_typestring | nullmáx. 60
gear_typestring | nullmáx. 60
body_stylestring | nullmáx. 60
colorstring | nullmáx. 60
doorsinteger | null≥ 0, ≤ 10
registration_numberstring | nullmáx. 60
vinstring | nullmáx. 32
listing_typestring | nullmáx. 30
record_typestring | nullmáx. 120
typestring | nullmáx. 30
merch_labeloferta \liquidacion \poco_km \
is_featuredboolean
vehicle_typeauto \camion \maquinaria \
sale_statusstring | nullmá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 | nullpatrón ^[0-9]{5}$
last_service_datestring | nullpatrón ^\d{4}-\d{2}-\d{2}$
service_odometer_kminteger | null≥ 0
curl -X PUT https://api.vitrinadev.com/api/v1/vehicles/<id> \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{ … }'

Responde: 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.

ParámetroEnTipoObligatorioRestricciones
idpathuuid
limitqueryintegerno≥ 1, ≤ 50
curl https://api.vitrinadev.com/api/v1/vehicles/<id>/activity \
  -H "Authorization: Bearer $VITRINA_KEY"

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
idpathuuid
curl -X POST https://api.vitrinadev.com/api/v1/vehicles/<id>/close-ads \
  -H "Authorization: Bearer $VITRINA_KEY"

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
idpathuuid
standingqueryopen \resolvedno
limitqueryintegerno≥ 1, ≤ 500
curl https://api.vitrinadev.com/api/v1/vehicles/<id>/document-discrepancies \
  -H "Authorization: Bearer $VITRINA_KEY"

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
idpathuuid

Cuerpo

CampoTipoObligatorioRestricciones
attachment_iduuid
fieldregistration_number \vin \year \
document_valuestringmín. 1, máx. 120
notestringmí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 '{ … }'

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
idpathuuid
discrepancyIdpathuuid

Cuerpo

CampoTipoObligatorioRestricciones
reasonstringmí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 '{ … }'

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
idpathuuid
discrepancyIdpathuuid

Cuerpo

CampoTipoObligatorioRestricciones
reasonstringmí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 '{ … }'

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
idpathuuid
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
idpathuuid

Cuerpo

CampoTipoObligatorioRestricciones
fieldsstring[]
curl -X POST https://api.vitrinadev.com/api/v1/vehicles/<id>/fields/protect \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{ … }'

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
idpathuuid

Cuerpo

CampoTipoObligatorioRestricciones
fieldsstring[]
curl -X POST https://api.vitrinadev.com/api/v1/vehicles/<id>/fields/release \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{ … }'

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. The UI merges it with /activity (leads) into one "Leads y cambios" timeline.

ParámetroEnTipoObligatorioRestricciones
idpathuuid
limitqueryintegerno≥ 1, ≤ 100
curl https://api.vitrinadev.com/api/v1/vehicles/<id>/history \
  -H "Authorization: Bearer $VITRINA_KEY"

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
idpathuuid
curl https://api.vitrinadev.com/api/v1/vehicles/<id>/interest \
  -H "Authorization: Bearer $VITRINA_KEY"

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
idpathuuid

Cuerpo

CampoTipoObligatorioRestricciones
pipeline_idstring | null
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 '{ … }'

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
idpathuuid

Cuerpo

CampoTipoObligatorioRestricciones
user_idstring | null
curl -X PUT https://api.vitrinadev.com/api/v1/vehicles/<id>/prep-assignee \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{ … }'

Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429

GET /vehicles/{id}/publications

List marketplace publications of a vehicle

ParámetroEnTipoObligatorioRestricciones
idpathuuid
curl https://api.vitrinadev.com/api/v1/vehicles/<id>/publications \
  -H "Authorization: Bearer $VITRINA_KEY"

Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429

DELETE /vehicles/{id}/publications/{pubId}

Unpublish a vehicle from a marketplace

ParámetroEnTipoObligatorioRestricciones
idpathuuid
pubIdpathuuid
curl -X DELETE https://api.vitrinadev.com/api/v1/vehicles/<id>/publications/<id> \
  -H "Authorization: Bearer $VITRINA_KEY"

Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429

POST /vehicles/{id}/publish

Publish a vehicle to one or more connected marketplaces

ParámetroEnTipoObligatorioRestricciones
idpathuuid

Cuerpo

CampoTipoObligatorioRestricciones
integration_idsuuid[]
curl -X POST https://api.vitrinadev.com/api/v1/vehicles/<id>/publish \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{ … }'

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
idpathuuid

Cuerpo

CampoTipoObligatorioRestricciones
stage_iduuid
reasonstring | nullmá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 '{ … }'

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
idpathuuid

Cuerpo

CampoTipoObligatorioRestricciones
reasonstring | nullmá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 '{ … }'

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
idpathuuid
curl https://api.vitrinadev.com/api/v1/vehicles/<id>/suggestions \
  -H "Authorization: Bearer $VITRINA_KEY"

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
idpathuuid
suggestionIdpathuuid
curl -X POST https://api.vitrinadev.com/api/v1/vehicles/<id>/suggestions/<id>/dismiss \
  -H "Authorization: Bearer $VITRINA_KEY"

Responde: 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"

Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429

GET /vehicles/catalog/models

List canonical models for a brand (autocomplete)

ParámetroEnTipoObligatorioRestricciones
brandquerystringmín. 1, máx. 120
curl https://api.vitrinadev.com/api/v1/vehicles/catalog/models \
  -H "Authorization: Bearer $VITRINA_KEY"

Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429

GET /vehicles/catalog/versions

List versions/trims for a brand+model (autocomplete suggestions)

ParámetroEnTipoObligatorioRestricciones
brandquerystringmín. 1, máx. 120
modelquerystringmín. 1, máx. 120
curl https://api.vitrinadev.com/api/v1/vehicles/catalog/versions \
  -H "Authorization: Bearer $VITRINA_KEY"

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[]
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 '{ … }'

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 '{ … }'

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"

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"

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[]
curl -X PATCH https://api.vitrinadev.com/api/v1/vehicles/estacionamientos \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{ … }'

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
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"

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_idqueryuuid
curl https://api.vitrinadev.com/api/v1/vehicles/kanban \
  -H "Authorization: Bearer $VITRINA_KEY"

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_iduuid
curl -X POST https://api.vitrinadev.com/api/v1/vehicles/parity-report \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{ … }'

Responde: 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.

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 '{ … }'

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"

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 '{ … }'

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". Automotive-vertical only: a workspace on another vertical gets 403 here however its scopes are set.

ParámetroEnTipoObligatorioRestricciones
idsquerystringmín. 1
curl https://api.vitrinadev.com/api/v1/vehicles/publications \
  -H "Authorization: Bearer $VITRINA_KEY"

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 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.

Cuerpo

CampoTipoObligatorioRestricciones
vehicle_idsuuid[]
integration_iduuid
curl -X POST https://api.vitrinadev.com/api/v1/vehicles/publish-bulk \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{ … }'

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_iduuid
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 '{ … }'

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_iduuid
curl -X POST https://api.vitrinadev.com/api/v1/vehicles/source-retirement/report \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{ … }'

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. 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"

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[]
tenenciapropio
curl -X POST https://api.vitrinadev.com/api/v1/vehicles/tenencia \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{ … }'

Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429

En esta página