VitrinaAPI

Public stock

A dealership's published lot: list, count and read one unit. This is the surface a dealer website consumes.

Beta
This may change at any time, with a changelog entry and a notice to recent callers — see versioning.

Download the full API pública projection: openapi.json.

Load and keep the lot explains this resource in prose, with runnable examples.

MethodPathWhat it does
GET/stockList the public stock (dealer-website read API)
GET/stock/{id}Get one public-stock vehicle
GET/stock/countCount the public stock (same filters as the list)
GET/storefrontStorefront bootstrap (channels, published site content + visual identity)
POST/storefront/eventsRecord a storefront traffic/conversion event
GET/storefront/trade-inBuy table for the business’s own quoter (trade-in / buyback)
POST/website-leadsSubmit 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.

ParameterInTypeRequiredConstraints
brandquerystringnomín. 1, máx. 120
modelquerystringnomín. 1, máx. 120
gearboxquerystringnomín. 1, máx. 60
fuel_typequerystringnomín. 1, máx. 60
bodyquerystringnomín. 1, máx. 60
typequeryCar \Truck \Motorcycle \
min_yearqueryintegerno≥ 1900, ≤ 2100
max_yearqueryintegerno≥ 1900, ≤ 2100
min_pricequerynumber | nullno≥ 0
max_pricequerynumber | nullno≥ 0
min_odometerqueryinteger | nullno≥ 0
max_odometerqueryinteger | nullno≥ 0
created_fromquerystringnodate-time
created_toquerystringnodate-time
sucursalqueryuuidno
statusquerydisponible \reservado \vendido
featuredqueryanyno
sortqueryprice_asc \price_desc \published_asc \
limitqueryintegerno≥ 1, ≤ 100
offsetqueryinteger | nullno≥ 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.

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

ParameterInTypeRequiredConstraints
brandquerystringnomín. 1, máx. 120
modelquerystringnomín. 1, máx. 120
gearboxquerystringnomín. 1, máx. 60
fuel_typequerystringnomín. 1, máx. 60
bodyquerystringnomín. 1, máx. 60
typequeryCar \Truck \Motorcycle \
min_yearqueryintegerno≥ 1900, ≤ 2100
max_yearqueryintegerno≥ 1900, ≤ 2100
min_pricequerynumber | nullno≥ 0
max_pricequerynumber | nullno≥ 0
min_odometerqueryinteger | nullno≥ 0
max_odometerqueryinteger | nullno≥ 0
created_fromquerystringnodate-time
created_toquerystringnodate-time
sucursalqueryuuidno
statusquerydisponible \reservado \vendido
featuredqueryanyno
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

FieldTypeRequiredConstraints
eventpage_view \vehicle_view \lead_submit
vehicle_iduuid—
dedup_keystring—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

FieldTypeRequiredConstraints
namestring—mín. 1, máx. 200
emailstring—email, máx. 254
phonestring—mín. 5, máx. 40
messagestring—mín. 1, máx. 5000
vehicle_iduuid—
project_iduuid—
unit_model_iduuid—
attributionobject—
pipeline_iduuid—
stage_iduuid—
value_amountnumber—≥ 0, ≤ 1000000000000
value_currencystring—mín. 3, máx. 3
quote_amountnumber—≥ 0, ≤ 1000000000000
item_labelstring—mín. 1, máx. 120
source_refstring—mín. 1, máx. 200
fieldsobject[]—
form_titlestring—máx. 80
highlightobject—
channel_codestring—mín. 1, máx. 40
intentbuy \sell \financing \
hp_websitestring—máx. 500
turnstile_tokenstring—máx. 4096
consentobject—
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

On this page