1. Visión general
EstateWave es la plataforma central de inventario. Cada usuario de EstateWave puede conectar una o varias webs propias (por ejemplo abracadabratulum.com, nombrebroker.com). Cada web queda asociada a ese usuario.
La integración permite tres cosas:
- Web → EstateWave: tu web crea y actualiza propiedades en EstateWave llamando a esta API REST.
- EstateWave → Web: EstateWave notifica a tu web (webhook firmado) cuando el agente publica o cambia algo desde EstateWave.
- Sincronización continua: precio, estado, disponibilidad y campos comunes se mantienen alineados, con gestión de conflictos para los campos sensibles.
2. Conceptos clave
| Concepto | Descripción |
|---|---|
sync_global_id | Identificador global único (UUID) e inmutable de cada propiedad sincronizada. Es la referencia canónica entre sistemas. Nunca cambia. No uses el título, el slug ni la URL como referencia. |
external_property_id | El id que tu web usa internamente. EstateWave lo guarda en el mapeo; puedes referenciar propiedades por este id (dentro de tu conexión) además de por sync_global_id. |
estatewave_property_id | El id interno de la propiedad en EstateWave. Te lo devolvemos al crear/actualizar. |
source_system | Origen del registro de sincronización: website (lo enviaste tú) o estatewave. |
sync_status | Estado del mapeo: activepending_syncconflictinactiveblockederror |
| baseline | Último valor "acordado" de los campos sensibles. Se usa para detectar conflictos (ver §10). |
3. Obtener credenciales
El dueño de la web entra a EstateWave → Webs conectadas → + Conectar web, indica el nombre y la URL de la web. EstateWave genera:
api_key— identificador público de la conexión (formatoewk_…).api_secret— secreto (formatoews_…). Se muestra una sola vez.
Cada web conectada pertenece a un usuario. La API solo permitirá tocar propiedades de ese usuario (ver §12).
4. Autenticación
Toda petición debe incluir la cabecera de la API Key y una forma de prueba del secreto:
| Cabecera | Obligatoria | Descripción |
|---|---|---|
X-EW-Key | Sí | Tu api_key. |
X-EW-Signature | Recomendada | HMAC-SHA256 (hex) del cuerpo crudo de la petición, usando tu api_secret como clave. Para peticiones GET (sin cuerpo), firma la cadena vacía "". |
X-EW-Secret | Alternativa | Tu api_secret en claro. Más simple, pero usa siempre HTTPS. Úsala solo si no puedes firmar. |
Se aceptan también los alias X-EstateWave-Key, X-EstateWave-Signature y X-EstateWave-Secret.
Firmar la petición (Node.js)
// Helper reutilizable: firma el cuerpo con HMAC-SHA256 y llama a la API. import crypto from 'node:crypto'; const BASE = 'https://estatewave.xyz/api/integrations'; const KEY = process.env.EW_KEY; // ewk_... const SECRET = process.env.EW_SECRET; // ews_... async function ewPost(path, payload) { const body = JSON.stringify(payload); const signature = crypto.createHmac('sha256', SECRET).update(body, 'utf8').digest('hex'); const res = await fetch(BASE + path, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-EW-Key': KEY, 'X-EW-Signature': signature, }, body, }); const json = await res.json(); if (!res.ok) throw new Error(json.error?.message || ('HTTP ' + res.status)); return json.data; }
body (no re-serialices después). Para GET, la firma es la del cuerpo vacío.5. Base URL y formato de respuestas
Base: https://estatewave.xyz/api/integrations
Todas las respuestas exitosas vienen envueltas en data; los errores en error:
// Éxito { "data": { "sync_global_id": "7c1f…", "estatewave_property_id": "a0…" } } // Error { "error": { "code": "BAD_AUTH", "message": "Invalid signature or secret." } }
Códigos de error
| HTTP | code | Significado |
|---|---|---|
| 400 | VALIDATION / BAD_JSON | Falta un campo requerido o el JSON es inválido. |
| 401 | NO_KEY / BAD_KEY / BAD_AUTH | Falta la key, key inválida, o firma/secreto incorrectos. |
| 403 | INACTIVE / SYNC_DISABLED / BAD_ORIGIN | Conexión inactiva, sync pausado, o dominio no autorizado. |
| 404 | NOT_FOUND | La propiedad no existe o no pertenece a tu conexión. |
| 413 | TOO_LARGE | Cuerpo demasiado grande (límite 6 MB). |
| 429 | RATE_LIMITED | Más de 120 peticiones por minuto por API Key. |
6. Endpoints
Crea (o registra) una propiedad en EstateWave desde tu web. Requiere al menos title y price. La propiedad queda con source = external. Si no envías sync_global_id, EstateWave genera uno y te lo devuelve — guárdalo junto a tu registro.
# curl (autenticación simple con secreto) curl -X POST https://estatewave.xyz/api/integrations/properties/create \ -H "X-EW-Key: ewk_xxx" \ -H "X-EW-Secret: ews_xxx" \ -H "Content-Type: application/json" \ -d '{ "external_property_id": "MLS-1024", "title": "Departamento frente al mar", "description": "Vista a la selva, alberca privada…", "price": 4500000, "currency": "MXN", "city": "Tulum", "neighborhood": "Aldea Zamá", "property_type": "Departamento", "bedrooms": 2, "bathrooms": 2, "built_area": 95, "lot_area": 120, "availability": "available", "amenities": ["Alberca", "Pet-friendly"], "images": ["https://tu-web.com/img1.jpg"], "slug": "depto-frente-al-mar" }'
Respuesta 201: { data: { sync_global_id, estatewave_property_id, property } }
Actualiza una propiedad existente. Identifícala con sync_global_id (recomendado), o external_property_id, o estatewave_property_id. Aplica los campos comunes; los campos sensibles pueden generar conflicto (ver §10).
// con el helper ewPost() de §4 const r = await ewPost('/properties/update', { sync_global_id: '7c1f…', price: 3990000, title: 'Departamento frente al mar (rebajado)' }); // r.action → 'property.updated' | 'property.price_changed' | 'sync.conflict' // r.conflict → true/false ; r.conflicts → [ { field, estatewave_value, external_value } ]
Respuesta 200: { data: { sync_global_id, estatewave_property_id, action, conflict, conflicts, property } }
Cambia status y/o availability (baja lógica). Nunca borra físicamente. Útil para marcar vendido, rentado, despublicado, reactivado.
await ewPost('/properties/status', { sync_global_id: '7c1f…', availability: 'sold' });
await ewPost('/properties/status', { sync_global_id: '7c1f…', status: 'inactive' }); // despublicar
Baja lógica (soft delete). Marca la propiedad como inactive en EstateWave y el mapeo como inactive. No se elimina físicamente.
await ewPost('/properties/delete', { sync_global_id: '7c1f…' });
Lee una propiedad (para que tu web "jale" el estado actual desde EstateWave). Auth por cabecera; la firma es la del cuerpo vacío.
const sig = crypto.createHmac('sha256', SECRET).update('').digest('hex');
const res = await fetch(BASE + '/property/7c1f…', {
headers: { 'X-EW-Key': KEY, 'X-EW-Signature': sig }
});
// { data: { property: { sync_global_id, title, price, availability, relationship_to_user, … } } }
Resuelve tanto las propiedades propias del usuario como sus favoritas activas y públicas de otros brokers (para refrescar cualquiera de las dos por id). La respuesta incluye los campos de relación (ver abajo).
Inventario activo completo del usuario de la conexión — pensado para que tu web reconstruya su listado con EstateWave como fuente de verdad. Devuelve, en un solo array:
- Sus propiedades propias activas y públicas (
relationship_to_user: "owner"). - Sus favoritas activas y públicas de otros brokers (
relationship_to_user: "favorite"). Aparecen en su web sin cambiar la titularidad dentro de EstateWave.
const sig = crypto.createHmac('sha256', SECRET).update('').digest('hex');
const res = await fetch(BASE + '/inventory', {
headers: { 'X-EW-Key': KEY, 'X-EW-Signature': sig }
});
// {
// data: {
// inventory: [ { sync_global_id, title, price, …, relationship_to_user, is_own_property,
// is_favorite, owner_user_id, display_user_id, favorite_created_at, sync_status } ],
// count, own_count, favorites_count, source_of_truth: "active_full_inventory", generated_at
// }
// }
| Campo de relación | Tipo | Notas |
|---|---|---|
relationship_to_user | enum | owner (propia) · favorite (favorita de otro broker). |
is_own_property | bool | true si el usuario de la conexión es el dueño. |
is_favorite | bool | true si es una favorita de otro broker. |
owner_user_id | string | Broker propietario real (no cambia al marcar favorita). |
display_user_id | string | Usuario en cuya web se muestra (el de la conexión). |
favorite_created_at | iso | Cuándo se marcó como favorita (null para propias). |
sync_status | string | active — el endpoint solo devuelve inventario activo. |
Receptor genérico. Si prefieres un único endpoint, manda { event, data } y EstateWave lo despacha al manejador correcto según el evento (ver §9).
await ewPost('/webhook', {
event: 'property.price_changed',
data: { sync_global_id: '7c1f…', price: 3750000 }
});
Verifica las credenciales y el estado de la conexión. Útil como "ping" de salud.
// { data: { connection: { website_name, connection_status, sync_enabled, last_sync_at, properties_synced } } }
7. Payload de propiedad
Campos aceptados por create / update (el resto se ignora). Solo title y price son obligatorios al crear.
| Campo (externo) | Tipo | Notas |
|---|---|---|
sync_global_id | uuid | Opcional al crear (se genera). Referencia canónica para el resto. |
external_property_id | string | Tu id interno. |
title * | string | Título de la propiedad. |
description | string | Descripción larga. |
short_description | string | Descripción corta. |
price * | number | Precio de lista. (campo sensible — §10) |
offer_price | number | Precio de oferta (menor al de lista). |
currency | enum | MXN | USD (por defecto MXN). |
city | string | Ciudad. |
neighborhood | string | Zona / colonia / barrio. |
location | string | Alternativa libre si no envías neighborhood. |
property_type | enum | Departamento · Casa · Penthouse · Casa en condominio · Terreno · Local comercial · Oficina. |
bedrooms | int | Recámaras. |
bathrooms | number | Baños (admite .5). |
built_area | number | Superficie interior (m²). |
lot_area | number | Superficie total / terreno (m²). |
parking | int | Estacionamientos. |
year | int | Año. |
status | enum | active | inactive | draft. (sensible — §10) |
availability | enum | available | reserved | sold | rented. (sensible — §10) |
web_visibility | enum | private | web | shareable. |
amenities | string[] | Lista de amenidades. |
images | string[] | URLs de fotos. Solo se importan si la propiedad aún no tiene fotos en EstateWave (no se sobrescriben las cargadas en EstateWave). |
slug / url | string | Enlace al listing en tu web; se guarda como external_url del mapeo. |
8. Campos internos protegidos
Una web externa no puede escribir estos campos. Si los envías, se ignoran silenciosamente:
owner_contactcommissioninternal_notes legal_statusdocumentstrust_score NLS_visibilitybroker_notes
9. Eventos
Nombres de evento usados en /webhook (entrante) y en el webhook saliente (§11):
property.createdproperty.updatedproperty.price_changed property.unpublishedproperty.reactivatedproperty.sold property.rentedproperty.deleted
El receptor /webhook mapea cada evento al manejador correspondiente (create / update / status / delete soft). Para property.deleted se aplica baja lógica, nunca borrado físico.
10. Gestión de conflictos
Los campos sensibles son price, status y availability. EstateWave guarda el último valor "acordado" (baseline) por conexión.
Cuando tu web envía un cambio en un campo sensible:
- Si el valor entrante es igual al actual → no pasa nada.
- Si EstateWave no cambió ese campo desde el último acuerdo → se aplica tu valor (last-update-wins) y avanza el baseline.
- Si EstateWave sí cambió ese campo desde el baseline y tu valor difiere → CONFLICTO: ese campo no se sobrescribe. La respuesta trae
conflict: trueyconflicts: [ … ].
Los campos no sensibles de la misma petición sí se aplican. El conflicto lo resuelve el agente dentro de EstateWave (Webs conectadas → Conservar EstateWave / Conservar Web). Tu web no necesita lógica adicional, pero debe tolerar que un campo sensible quede pendiente.
// respuesta de /properties/update con conflicto { "data": { "action": "sync.conflict", "conflict": true, "conflicts": [ { "field": "price", "estatewave_value": 5500000, "external_value": 4800000 } ] } }
conflict: true, registra que ese campo quedó pendiente de resolución en EstateWave. Cuando el agente resuelva, el valor final se reflejará y (si configuras el webhook saliente) recibirás la actualización.11. Webhook saliente (EstateWave → tu web)
Opcional pero recomendado para tiempo real. Si configuras el API base URL de tu conexión en EstateWave, cuando el agente publique o cambie una propiedad desde EstateWave, te enviaremos un POST firmado a:
{api_base_url}/estatewave/webhook
Con estas cabeceras y cuerpo:
| Cabecera | Valor |
|---|---|
X-EstateWave-Event | El evento (ej. property.updated). |
X-EstateWave-Signature | HMAC-SHA256 (hex) del cuerpo crudo, con tu api_secret. |
// cuerpo { "event": "property.updated", "sent_at": "2026-06-09T17:00:00.000Z", "connection_id": "…", "data": { "sync_global_id": "7c1f…", "title": "…", "price": 3750000, "availability": "available", … } }
Implementación del endpoint (Express)
import crypto from 'node:crypto'; const EW_SECRET = process.env.EW_SECRET; // usa el cuerpo CRUDO para validar la firma app.post('/estatewave/webhook', express.raw({ type: 'application/json' }), (req, res) => { const raw = req.body; // Buffer const expected = crypto.createHmac('sha256', EW_SECRET).update(raw).digest('hex'); if (req.get('X-EstateWave-Signature') !== expected) return res.status(401).end(); const { event, data } = JSON.parse(raw.toString('utf8')); // upsert en tu web usando data.sync_global_id como referencia res.status(200).json({ ok: true }); });
connection.test).12. Seguridad
- API Key + Secret: el secreto identifica a la conexión. Trátalo como contraseña; guárdalo en variables de entorno, nunca en el frontend.
- Firma HMAC: preferida sobre el secreto en claro. Garantiza integridad del cuerpo.
- Ownership: una conexión solo puede ver/modificar propiedades del usuario dueño de esa conexión. Intentar tocar otra propiedad devuelve
404. - Rate limiting: 120 peticiones por minuto por API Key (
429si se excede). - Dominio autorizado: si la petición incluye un
Originde navegador, debe coincidir con el dominio de la web conectada. Las llamadas servidor-a-servidor (sin Origin) no se ven afectadas — haz las llamadas desde tu backend, no desde el navegador, para no exponer el secreto. - HTTPS siempre.
13. Flujo recomendado
- Conecta la web en EstateWave → guarda
api_keyyapi_secret. - (Opcional) configura el API base URL e implementa
/estatewave/webhook(§11) para recibir cambios en tiempo real. - Carga inicial: por cada propiedad de tu web, llama a
POST /properties/createy guarda elsync_global_iddevuelto junto a tu registro. - Mantenimiento: ante cada cambio en tu web, llama a
POST /properties/update(o/status) usando elsync_global_id. - Recibir cambios de EstateWave: vía el webhook saliente, o haciendo polling con
GET /property/{sync_global_id}. - Reconstruir el listado de tu web: llama periódicamente a
GET /inventoryy reemplaza tu listado con lo devuelto (propias + favoritas activas). Es la fuente de verdad: elimina de tu web lo que ya no aparezca. - Conflictos: si una respuesta trae
conflict: true, marca el campo como pendiente; el agente lo resuelve en EstateWave.
14. Checklist de integración
- ☐ Conexión creada en EstateWave;
api_keyyapi_secretguardados de forma segura. - ☐ Llamadas hechas desde el backend (no navegador), por HTTPS.
- ☐ Firma HMAC implementada (o secreto por cabecera como mínimo).
- ☐
sync_global_idpersistido junto a cada propiedad de tu web. - ☐ Carga inicial con
/properties/createcompletada. - ☐ Actualizaciones (
/update,/status) referenciando porsync_global_id. - ☐ Manejo de
conflict: trueen respuestas. - ☐ (Opcional) Endpoint
/estatewave/webhookcon validación de firma. - ☐ (Recomendado) Reconstrucción periódica del listado con
GET /inventory(reemplazo total). - ☐ Probar con
GET /connection/status.