← Volver a EstateWave
EstateWave · Hub de Sincronización

Guía de Integración para Webs Externas

EstateWave funciona como hub central. Tu web es un cliente conectado: puedes enviar tus propiedades a EstateWave, recibir las que se gestionan en EstateWave, y mantener sincronizados precio, estado y disponibilidad — de forma segura y multiusuario.

Versión 1.0 · Junio 2026 REST · JSON API Key + HMAC base: estatewave.xyz/api/integrations

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.
Importante: la integración es genérica. Estos mismos endpoints sirven para cualquier web de agente; no hay nada específico de un sitio concreto.

2. Conceptos clave

ConceptoDescripción
sync_global_idIdentificador 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_idEl 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_idEl id interno de la propiedad en EstateWave. Te lo devolvemos al crear/actualizar.
source_systemOrigen del registro de sincronización: website (lo enviaste tú) o estatewave.
sync_statusEstado 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 (formato ewk_…).
  • api_secret — secreto (formato ews_…). Se muestra una sola vez.
Guarda el secreto al crearlo. No se vuelve a mostrar. Si lo pierdes, usa “Rotar secreto” para generar uno nuevo (invalida el anterior).

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:

CabeceraObligatoriaDescripción
X-EW-KeyTu api_key.
X-EW-SignatureRecomendadaHMAC-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-SecretAlternativaTu 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;
}
La firma se calcula sobre los bytes exactos que envías. Firma el mismo string que mandas como 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

HTTPcodeSignificado
400VALIDATION / BAD_JSONFalta un campo requerido o el JSON es inválido.
401NO_KEY / BAD_KEY / BAD_AUTHFalta la key, key inválida, o firma/secreto incorrectos.
403INACTIVE / SYNC_DISABLED / BAD_ORIGINConexión inactiva, sync pausado, o dominio no autorizado.
404NOT_FOUNDLa propiedad no existe o no pertenece a tu conexión.
413TOO_LARGECuerpo demasiado grande (límite 6 MB).
429RATE_LIMITEDMás de 120 peticiones por minuto por API Key.

6. Endpoints

POST/properties/create

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

POST/properties/update

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

POST/properties/status

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
POST/properties/delete

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…' });
GET/property/{sync_global_id}

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).

GET/inventory

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
//   }
// }
Reemplazo total (fuente de verdad). Trata cada respuesta como el inventario activo completo: reconstruye tu listado con lo que llega y elimina de tu web cualquier propiedad que ya no aparezca. Una propiedad desaparece del inventario cuando se vende, se desactiva, se elimina, o —si era favorita— cuando el usuario la quita de sus favoritas.
Campo de relaciónTipoNotas
relationship_to_userenumowner (propia) · favorite (favorita de otro broker).
is_own_propertybooltrue si el usuario de la conexión es el dueño.
is_favoritebooltrue si es una favorita de otro broker.
owner_user_idstringBroker propietario real (no cambia al marcar favorita).
display_user_idstringUsuario en cuya web se muestra (el de la conexión).
favorite_created_atisoCuándo se marcó como favorita (null para propias).
sync_statusstringactive — el endpoint solo devuelve inventario activo.
POST/webhook

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 }
});
GET/connection/status

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)TipoNotas
sync_global_iduuidOpcional al crear (se genera). Referencia canónica para el resto.
external_property_idstringTu id interno.
title *stringTítulo de la propiedad.
descriptionstringDescripción larga.
short_descriptionstringDescripción corta.
price *numberPrecio de lista. (campo sensible — §10)
offer_pricenumberPrecio de oferta (menor al de lista).
currencyenumMXN | USD (por defecto MXN).
citystringCiudad.
neighborhoodstringZona / colonia / barrio.
locationstringAlternativa libre si no envías neighborhood.
property_typeenumDepartamento · Casa · Penthouse · Casa en condominio · Terreno · Local comercial · Oficina.
bedroomsintRecámaras.
bathroomsnumberBaños (admite .5).
built_areanumberSuperficie interior (m²).
lot_areanumberSuperficie total / terreno (m²).
parkingintEstacionamientos.
yearintAño.
statusenumactive | inactive | draft. (sensible — §10)
availabilityenumavailable | reserved | sold | rented. (sensible — §10)
web_visibilityenumprivate | web | shareable.
amenitiesstring[]Lista de amenidades.
imagesstring[]URLs de fotos. Solo se importan si la propiedad aún no tiene fotos en EstateWave (no se sobrescriben las cargadas en EstateWave).
slug / urlstringEnlace 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

Esto protege la información comercial y legal que vive solo en EstateWave.

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: true y conflicts: [ … ].

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 }
    ]
} }
Recomendación: si recibes 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:

CabeceraValor
X-EstateWave-EventEl evento (ej. property.updated).
X-EstateWave-SignatureHMAC-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 });
});
Responde 200 rápido y procesa de forma asíncrona si hace falta. Valida siempre la firma antes de confiar en el cuerpo. El mismo mecanismo se usa para la acción “Probar conexión” (evento 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 (429 si se excede).
  • Dominio autorizado: si la petición incluye un Origin de 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

  1. Conecta la web en EstateWave → guarda api_key y api_secret.
  2. (Opcional) configura el API base URL e implementa /estatewave/webhook (§11) para recibir cambios en tiempo real.
  3. Carga inicial: por cada propiedad de tu web, llama a POST /properties/create y guarda el sync_global_id devuelto junto a tu registro.
  4. Mantenimiento: ante cada cambio en tu web, llama a POST /properties/update (o /status) usando el sync_global_id.
  5. Recibir cambios de EstateWave: vía el webhook saliente, o haciendo polling con GET /property/{sync_global_id}.
  6. Reconstruir el listado de tu web: llama periódicamente a GET /inventory y reemplaza tu listado con lo devuelto (propias + favoritas activas). Es la fuente de verdad: elimina de tu web lo que ya no aparezca.
  7. 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_key y api_secret guardados de forma segura.
  • ☐ Llamadas hechas desde el backend (no navegador), por HTTPS.
  • ☐ Firma HMAC implementada (o secreto por cabecera como mínimo).
  • sync_global_id persistido junto a cada propiedad de tu web.
  • ☐ Carga inicial con /properties/create completada.
  • ☐ Actualizaciones (/update, /status) referenciando por sync_global_id.
  • ☐ Manejo de conflict: true en respuestas.
  • ☐ (Opcional) Endpoint /estatewave/webhook con validación de firma.
  • ☐ (Recomendado) Reconstrucción periódica del listado con GET /inventory (reemplazo total).
  • ☐ Probar con GET /connection/status.
Soporte: ante cualquier duda de integración, el equipo de EstateWave puede revisar los logs de sincronización (visibles en Webs conectadas) para diagnosticar peticiones por conexión, acción, dirección y resultado.