Skip to main content

Web — Properties (público)

Feed plano de casas publicadas en la web de marketing. Pensado para que otros servicios backend (vivla-api, vivla-mobile, Windmill) lean este endpoint en vez de consultar Webflow. Vive bajo /api/v1/web/ para mantenerse coherente con el resto del módulo web (CMS, locations, amenities) — misma fuente de datos, distinta superficie. La diferencia con /web/properties (sin v1) es que ésa es la cara CMS y devuelve la shape interna PropertyForWeb; esta es la cara pública con shape v1 plano (PropertyPublicDto). Por defecto la lista devuelve sólo filas con property_web_data.status_web = 'listed' — el mismo conjunto que renderiza vivla.com en /listings — pero acepta un filtro ?status= para traer también coming_soon / sold_out (ver Query params). hidden (el estado CMS “no está en web”) nunca es seleccionable ni sale al wire. Los campos editoriales internos (SEO, drafts, audit metadata) se descartan en el wire.

Base path y auth

Base URLs (el backend vive en su propio subdominio; tools.vivla.com a secas es el frontend SPA y no sirve la API): Referencia interactiva OpenAPI/Swagger en <base>/docs (p.ej. api.tools.vivla.com/api/docs, tag Web — Properties (public)), con los schemas exactos de request/response. Auth via el global Auth0OrApiKeyGuard: Sin credenciales válidas → 401 authentication_required / authentication_invalid. El valor de $API_KEY lo comparte el equipo de tools por canal seguro — no está en ningún repo.

Query params

Shape de la lista

Sigue la convención v1 (docs/wiki/api/conventions.mdx):
Ver Paginación cursor para el patrón general.

Shape del item — PropertyPublicDto

Notas de campos

  • description se almacena en property_web_data.description como árbol Lexical i18n. Este endpoint lo serializa a texto plano por locale; los párrafos se separan con \n\n. Si necesitás formato rich-text, abrí ticket — podemos exponer también description_html en una versión futura sin breaking change.
  • hero_image resuelve property_web_data.hero_photo_id. Si no hay heroPhotoId set, cae a la primera foto con web_section='hero' (web_sort_order ascendente). Devuelve null cuando no hay ninguna candidata.
  • gallery trae todas las fotos con is_visible_on_web=true, ordenadas por sort_order. El section permite agrupar client-side (“hero”, “studio”, etc.).
  • status es el property_web_data.status_web: listed, coming_soon o sold_out. hidden nunca sale (ni en lista ni en detalle). En el detalle por id/slug sirve cualquiera de los tres — usa este campo para decidir (una casa referenciada en un lead de MyInvestor puede haber pasado a sold_out).
  • hid es el properties.hid (home id de Firebase/vivla-api). Permite mapear el feed contra los homes del consumidor. null cuando la casa no está sincronizada desde v1.
  • video_url resuelve property_videos visibles en web (is_visible_on_web=true): prefiere el de type='hero', si no hay usa el de menor sort_order. null cuando no hay ninguno.
  • location_text es el property_web_data.location_text (blurb libre de ubicación por locale). Objeto vacío {} cuando no está seteado.
  • type es el del properties.type real (beach/ski). No se hardcodea como en Webflow.
  • url se construye desde el slug (https://www.vivla.com/listings/<slug>). Cambialo en consumidores si el dominio público se mueve.
  • amenities sale de la tabla amenities vía la junction property_web_amenities. El slug es el identificador estable — es lo que hay que usar si tratás las amenities como enum. El label es i18n editable desde el CMS y no es un identificador: puede cambiar sin aviso, y hoy 24 de las 42 entradas todavía traen el inglés en es/fr (vienen del import de Airtable, quedan pendientes de traducir). Ver el vocabulario completo abajo.

Vocabulario de amenities

Éste es el catálogo completo, idéntico en producción y en dev/staging — si tu consumidor mapea slugs a un enum cerrado, ésta es la lista a cubrir. Ojo con los pares casi-duplicados (sea_view/sea_views, fireplace/chimney, air_conditioning/air_conditioner): son entradas distintas del catálogo y las dos del par pueden aparecer en el feed. Un slug nuevo se añade desde el CMS sin desplegar, así que tratá un valor desconocido como no fatal (default genérico) en vez de romper.

Ejemplos

Primera página

Siguiente página

Repetir mientras has_more: true. Cuando has_more: false el next_cursor es null.

Detalle por slug o id

Devuelve un único PropertyPublicDto (sin envelope object: list). El path param se matchea como properties.id cuando es un UUID, si no como property_web_data.slug. Sirve cualquier status_web excepto hidden — para saber el estado real mirá el campo status. 404 resource_missing si no existe ninguna casa con ese slug/id o si está hidden. Es la vía recomendada para resolver el nombre (u otros datos) de la casa de un lead de MyInvestor a partir de su id: GET /api/v1/web/properties/:id.

Filtrar por tipo

Filtrar por estado

Errores

Catálogo completo en Códigos de error.

Patrón de cliente recomendado

Migración desde Webflow

Este endpoint reemplaza la lectura directa de vivla-api contra Webflow (WEBFLOW_URL + Bearer token). Equivalencias para el adaptador del lado consumidor:
El id de cada item deja de ser el id de Webflow y pasa a ser el UUID de tools (properties.id). Hay que avisar a MyInvestor: los ids que guarden/referencien cambian. Para resolver el nombre (u otros datos) de la casa de un lead a partir de su id, GET /api/v1/web/properties/:id (sirve cualquier estado salvo hidden).
Transición Webflow → tools. Mientras dure la migración, un sync diario de Windmill (f/vivla_tools/sync/sync_web_webflow, 07:00 Madrid) mantiene status / precio / coordenadas de tools en paridad con Webflow, de modo que ambas fuentes coinciden hasta que MyInvestor corte del todo con Webflow.

Implementación

  • Controller: apps/backend/src/web/controllers/public-properties.controller.ts (la ruta de detalle detecta UUID vs slug con isUUID)
  • Servicio: WebPropertiesService.listPublic() + getPublicBySlug() + getPublicById() en apps/backend/src/web/services/web-properties.service.ts
  • Serializer: apps/backend/src/web/serializers/property-public.serializer.ts
  • DTOs: apps/backend/src/web/dto/property-public.dto.ts
Hidratación interna reusa WebPropertiesService.hydrateOne() — el mismo que usa el CMS — y luego aplica toPropertyPublicDto() para producir la shape pública. El detalle por slug/id comparte el gate toPublicDetail() que rechaza hidden con 404.