# Your website with a publishable key

List properties with filters, receive forms and visit requests, book appointments and build a guided finder.

Guide Publishable key No approval

## Create a publishable key

In ByDomu, Settings › API Keys › New key › Publishable. It works on any paid plan, with no request, and only reaches public data: `portal:read`, `leads:write`, `booking:write`, `finder:write`, `events:write`.

It can live in the browser (the API answers with open CORS), but all your visitors share its limit: 300 requests and 60 writes per minute. For listings, fetch from your server and cache; leave only the forms in the browser. A secret key in the browser gets 403 SECRET_KEY_IN_BROWSER.

What for Endpoint

List with filters `GET /properties`

Detail `GET /properties/{id}`

Featured `GET /properties/featured`

Filter options `GET /filters`

Agents `GET /agents`

Portal settings `GET /portal`

Contact form `POST /leads`

Visit request `POST /leads/schedule`

Online booking `POST /booking/{slug}`

Guided finder `POST /finder`

Portal analytics `POST /events`

## List properties with filters

Only active properties published on the portal come back, never internal notes or custom fields. When the location is approximate, address and zipCode arrive as null and coordinates are rounded.

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"] } }
```

**Careful with booleans.** On this endpoint the has* filters and includeTotal count as true with any non-empty value, even “false”: to skip a filter, don’t send it. To walk everything without counting the total, paginate with cursor (the id of the last property).

## Receive the contact form

It creates or updates the contact with its listing and UTMs, and adds to the portal analytics. A 400 says in words which field failed: show it on the form.

```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 } }
```

If you listen to webhooks: the form fires lead.submitted, but today that event only carries the API response; the person’s data arrives in contact.created.

## Visits and bookings

`POST /leads/schedule` records a visit request with a preferred date and time slot; it doesn’t create the appointment: the agency confirms it later.

```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
  }),
});
```

To actually book, use each agent’s online booking: their free slots and the appointment in a free slot (it’s CONFIRMED if the page auto-confirms; otherwise SCHEDULED). These three responses come double-wrapped: `{ 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": … } } }
```

## Build a guided finder

Ask step by step what the person is looking for and send it all at the end: it’s saved as a contact and the response carries up to 12 matching properties, best first, with the score (0-100) and the breakdown.

```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" }], … }] } }
```

## Count visits and clicks

With events:write, your site feeds the portal analytics in ByDomu. The response is immediate and doesn’t confirm the event was stored.

```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 } }
```

## If something fails

- **400** A field doesn’t match the schema (name too short, invalid phone, pageSize over 48, month outside 0-11).
- **403** You used a publishable key on a private endpoint (the message lists what it can use) or a secret key from a browser.
- **404** The property or booking slug doesn’t belong to your agency.
- **429** Your visitors used up the key’s limit: serve listings from a cache.
