# Tu página web con clave publicable

Lista propiedades con filtros, recibe formularios y visitas, agenda citas y arma un buscador guiado.

Guía Clave publicable Sin aprobación

## Crea una clave publicable

En ByDomu, Configuración › API Keys › Nueva clave › Publicable. Sirve con cualquier plan pagado, sin solicitud, y solo alcanza lo público: `portal:read`, `leads:write`, `booking:write`, `finder:write`, `events:write`.

Puede ir en el navegador (la API responde con CORS abierto), pero todos tus visitantes comparten su límite: 300 peticiones y 60 escrituras por minuto. Para el inventario, pide desde tu servidor y guarda en caché; deja en el navegador solo los formularios. Una clave secreta en el navegador responde 403 SECRET_KEY_IN_BROWSER.

Para qué Endpoint

Listado con filtros `GET /properties`

Ficha `GET /properties/{id}`

Destacadas `GET /properties/featured`

Opciones de los filtros `GET /filters`

Asesores `GET /agents`

Configuración del portal `GET /portal`

Formulario de contacto `POST /leads`

Solicitud de visita `POST /leads/schedule`

Agenda en línea `POST /booking/{slug}`

Buscador guiado `POST /finder`

Analítica del portal `POST /events`

## Lista propiedades con filtros

Solo salen las propiedades activas y publicadas en el portal, nunca las notas internas ni los campos personalizados. Si la ubicación es aproximada, address y zipCode llegan en null y las coordenadas redondeadas.

JavaScript:

```js
// Clave publicable: puede ir en el navegador. Mejor aún, en tu servidor con caché.
const API = "https://api.bydomu.com/v1";
const KEY = "domu_pk_live_XXXXXXXX";

const params = new URLSearchParams({
  transactionType: "RENT",   // SALE | RENT | SALE_OR_RENT
  type: "APARTMENT",
  city: "Medellín",          // coincidencia parcial
  priceMax: "4000000",
  bedroomsMin: "2",
  hasPetFriendly: "1",       // los has* se mandan solo cuando aplican
  sortBy: "price",           // price | createdAt | area
  sortOrder: "asc",
  pageSize: "12",            // máximo 48
});
const res = await fetch(`${API}/properties?${params}`, { headers: { "X-API-Key": KEY } });
const { data, meta } = await res.json(); // meta: { total, page, pageSize, totalPages }
```

cURL:

```bash
curl https://api.bydomu.com/v1/filters -H "X-API-Key: $BYDOMU_PK"
# { "data": { "cities": ["Bogotá", "Medellín"], "propertyTypes": ["APARTMENT", "HOUSE"],
#             "transactionTypes": ["SALE", "RENT"] } }
```

**Ojo con los booleanos.** En este endpoint los has* e includeTotal cuentan como verdaderos con cualquier valor no vacío, incluso «false»: para no filtrar, no los mandes. Para recorrer todo sin contar el total, pagina con cursor (el id de la última propiedad).

## Recibe el formulario de contacto

Crea o actualiza el contacto con su propiedad y sus UTM, y suma la analítica del portal. Un 400 trae en palabras qué campo falló: muéstralo en el formulario.

```js
await fetch("https://api.bydomu.com/v1/leads", {
  method: "POST",
  headers: { "X-API-Key": KEY, "Content-Type": "application/json" },
  body: JSON.stringify({
    firstName: "Laura",                 // obligatorio (2-100)
    phone: "+573000000001",             // obligatorio (7-20)
    email: "laura@example.com",
    message: "¿Sigue disponible?",
    propertyId: "clx1prop001",          // en la ficha de una propiedad
    interestedIn: "RENT",               // SALE | RENT | SALE_OR_RENT
    utmSource: new URLSearchParams(location.search).get("utm_source") ?? undefined,
  }),
});
// 201 → { "data": { "success": true } }
```

Si escuchas webhooks: el formulario dispara lead.submitted, pero hoy ese evento solo trae la respuesta de la API; los datos de la persona llegan en contact.created.

## Visitas y citas

`POST /leads/schedule` registra una solicitud de visita con fecha y franja preferidas; no crea la cita: la inmobiliaria la confirma después.

```js
await fetch("https://api.bydomu.com/v1/leads/schedule", {
  method: "POST",
  headers: { "X-API-Key": KEY, "Content-Type": "application/json" },
  body: JSON.stringify({
    firstName: "Laura",
    phone: "+573000000001",
    propertyId: "clx1prop001",          // obligatorio
    preferredDate: "2026-10-04",
    preferredTime: "morning",           // morning | afternoon | evening
  }),
});
```

Para agendar de verdad, usa la agenda en línea de cada asesor: sus horas libres y la cita en un espacio libre (queda CONFIRMED si la página confirma sola; si no, SCHEDULED). Estas tres respuestas vienen doblemente envueltas: `{ data: { data: … } }`.

```js
// 1. Asesores con agenda en línea (cada uno con su slug)
const agents = await get("/booking/agents");
// 2. Horas libres de un mes. OJO: month va de 0 (enero) a 11 (diciembre)
const { data: { data: availability } } = await get("/booking/carlos-rodriguez/availability?month=9&year=2026");
// availability = { configured: true, availableDates: [{ date: "2026-10-05", slots: ["09:00", "10:00"] }] }
// 3. Agendar en un espacio libre
const res = await post("/booking/carlos-rodriguez", {
  firstName: "Laura",
  phone: "+573000000001",
  date: "2026-10-05",
  startTime: "09:00",
  type: "SHOWING",
  propertyId: "clx1prop001",
});
// 201 → { "data": { "data": { "appointmentId": "…", "status": "CONFIRMED" | "SCHEDULED", "ownerNotified": … } } }
```

## Arma un buscador guiado

Pregunta paso a paso qué busca la persona y manda todo junto al final: queda como contacto y la respuesta trae hasta 12 propiedades que encajan, de mejor a peor, con el puntaje (0-100) y el detalle del cálculo.

```js
const res = await post("/finder", {
  transactionType: "SALE",            // SALE | RENT
  propertyType: "APARTMENT",
  city: "Bogotá",
  neighborhood: "Chapinero",
  budgetMin: 250000000,
  budgetMax: 450000000,
  minBedrooms: 2,
  amenities: ["parking", "gym"],
  firstName: "Laura",
  phone: "+573000000001",
  subscribedToAlerts: true,
});
// 201 → { "data": { "contact": { "id": "…" }, "total": 7,
//   "properties": [{ "id": "…", "title": "…", "price": 450000000, "matchScore": 87,
//                    "breakdown": [{ "criterio": "presupuesto", "puntos": 30, "estado": "match" }], … }] } }
```

## Cuenta visitas y clics

Con events:write, tu web alimenta la analítica del portal en ByDomu. La respuesta es inmediata y no confirma que el evento se haya guardado.

```js
await post("/events", { type: "PROPERTY_VIEW", propertyId: "clx1prop001", referrer: document.referrer });
// PAGE_VIEW | PROPERTY_VIEW | SEARCH | LEAD_FORM | WHATSAPP_CLICK | CALL_CLICK
// SCHEDULE_CLICK | SHARE_CLICK | FINDER_SUBMIT  → 200 { "data": { "success": true } }
```

## Si algo falla

- **400** Un campo no cumple el esquema (nombre muy corto, teléfono inválido, pageSize mayor a 48, month fuera de 0-11).
- **403** Usaste una publicable en un endpoint privado (el mensaje dice cuáles puede usar) o una secreta desde el navegador.
- **404** La propiedad o el slug de agenda no es de tu inmobiliaria.
- **429** Tus visitantes agotaron el límite de la clave: sirve el inventario desde una caché.
