Public stock
A dealership's published lot: list, count and read one unit. This is the surface a dealer website consumes.
Download the full API pública projection: openapi.json.
Load and keep the lot explains this resource in prose, with runnable examples.
| Method | Path | What it does |
|---|---|---|
GET | /stock | List the public stock (dealer-website read API) |
GET | /stock/{id} | Get one public-stock vehicle |
GET | /stock/count | Count the public stock (same filters as the list) |
GET | /storefront | Storefront bootstrap (channels, published site content + visual identity) |
POST | /storefront/events | Record a storefront traffic/conversion event |
GET | /storefront/trade-in | Buy table for the business’s own quoter (trade-in / buyback) |
POST | /website-leads | Submit a website buyer inquiry (dealer-website write API) |
GET /stock
List the public stock (dealer-website read API)
The tenant’s public lot: active vehicles that are not sold (vendido) nor hidden. reservado units are included with their reserved_at so the website can badge them. Filters cover brand/model/gearbox/fuel_type/body/type/status/featured, year/price/odometer/created ranges and sucursal; sort by price, published_at, created_at or featured_at (default created_desc). limit ≤ 100 (default 20) + offset. The plate is never exposed. Each item carries merch_label + merch_label_es — the dealer’s optional merchandising ribbon (oferta | liquidacion | poco_km | cuatro_x_cuatro | full_equipo | recien_llegado | unico_dueno), null when there is none. A tenant that turns stock_publico.mostrar_vendidos on ALSO gets the units sold in the last 90 days, flagged status: "vendido" with their sold_at, in the list AND in /stock/count; the switch is off by default and there is no caller-side override. Each item also carries is_featured + featured_at — whether the dealer PINNED this unit to the destacados grid of their portada, and when. featured=true narrows the selection to those pinned units (pair it with sort=featured_desc, their pinning order, newest pin first); that selection NEVER includes sold units, whatever mostrar_vendidos says, so a featured car that sells leaves the portada on its own. A tenant that turns stock_publico.visit_coordination_disclosure on (ADR 0095, off by default) additionally gets visit_coordination_required: true on a unit with an ACTIVE, modalidad=virtual consignment contract — a purely logistics fact ("coordinate the visit with the owner"), never the commercial one (tenencia/modalidad are never exposed, on this or any other public surface). The key is ABSENT — never false — on every other unit, opted-in or not.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
brand | query | string | no | mín. 1, máx. 120 |
model | query | string | no | mín. 1, máx. 120 |
gearbox | query | string | no | mín. 1, máx. 60 |
fuel_type | query | string | no | mín. 1, máx. 60 |
body | query | string | no | mín. 1, máx. 60 |
type | query | Car \ | Truck \ | Motorcycle \ |
min_year | query | integer | no | ≥ 1900, ≤ 2100 |
max_year | query | integer | no | ≥ 1900, ≤ 2100 |
min_price | query | number | null | no | ≥ 0 |
max_price | query | number | null | no | ≥ 0 |
min_odometer | query | integer | null | no | ≥ 0 |
max_odometer | query | integer | null | no | ≥ 0 |
created_from | query | string | no | date-time |
created_to | query | string | no | date-time |
sucursal | query | uuid | no | |
status | query | disponible \ | reservado \ | vendido |
featured | query | any | no | |
sort | query | price_asc \ | price_desc \ | published_asc \ |
limit | query | integer | no | ≥ 1, ≤ 100 |
offset | query | integer | null | no | ≥ 0 |
curl https://api.vitrinadev.com/api/v1/stock \
-H "Authorization: Bearer $VITRINA_KEY"Example response (200)
{
"data": [
{
"id": "e1e1e1e1-0000-4000-8000-000000000001",
"make": "Suzuki",
"model": "Swift",
"version": "GL 1.2",
"year": 2021,
"title": null,
"price": {
"amount": 9490000,
"currency": "CLP"
},
"odometer": {
"value": 31400,
"unit": "KM"
},
"fuel_type": null,
"fuel_label": null,
"gear_type": null,
"gear_label": null,
"body_style": null,
"body_label": null,
"color": null,
"doors": null,
"displacement_cc": null,
"type": "Car",
"type_label": "Auto",
"merch_label": null,
"merch_label_es": null,
"is_featured": false,
"featured_at": null,
"listing_type": "Usado",
"status": "disponible",
"reserved_at": null,
"sold_at": null,
"sucursal": {
"id": "b1b1b1b1-0000-4000-8000-000000000001",
"name": "Sucursal Providencia",
"address": null,
"address_street": "Av. Nueva Providencia",
"address_number": "2214",
"address_unit": null,
"comuna_code": "13123",
"comuna_name": "Providencia",
"region_code": "13"
},
"photos": [],
"description": null,
"tags": [],
"equipment": [],
"created_at": "2026-09-20T00:30:01.896Z",
"published_at": null
}
],
"meta": {
"pagination": {
"limit": 20,
"offset": 0
}
}
}Answers: 200 · 400 · 401 · 403 · 404 · 409 · 429
GET /stock/{id}
Get one public-stock vehicle
Missing, foreign-tenant and hidden (inactive / deleted / sold) ids all return the same 404. A tenant with stock_publico.mostrar_vendidos on serves the ficha of a unit sold in the last 90 days instead of 404ing it, so the card on the grid links somewhere; every other gate (inactive / deleted / merged) still 404s. Same visit_coordination_required disclosure rule as the list (ADR 0095): present and true only with the tenant opted into stock_publico.visit_coordination_disclosure AND an ACTIVE, modalidad=virtual consignment contract on this unit; absent otherwise.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
id | path | uuid | yes |
curl https://api.vitrinadev.com/api/v1/stock/<id> \
-H "Authorization: Bearer $VITRINA_KEY"Example response (200)
{
"data": {
"id": "e1e1e1e1-0000-4000-8000-000000000001",
"make": "Suzuki",
"model": "Swift",
"version": "GL 1.2",
"year": 2021,
"title": null,
"price": {
"amount": 9490000,
"currency": "CLP"
},
"odometer": {
"value": 31400,
"unit": "KM"
},
"fuel_type": null,
"fuel_label": null,
"gear_type": null,
"gear_label": null,
"body_style": null,
"body_label": null,
"color": null,
"doors": null,
"displacement_cc": null,
"type": "Car",
"type_label": "Auto",
"merch_label": null,
"merch_label_es": null,
"is_featured": false,
"featured_at": null,
"listing_type": "Usado",
"status": "disponible",
"reserved_at": null,
"sold_at": null,
"sucursal": {
"id": "b1b1b1b1-0000-4000-8000-000000000001",
"name": "Sucursal Providencia",
"address": null,
"address_street": "Av. Nueva Providencia",
"address_number": "2214",
"address_unit": null,
"comuna_code": "13123",
"comuna_name": "Providencia",
"region_code": "13"
},
"photos": [],
"description": null,
"tags": [],
"equipment": [],
"created_at": "2026-09-20T00:30:01.896Z",
"published_at": null
}
}Answers: 200 · 400 · 401 · 403 · 404 · 409 · 429
GET /stock/count
Count the public stock (same filters as the list)
Same filters as GET /stock, minus sort/pagination (unknown keys are simply ignored). Useful for a results-count badge without paging the whole list.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
brand | query | string | no | mín. 1, máx. 120 |
model | query | string | no | mín. 1, máx. 120 |
gearbox | query | string | no | mín. 1, máx. 60 |
fuel_type | query | string | no | mín. 1, máx. 60 |
body | query | string | no | mín. 1, máx. 60 |
type | query | Car \ | Truck \ | Motorcycle \ |
min_year | query | integer | no | ≥ 1900, ≤ 2100 |
max_year | query | integer | no | ≥ 1900, ≤ 2100 |
min_price | query | number | null | no | ≥ 0 |
max_price | query | number | null | no | ≥ 0 |
min_odometer | query | integer | null | no | ≥ 0 |
max_odometer | query | integer | null | no | ≥ 0 |
created_from | query | string | no | date-time |
created_to | query | string | no | date-time |
sucursal | query | uuid | no | |
status | query | disponible \ | reservado \ | vendido |
featured | query | any | no |
curl https://api.vitrinadev.com/api/v1/stock/count \
-H "Authorization: Bearer $VITRINA_KEY"Example response (200)
{
"data": {
"count": 7
}
}Answers: 200 · 400 · 401 · 403 · 404 · 409 · 429
GET /storefront
Storefront bootstrap (channels, published site content + visual identity)
The dealer’s public identity and live contact channels: trading name, whether WhatsApp is connected (and on which number), whether the embeddable webchat is configured, and the active sucursales. Lets a Vitrina-built website decide which CTAs to render — a dealer who connects WhatsApp sees the button appear with no config edit and no redeploy. Readable with the narrow stock:read scope; returns only facts a storefront already publishes to the world (no secrets).
Tenants holding the dealer_site add-on additionally get sitio — the PUBLISHED site snapshot, never the draft — whose sitio.theme carries the visual identity: primary_color (#rrggbb), base (light|dark), display_font/body_font (from the closed whitelist saira|ibmPlexSans|poppins|nunitoSans|archivo|montserrat), radius (sharp|rounded) and density (compact|roomy), plus the OPTIONAL skin (classic|luxe) — which component tree the renderer draws the same content with. skin is the one theme field that may be absent: absent means classic, the look every site published before the field existed already has, so a stored value the server does not recognise drops the skin alone and leaves the rest of the theme intact — plus branding (logo_url, and primary_color as the fallback for a tenant that has not published a theme). These are RAW identity inputs, not a derived palette: the renderer derives the token set from the single brand colour, contrast-adjusting the text/accent token against the active base so any valid hex yields a WCAG-AA-readable page (vitrina-template ADR 0005). Server-side validation is therefore strict SHAPE validation (hex format, whitelisted font, closed enums) — colours are never silently repainted. Also carries analytics (ga4_measurement_id, meta_pixel_id, ticket 86e2rwqy9) — the dealer’s own GA4/Meta Pixel ids, null per field when unconfigured, so the template loads NO third-party script until a valid id is set.
Entitled tenants ALSO get the rest of the dealer’s public identity (ticket 86e2t94ee), so a website can render itself end to end from this one response with no per-client config file: content (tagline, hero_image_url, city, founded_year, show_name_with_logo, partners[], stats[], and google_rating — \{ rating, count, url, as_of \} or null: the dealer-declared Google score with the date it was last verified; the template hides it once as_of is older than 120 days), contact (sales and optional service channels with label/email/phone_display/phone_e164/whatsapp_message, plus address and maps_url), social (instagram/facebook/linkedin/tiktok/x/youtube), features (blog, services, consignment, catalog_filters, financing, branches, chat), trust (financing, card_payment, trade_in, insurance, digital_transfer) and financing (annualRate, monthlyRatePct, caeRefPct, terms[], minDownPct, defaultDownPct, defaultTermMonths, legalNote, highlight — annualRate/monthlyRatePct are the TAN/TEM rate pair, at least one required; caeRefPct is the CAE referencial shown with equal prominence per Art. 17 G, Ley 19.496). content/contact/features/trust are LIVE tenant config — an edit reaches the site at the next bootstrap window with no Publicar — while social and financing are projected from the PUBLISHED Sitio snapshot, where the dealer already edits them. Unset feature flags take the platform defaults; unset trust seals are false, because a seal is a promise the dealership makes and is never inferred. The webchat block additionally carries welcome_message and position (br|bl) from the same settings.webchat key GET /widget/config serves — webchat.enabled stays UNGATED (it is a channel fact), only its appearance rides the add-on gate.
Also carries publishable_key (ClickUp 86e2t94e1): a browser-safe pk_ widget key (ADR 0033) DERIVED on every call from the workspace’s registered storefront domains (GET /tenant/storefront-domains), origin-locked to apex + www. of each, plus the Vitrina-hosted subdomain. Nothing is stored: adding or removing a domain changes the next value, so a new domain reaches the site at its next bootstrap window with no re-mint and no redeploy. null when the workspace has registered no domain — a key with an empty allow-list admits nothing, so returning one would look like success and behave like a 403. POST /publishable-keys is unchanged and remains the surface for arbitrary origins and isolated keys. It is NOT gated on dealer_site: what gates it is having a domain.
Also carries turnstile_site_key: the Cloudflare Turnstile site key every public form on the site must render, or null for "render no widget". Resolved from the same pair the intake routes verify with (POST /widget/consignments, /website-leads, /widget/appointments), so the challenge the page shows and the check the server runs cannot drift — a site that renders no widget against a verifying server has every submission refused. Mirrors GET /widget/config.turnstileSiteKey.
Also carries vende_tu_auto (#5872): the «Vende tu auto» consignment landing view — path, the Oferta headline and claims, three steps, the Condiciones hint, rating and address — or null while the landing is off. Same view as GET /widget/consignments/landing.
Without the add-on, or before the first Publicar, sitio/branding/analytics/content/contact/social/features/trust/financing are null and theme is absent, and the storefront falls back to its built-in defaults.
curl https://api.vitrinadev.com/api/v1/storefront \
-H "Authorization: Bearer $VITRINA_KEY"Example response (200)
{
"data": {
"name": "Autos del Sur",
"whatsapp": {
"enabled": true,
"phone_e164": null
},
"webchat": {
"enabled": false,
"welcome_message": null,
"position": null
},
"sucursales": [
{
"id": "b1b1b1b1-0000-4000-8000-000000000001",
"name": "Sucursal Providencia",
"address": null,
"address_street": "Av. Nueva Providencia",
"address_number": "2214",
"address_unit": null,
"comuna_code": "13123",
"comuna_name": "Providencia",
"region_code": "13",
"coordinates": null
}
],
"sitio": null,
"branding": null,
"analytics": null,
"publishable_key": null,
"turnstile_site_key": null,
"vende_tu_auto": null,
"content": null,
"contact": null,
"social": null,
"features": null,
"trust": null,
"financing": null
}
}Answers: 200 · 400 · 401 · 403 · 404 · 409 · 429
POST /storefront/events
Record a storefront traffic/conversion event
Capture one page_view / vehicle_view / lead_submit signal from the tenant's own Vitrina-built storefront. Append-only, no person data — the optional dedup_key is an opaque session-ish hint, hashed server-side before storage. Requires the narrow, write-only storefront_events:write scope. A bogus or foreign-tenant vehicle_id is refused by the underlying composite FK and acked identically to success (no enumeration oracle). Feeds GET /insights/storefront.
Body
| Field | Type | Required | Constraints |
|---|---|---|---|
event | page_view \ | vehicle_view \ | lead_submit |
vehicle_id | uuid | — | |
dedup_key | string | — | mín. 1, máx. 200 |
curl -X POST https://api.vitrinadev.com/api/v1/storefront/events \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"event": "vehicle_view",
"vehicle_id": "e1e1e1e1-0000-4000-8000-000000000001",
"dedup_key": "sess_8f2c1a"
}'Example response (201)
{
"data": {
"status": "recorded"
}
}Answers: 201 · 400 · 401 · 403 · 404 · 409 · 429
GET /storefront/trade-in
Buy table for the business’s own quoter (trade-in / buyback)
What the business pays at most for each used model, so its own website quoter reads the same table the agent quotes from instead of a hardcoded copy. Per model: top (flawless, «Impecable»), used (normal wear, «Con uso»), direct_purchase (false = taken only as part of the payment for another item, never bought outright) and min_battery_pct (a known battery under it is not bought; null = no minimum) and low_battery_factor (a known battery under perfect_battery_pct caps the price at used × low_battery_factor; null = used). Plus not_bought (models never bought), currency and perfect_battery_pct (from this battery health a unit with nothing else wrong counts as flawless). Readable with the storefront’s stock:read; the browser twin is GET /widget/trade-in with the site’s pk_ key. Cache-Control: private, max-age=300 + a strong ETag (send If-None-Match for a 304). 404 when the workspace keeps no buy table (PUT /tenant/settings trade_in).
curl https://api.vitrinadev.com/api/v1/storefront/trade-in \
-H "Authorization: Bearer $VITRINA_KEY"Example response (200)
{
"currency": "CLP",
"perfect_battery_pct": 90,
"models": [
{
"model": "iPhone 15",
"top": 350000,
"used": 320000,
"direct_purchase": false,
"min_battery_pct": null,
"low_battery_factor": null
},
{
"model": "iPhone 16 Pro",
"top": 600000,
"used": 570000,
"direct_purchase": true,
"min_battery_pct": null,
"low_battery_factor": 0.85
}
],
"not_bought": [
"iPhone 13 mini",
"iPhone 16e"
]
}Answers: 200 · 400 · 401 · 403 · 404 · 409 · 429
POST /website-leads
Submit a website buyer inquiry (dealer-website write API)
Creates a CRM lead with website origin, attaching the inquired Vehículo when vehicle_id is given. A proyecto form (Módulo Proyectos) sends project_id and/or unit_model_id (a tipología names its proyecto): the lead lands on the proyecto, is routed to its ejecutivos, and message becomes optional. attribution carries the visitor's first_touch and last_touch (utm_*, gclid, fbclid, anonymous_id); both are kept on the lead. value_amount is the lead's value; quote_amount is a figure the site quoted that is not the deal (a trade-in quote): it only fills the first-touch template's amount and the inbox card when value_amount is absent, and is never stored as the lead's value. Requires the write-only leads:intake scope (never leads:write). Returns a minimized \{ status \} ack — received on create, duplicate on dedup — and echoes no internal ids. Rate-limited like every authenticated endpoint; a hidden hp_website honeypot field silently drops spam.
Body
| Field | Type | Required | Constraints |
|---|---|---|---|
name | string | — | mín. 1, máx. 200 |
email | string | — | email, máx. 254 |
phone | string | — | mín. 5, máx. 40 |
message | string | — | mín. 1, máx. 5000 |
vehicle_id | uuid | — | |
project_id | uuid | — | |
unit_model_id | uuid | — | |
attribution | object | — | |
pipeline_id | uuid | — | |
stage_id | uuid | — | |
value_amount | number | — | ≥ 0, ≤ 1000000000000 |
value_currency | string | — | mín. 3, máx. 3 |
quote_amount | number | — | ≥ 0, ≤ 1000000000000 |
item_label | string | — | mín. 1, máx. 120 |
source_ref | string | — | mín. 1, máx. 200 |
fields | object[] | — | |
form_title | string | — | máx. 80 |
highlight | object | — | |
channel_code | string | — | mín. 1, máx. 40 |
intent | buy \ | sell \ | financing \ |
hp_website | string | — | máx. 500 |
turnstile_token | string | — | máx. 4096 |
consent | object | — |
curl -X POST https://api.vitrinadev.com/api/v1/website-leads \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Camila Reyes",
"email": "[email protected]",
"phone": "+56911112222",
"message": "Hola, ¿el Corolla 2022 sigue disponible?",
"vehicle_id": "e1e1e1e1-0000-4000-8000-000000000001",
"intent": "buy"
}'Example response (200)
{
"data": {
"status": "duplicate"
}
}Answers: 200 · 201 · 400 · 401 · 403 · 404 · 409 · 429