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
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):
Shape del item — PropertyPublicDto
Notas de campos
descriptionse almacena enproperty_web_data.descriptioncomo á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éndescription_htmlen una versión futura sin breaking change.hero_imageresuelveproperty_web_data.hero_photo_id. Si no hay heroPhotoId set, cae a la primera foto conweb_section='hero'(web_sort_orderascendente). Devuelvenullcuando no hay ninguna candidata.gallerytrae todas las fotos conis_visible_on_web=true, ordenadas porsort_order. Elsectionpermite agrupar client-side (“hero”, “studio”, etc.).statuses elproperty_web_data.status_web:listed,coming_soonosold_out.hiddennunca 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 asold_out).hides elproperties.hid(home id de Firebase/vivla-api). Permite mapear el feed contra los homes del consumidor.nullcuando la casa no está sincronizada desde v1.video_urlresuelveproperty_videosvisibles en web (is_visible_on_web=true): prefiere el detype='hero', si no hay usa el de menorsort_order.nullcuando no hay ninguno.location_textes elproperty_web_data.location_text(blurb libre de ubicación por locale). Objeto vacío{}cuando no está seteado.typees el delproperties.typereal (beach/ski). No se hardcodea como en Webflow.urlse construye desde elslug(https://www.vivla.com/listings/<slug>). Cambialo en consumidores si el dominio público se mueve.amenitiessale de la tablaamenitiesvía la junctionproperty_web_amenities. Elsluges el identificador estable — es lo que hay que usar si tratás las amenities como enum. Ellabeles 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 enes/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
has_more: true. Cuando has_more: false el next_cursor es null.
Detalle por slug o id
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 devivla-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 conisUUID) - Servicio:
WebPropertiesService.listPublic()+getPublicBySlug()+getPublicById()enapps/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
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.