Vitrina API

Stock

El lote publicado de una automotora — qué autos entran, qué campos trae cada uno, cómo se filtra y qué nunca sale al público.

Tres endpoints de lectura sobre una misma cosa: el lote público de una automotora.

curl https://api.vitrinadev.com/api/v1/stock \
  -H "Authorization: Bearer $VITRINA_KEY"

Es la superficie que consume el sitio web de la automotora. No es el inventario: el inventario está en /vehicles, con las patentes, los costos y los pisos de precio, y pide permisos que un sitio web no debería tener.

Qué autos entran al lote

Un auto aparece acá si está activo, no fue borrado, no fue fusionado con otro y no está vendido.

Lo que sorprende de esa lista es lo que no contiene: no hay ninguna condición de "publicado en un portal". El sitio propio de la automotora muestra todo su lote activo, no solo los autos que además están cruzados a Chileautos, Yapo o Mercado Libre. published_at existe como dato y sirve para ordenar, pero no filtra nada.

Los reservados aparecen, con status: "reservado" y su reserved_at, para que puedas pintarles la etiqueta en vez de hacerlos desaparecer. Los vendidos no aparecen, salvo que la automotora haya pedido expresamente que sus ventas recientes se queden a la vista.

El objeto

Así viene un auto, completo, tal como lo devolvió GET /stock/{id}:

{
  "data": {
    "id": "08eecaca-10bc-40b0-a2eb-d0915dba1cc9",
    "make": "Toyota",
    "model": "Corolla",
    "version": "1.8 XEI CVT",
    "year": 2022,
    "title": null,
    "price": { "amount": 13990000, "currency": "CLP" },
    "odometer": { "value": 42500, "unit": "KM" },
    "fuel_type": "Bencina",
    "fuel_label": "Bencina",
    "gear_type": "Automática",
    "gear_label": "Automática",
    "body_style": "Sedán",
    "body_label": "Sedán",
    "color": "Gris",
    "doors": 4,
    "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": "6812d9f0-9bed-44b5-91df-4e5b90941f6b",
      "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": [{ "url": "https://images.example.cl/corolla-2022-1.jpg", "order": 0 }],
    "description": "Mantenciones al día en concesionario.",
    "tags": [],
    "equipment": [],
    "created_at": "2026-09-15T18:34:48.575Z",
    "published_at": null
  }
}

Algunas cosas que no se deducen mirándolo:

Los pares *_type y *_label no son redundantes. El primero es el valor tal como lo guardó la automotora o lo entregó su feed; el segundo es la etiqueta en español lista para mostrar. Cuando coinciden es porque la automotora ya escribió el valor canónico. Si vas a renderizar, usa *_label; si vas a agrupar o comparar, usa *_type.

price.amount es un entero en la moneda de price.currency, sin decimales ni separadores. En pesos chilenos, 13990000 son 13.990.000.

sucursal viene embebida: no tienes que pedir la sucursal por separado para mostrar dónde está el auto. Trae comuna_name ya resuelto desde el código CUT.

photos es una lista ordenada por order; la primera es la portada.

Trampa

No pidas la patente: no está

El objeto público no trae registration_number ni vin, y no hay parámetro que los agregue. Los sitios de clasificados chilenos casi nunca muestran la patente, y una API pública no debe filtrarla. Tampoco salen los precios internos, las etiquetas de fuente, ni de quién es el auto — si estás construyendo algo que necesita eso, el recurso es /vehicles, no éste.

Filtrar

Todos los filtros son parámetros de query y se combinan con Y lógico.

ParámetroQué hace
brand, modelMarca y modelo.
bodyCarrocería.
typeTipo de unidad: Car, Truck, Motorcycle o Boat.
gearbox, fuel_typeTransmisión y combustible.
min_year, max_yearRango de año.
min_price, max_priceRango de precio, en la moneda del lote.
min_odometer, max_odometerRango de kilometraje.
created_from, created_toRango de fecha de carga.
sucursalEl uuid de una sucursal.
statusdisponible, reservado o vendido.
featuredSolo los destacados que eligió la automotora.
curl "https://api.vitrinadev.com/api/v1/stock?body=SUV&max_price=17000000&sort=price_asc" \
  -H "Authorization: Bearer $VITRINA_KEY"

De mis tres autos, ese filtro devolvió uno: el Hyundai Tucson, a 15.200.000. El Mazda CX-5 también es SUV, pero cuesta 18.490.000.

sort acepta price_asc, price_desc, created_asc, created_desc, published_asc, published_desc y featured_desc. Sin sort, el orden es created_desc: lo último cargado primero.

gearbox, fuel_type y body aceptan tanto el código canónico como la etiqueta en español. Probé los dos y devuelven las mismas filas:

curl "https://api.vitrinadev.com/api/v1/stock?gearbox=automatica" -H "Authorization: Bearer $VITRINA_KEY"
curl "https://api.vitrinadev.com/api/v1/stock?gearbox=Autom%C3%A1tica" -H "Authorization: Bearer $VITRINA_KEY"

No pasa lo mismo con type, que sí es un enum cerrado y rechaza lo que no reconoce:

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Request validation failed",
    "details": { "query": [{ "path": "type", "message": "Invalid enum value. Expected 'Car' | 'Truck' | 'Motorcycle' | 'Boat', received 'Bus'", "code": "invalid_enum_value" }] },
    "requestId": "7b0841cd-0345-4cb5-a2fb-3dbb4ad4285c"
  }
}

Trampa

`status=vendido` es gramática válida, no una puerta

El filtro se acepta, pero se aplica encima del lote público, no en lugar de él. Si la automotora no pidió mostrar sus ventas recientes, la consulta no devuelve nada — en mi lote, GET /stock/count?status=vendido devolvió { "data": { "count": 0 } } con tres autos cargados. Qué se muestra lo decide la automotora, nunca quien llama, y por eso tampoco existe un parámetro include_sold.

Contar

Cuando solo necesitas el número — el "12 autos disponibles" de un encabezado —, hay un endpoint que no trae los autos:

curl https://api.vitrinadev.com/api/v1/stock/count \
  -H "Authorization: Bearer $VITRINA_KEY"
{ "data": { "count": 3 } }

Acepta exactamente los mismos filtros que la lista, así que GET /stock/count?status=disponible cuenta lo mismo que contarías paginando — en mi lote, 2 de 3, porque el Tucson estaba reservado.

Paginar

limit y offset, no cursores.

curl "https://api.vitrinadev.com/api/v1/stock?limit=1&offset=1" \
  -H "Authorization: Bearer $VITRINA_KEY"
{
  "data": [ { "make": "Mazda", "model": "CX-5" } ],
  "meta": { "pagination": { "limit": 1, "offset": 1 } }
}

meta.pagination devuelve los valores efectivos, no los que pediste. El limit por defecto es 20 y el máximo es 100:

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Request validation failed",
    "details": { "query": [{ "path": "limit", "message": "Number must be less than or equal to 100", "code": "too_big" }] },
    "requestId": "278945a0-d78d-470c-ad5c-86b029724a96"
  }
}

No viene un total. Si necesitas saber cuántas páginas hay, pide GET /stock/count con los mismos filtros.

Una unidad

curl https://api.vitrinadev.com/api/v1/stock/08eecaca-10bc-40b0-a2eb-d0915dba1cc9 \
  -H "Authorization: Bearer $VITRINA_KEY"

Devuelve el mismo objeto de la lista. Un id que no existe, o uno que existe pero está fuera del lote público, responde igual:

{
  "error": {
    "code": "NOT_FOUND",
    "message": "Vehículo no encontrado",
    "requestId": "e3c36709-4fff-462d-8940-4ff9e8138ce6"
  }
}

Los dos casos son un 404 y no se distinguen desde afuera, a propósito: que un auto exista en el inventario de la automotora no es información pública.

Un id que ni siquiera es un uuid falla antes, con 400:

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Request validation failed",
    "details": { "params": [{ "path": "id", "message": "Invalid uuid", "code": "invalid_string" }] },
    "requestId": "6c0a7f7f-3147-4b29-8ce0-a7548c03aa51"
  }
}

Lo que sigue

En esta página