Your website with a publishable key
List properties with filters, receive forms and visit requests, book appointments and build a guided finder.
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.
GET /propertiesGET /properties/{id}GET /properties/featuredGET /filtersGET /agentsGET /portalPOST /leadsPOST /leads/schedulePOST /booking/{slug}POST /finderPOST /eventsList 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.
// 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 }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.
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: "[email protected]",
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.
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: … } }.
// 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.
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.
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
- 400A field doesn’t match the schema (name too short, invalid phone, pageSize over 48, month outside 0-11).
- 403You used a publishable key on a private endpoint (the message lists what it can use) or a secret key from a browser.
- 404The property or booking slug doesn’t belong to your agency.
- 429Your visitors used up the key’s limit: serve listings from a cache.