homlity/sdk-smarthome

SDK oficial de PHP de Homlity para consultar inventario inmobiliario (inmuebles, ubicaciones y tipos) y registrar leads en el CRM.

v3.0.0 2026-08-24 22:04 UTC

This package is auto-updated.

Last update: 2026-08-24 22:05:10 UTC


README

Homlity para desarrolladores

Homlity SDK · Smart Inmobiliario

SDK oficial de PHP para integrar el inventario inmobiliario y el CRM de Homlity en cualquier sitio o aplicación.

Packagist Descargas PHP GitHub

Sitio principal · Portal de desarrolladores · Packagist · GitHub

Tabla de contenido

  1. ¿Qué es este SDK?
  2. ¿Para qué sirve?
  3. Requisitos
  4. Instalación
  5. Credenciales
  6. Inicio rápido
  7. Referencia de la API del SDK
  8. Modelos de datos
  9. Catálogos y códigos
  10. Ejemplos completos
  11. Integración con frameworks
  12. Buenas prácticas
  13. Limitaciones y advertencias
  14. Endpoints REST subyacentes
  15. Solución de problemas
  16. Pruebas
  17. Versionado y changelog
  18. Soporte

¿Qué es este SDK?

homlity/sdk-smarthome es una librería PHP sin frameworks que envuelve la API REST del integrador inmobiliario de Homlity (https://api.smart-home.com.co). En lugar de armar URLs, manejar cURL y navegar arrays asociativos en inglés, trabajas con objetos PHP con métodos en español:

$propiedades = $sdk->obtenerPropiedades();

echo $propiedades[0]->getNombre();       // "VENTA DE OFICINA EN CENTRO COMERCIAL PRIMAVERA"
echo $propiedades[0]->getValorVenta();   // 650000000.00
echo $propiedades[0]->getCiudad();       // "VILLAVICENCIO"

El SDK resuelve tres cosas por ti:

Sin el SDK Con el SDK
Construir URLs con companyCode / projectCode en cada llamada Configuras las credenciales una vez en el builder
Manejar cURL, headers Accept y compresión gzip a mano Ya viene configurado en cada petición
Recorrer arrays con claves en inglés (unitCategoryDescription, mainImageURL…) Objetos tipados con getters legibles en español

¿Para qué sirve?

Casos de uso reales que este SDK habilita:

  • Portal inmobiliario propio. Publicar en tu sitio web el inventario que la inmobiliaria administra en Homlity, siempre sincronizado, sin duplicar la carga de datos.
  • Ficha de inmueble. Página de detalle con galería de fotos, video, tour virtual 360°, características, mapa (latitud/longitud) y datos del asesor.
  • Buscador y filtros. Construir filtros por ciudad, localidad, barrio, tipo de inmueble, gestión (venta/arriendo), precio, alcobas o baños usando los catálogos que expone la API.
  • Captación de leads hacia el CRM. Enviar los formularios de contacto de tu web directamente al CRM de Homlity, asociados al inmueble específico que generó el interés.
  • Landing pages y micrositios de proyectos que consumen un subconjunto del inventario.
  • Sindicación / feeds hacia portales de terceros o alimentación de un índice de búsqueda.

Requisitos

Requisito Versión
PHP >= 7.4 (probado hasta PHP 8.3)
Extensión ext-curl habilitada
Extensión ext-json habilitada
Extensión (recomendada) ext-zlib para descompresión gzip
Dependencia curl/curl ^2.5 (se instala automáticamente)
Red Salida HTTPS hacia api.smart-home.com.co

Instalación

composer require homlity/sdk-smarthome

⚠️ Nota de versión importante

La última etiqueta publicada en Packagist es v2.2.1, que expone una API distinta y anterior (SDKSmartInmoBuilder::build() + SmartHomeFacade). Toda esta documentación describe la API actual de la rama main (SDKSmartHomeBuilder), que aún no tiene etiqueta.

Mientras se publica la versión estable, instala la rama de desarrollo:

composer require homlity/sdk-smarthome:dev-main

O fijando el commit para builds reproducibles:

{
  "require": {
    "homlity/sdk-smarthome": "dev-main#bda386f"
  },
  "minimum-stability": "dev",
  "prefer-stable": true
}

Para el mantenedor: etiquetar v3.0.0 desde main publica esta API como versión estable y permite composer require homlity/sdk-smarthome:^3.0.

Instalación sin Composer

spl_autoload_register(function ($class) {
    $prefix  = 'Codwelt\\SDK\\SmartHome\\';
    $baseDir = __DIR__ . '/sdk-smartinmobiliario/src/';
    if (strncmp($prefix, $class, strlen($prefix)) !== 0) {
        return;
    }
    $file = $baseDir . str_replace('\\', '/', substr($class, strlen($prefix))) . '.php';
    if (file_exists($file)) {
        require $file;
    }
});

Necesitarás instalar también curl/curl manualmente.

Namespace

El paquete se llama homlity/sdk-smarthome, pero el namespace PHP sigue siendo Codwelt\SDK\SmartHome\ por compatibilidad hacia atrás. No lo confundas:

use Codwelt\SDK\SmartHome\SDKSmartHomeBuilder;

Credenciales

El SDK necesita hasta tres identificadores, que entrega el equipo de Homlity al habilitar la integración:

Credencial Método Se usa en Ejemplo
Company Code setCompanyCode() Todas las consultas de inventario 695704c3
Project Code setProjectCode() Todas las consultas de inventario 5bec54a5
Project ID setProjectId() Únicamente agregarLead() -IxD-MeYCo32TRLf…
  • Company Code identifica a la inmobiliaria dentro de la plataforma.
  • Project Code identifica el proyecto o bolsa de inventario que quieres exponer.
  • Project ID es el token largo del CRM al que se asocian los leads.

Solicítalos en el portal de desarrolladores de Homlity.

Seguridad: aunque el Company Code y el Project Code viajan en la URL y solo dan acceso de lectura al inventario que ya es público, nunca expongas el projectId en JavaScript del lado del cliente. Las llamadas del SDK deben ejecutarse siempre en el servidor.

Guárdalas fuera del código:

$sdk = new SDKSmartHomeBuilder();
$sdk->setCompanyCode(getenv('HOMLITY_COMPANY_CODE'));
$sdk->setProjectCode(getenv('HOMLITY_PROJECT_CODE'));
$sdk->setProjectId(getenv('HOMLITY_PROJECT_ID'));

Inicio rápido

<?php
require __DIR__ . '/vendor/autoload.php';

use Codwelt\SDK\SmartHome\SDKSmartHomeBuilder;

$sdk = new SDKSmartHomeBuilder();
$sdk->setCompanyCode('695704c3');
$sdk->setProjectCode('5bec54a5');
$sdk->setProjectId('-IxD-MeYCo32TRLfUCbfBpdb1rxYAsdHPqc1GL0Mc9CAdWenV8FW/V6iQAgBl-Zh');

// 1. Listado de inmuebles
$propiedades = $sdk->obtenerPropiedades();

// 2. Detalle de uno
$propiedad = $sdk->obtenerPropiedad($propiedades[0]->getCodigo());

// 3. Catálogo de ubicaciones
$ubicaciones = $sdk->obtenerUbicaciones();

// 4. Catálogo de tipos de inmueble
$tipos = $sdk->obtenerTipoPropiedades();

// 5. Registrar un lead en el CRM
$idLead = $sdk->agregarLead([
    'nombre'      => 'Sergio Wiesner',
    'correo'      => 'webdev@homlity.com',
    'telefono'    => '+573506573588',
    'web'         => 'mi-inmobiliaria.com',
    'commentario' => 'Quiero agendar una visita',
    'codigo'      => $propiedad->getModuleId(),
]);

Referencia de la API del SDK

SDKSmartHomeBuilder

Clase de entrada. Codwelt\SDK\SmartHome\SDKSmartHomeBuilder.

__construct(string $endpoint = "https://api.smart-home.com.co")

Crea el cliente. Solo necesitas pasar $endpoint si Homlity te asignó un entorno alterno (staging, on-premise):

$sdk = new SDKSmartHomeBuilder();                            // producción
$sdk = new SDKSmartHomeBuilder('https://staging.ejemplo.co'); // entorno alterno

No incluyas la barra final: el SDK concatena las rutas directamente.

setCompanyCode(string $companyCode): void

Define el código de la inmobiliaria. Obligatorio para todas las consultas de inventario.

setProjectCode(string $projectCode): void

Define el código del proyecto. Obligatorio para todas las consultas de inventario.

setProjectId(string $projectId): void

Define el identificador del proyecto en el CRM. Solo se usa en agregarLead(); si no vas a enviar leads puedes omitirlo.

obtenerPropiedades(): InmueblePreview[]

Devuelve todos los inmuebles del proyecto como un array de InmueblePreview.

$propiedades = $sdk->obtenerPropiedades();

foreach ($propiedades as $p) {
    printf("%s — %s — $%s\n", $p->getCodigo(), $p->getNombre(), number_format($p->getValorVenta()));
}
  • Devuelve [] (array vacío) si la petición falla o no hay inventario. No lanza excepciones.
  • La respuesta no está paginada desde el cliente: la API devuelve hasta 10.000 registros por llamada. Si tu inventario es grande, cachea el resultado.

obtenerPropiedad(string $unitCode): ?InmuebleDetail

Devuelve el detalle de un inmueble a partir de su código (getCodigo() del listado).

$propiedad = $sdk->obtenerPropiedad('20230022');

if ($propiedad === null || $propiedad->getCodigo() === null) {
    http_response_code(404);
    exit('Inmueble no disponible');
}
  • El parámetro es el code del inmueble (ej. "20230022"), no el moduleId.
  • Devuelve null solo si la petición HTTP falla (red caída, endpoint inalcanzable).
  • 🔴 Un código inexistente NO devuelve null. La API responde 200 con units: null y el SDK entrega un InmuebleDetail vacío: el objeto existe, pero su array interno es null, así que cada getter emite el aviso Trying to access array offset on value of type null y devuelve null. Por eso comprobar === null no basta: valida además un campo obligatorio como getCodigo(), tal como en el ejemplo de arriba.
  • El propio obtenerPropiedad() emite ese mismo aviso desde dentro del SDK antes de devolverte el objeto. Si quieres logs limpios, llámalo como @$sdk->obtenerPropiedad($codigo).

obtenerUbicaciones(): ?Ubicaciones

Devuelve el árbol geográfico del inventario (países, ciudades, localidades, barrios y zonas), con el conteo de inmuebles de cada nodo — ideal para construir filtros.

$ubicaciones = $sdk->obtenerUbicaciones();

foreach ($ubicaciones->getCiudades() as $ciudad) {
    echo $ciudad->getNombre() . ' (' . $ciudad->getCantidad() . ")\n";
}
// VILLAVICENCIO (12)
// ACACIAS (2)

obtenerTipoPropiedades(): ?TipoPropiedad[]

Devuelve los tipos de inmueble presentes en el inventario, con su conteo.

foreach ($sdk->obtenerTipoPropiedades() ?? [] as $tipo) {
    echo "{$tipo->getCodigo()} · {$tipo->getNombre()} ({$tipo->getCantidad()})\n";
}
// 1 · Apartamento (6)
// 2 · Casa (6)
// 4 · Oficina (2)

⚠️ Este método devuelve null (no []) cuando el proyecto no tiene tipos, y en ese caso además emite el aviso foreach() argument must be of type array|object, null given desde dentro del propio SDK. Usa siempre ?? [] antes de iterar.

agregarLead(array $datos): ?string

Registra un contacto en el CRM de Homlity y lo asocia a un inmueble.

$respuesta = $sdk->agregarLead([
    'nombre'      => 'Ana Ramírez',
    'correo'      => 'ana@ejemplo.com',
    'telefono'    => '+573001112233',
    'web'         => 'mi-inmobiliaria.com',
    'commentario' => 'Me interesa agendar una visita el sábado',
    'codigo'      => $propiedad->getModuleId(),
]);

Claves del array $datos — todas son obligatorias:

Clave Se envía como Descripción
nombre first_name Nombre del interesado
correo email Correo electrónico
telefono mobile_number Celular, preferiblemente en formato E.164 (+57…)
web origin Origen del lead: dominio, campaña o landing
commentario comment Mensaje del interesado
codigo moduleId getModuleId() del inmueble, no getCodigo()

🔴 Dos detalles que causan la mayoría de los errores:

  1. La clave se escribe commentario (con doble m), tal como la espera el SDK. Si la omites, PHP emitirá un warning de clave indefinida y el comentario viajará vacío.
  2. codigo debe ser el moduleId (un UUID como 35f98252-6db1-43d6-bd6b-18752b883c41), no el código visible del inmueble.

Además, setProjectId() debe haberse llamado antes; de lo contrario el lead llegará sin proyecto asociado.

Devuelve el cuerpo crudo de la respuesta del CRM (string) o null si la petición falló. Comprueba siempre !== null antes de mostrar un mensaje de éxito al usuario.

Modelos de datos

Todos los modelos viven en Codwelt\SDK\SmartHome\InfraStructure\ y son objetos de solo lectura construidos a partir de la respuesta JSON.

InmueblePreview / InmuebleDetail

InmuebleDetail extiende InmueblePreview sin añadir métodos: ambos exponen exactamente la misma interfaz. La diferencia está en el origen de los datos, no en la clase.

Identificación

Método Campo JSON Tipo Notas
getCodigo() code string Código visible del inmueble. Úsalo en obtenerPropiedad()
getModuleId() moduleId string (UUID) Identificador interno. Úsalo en agregarLead()
getNombre() name string Título comercial
getDescripcion() description string Descripción larga, puede traer saltos de línea
getMls() mls array|null Códigos MLS asociados

Áreas

Método Campo JSON Tipo
getArea() area float — área construida (m²)
getAreaPrivada() privateArea float — área privada (m²)
getAreaLote() lotArea float — área de lote (m²)

Precios

Método Campo JSON Tipo Notas
getValorVenta() price float Precio de venta
getvalorArriendo() monthlyFee float Canon mensual. ⚠️ La v va en minúscula
getMantenimiento() maintenanceFee float Administración
getInmpuesto() tax float Impuesto. ⚠️ Nombre con errata: Inmpuesto
getTieneImpuestos() hasTax bool
getTotalMensualidad() totalMonthlyFee float Canon + administración + impuestos

Gestión y tipo

Método Campo JSON Tipo Notas
getGestionId() unitCategory int Ver catálogo de gestión
getGestion() unitCategoryDescription string "Venta", "Arriendo"
getTipoUnidadId() unitType int Ver catálogo de tipos
getTipoUnidad() unitTypeDescription string "Apartamento", "Casa"

Distribución

Método Campo JSON Tipo
getAlcobas() bedroom float
getBanos() bathrooms float
getGarajes() garages int
getEstrato() strata string
getNivel() floor int — piso
getAnoConstruccion() yearBuild int0 si no se registró

Ubicación

Método Campo JSON Tipo Notas
getDireccion() address string
getLatitud() latitude float Para mapas
getLongitud() longitude float Para mapas
getCiudad() / getCiudadId() city / cityId string
getLocalidad() / getLocalidadId() locality / localityId ?string Suele venir null
getBarrio() / getBarrioId() neighborhood / neighborhoodId ?string neighborhoodId puede ser null aunque haya nombre
getZona() / getZonaId() zone / zoneId ?string Frecuentemente null

Multimedia y relaciones

Método Campo JSON Devuelve Notas
getImagenDestacada() mainImageURL ?string Puede ser null
getFotos() images Fotos[] ⚠️ Falla si images es null
getCaracteristicas() features Caracteristicas[] ⚠️ Falla si features es null
getAsesor() seller Asesor

⚠️ En inventarios reales, images y features llegan null con frecuencia (en el inventario de demostración: 3 de 15 y 4 de 15 respectivamente). Como los métodos hacen foreach directo sobre el campo, PHP 8 emite el aviso foreach() argument must be of type array|object, null given y el método devuelve [].

No es fatal por sí solo, pero sí es ruido en los logs — y en Laravel, Symfony o cualquier proyecto con un set_error_handler que convierte los avisos en excepciones, sí se convierte en una ErrorException. Usa accesores seguros que cubran ambos escenarios:

function fotosSeguras(InmueblePreview $inmueble): array
{
    try {
        $fotos = @$inmueble->getFotos();   // silencia el aviso de foreach sobre null
        return is_array($fotos) ? $fotos : [];
    } catch (\Throwable $e) {              // frameworks que convierten avisos en excepciones
        return [];
    }
}

Fotos

Representa una pieza multimedia: foto, video o tour virtual.

Método Campo JSON Notas
getNombre() title Si viene vacío, cae al nombre del inmueble — útil como alt
getTipoId() mediaType Ver catálogo multimedia
getUbicacion() source URL absoluta del recurso
getOrden() sequence Entero para ordenar la galería
usort($fotos, fn($a, $b) => $a->getOrden() <=> $b->getOrden());

foreach ($fotos as $foto) {
    if ($foto->getTipoId() === 1) {
        echo '<img src="' . htmlspecialchars($foto->getUbicacion()) . '"
                   alt="' . htmlspecialchars($foto->getNombre()) . '">';
    }
}

Caracteristicas

Método Campo JSON
getNombre() name — ej. "BALCON", "ILUMINACION"

Asesor

Datos de contacto del comercial responsable del inmueble.

Método Campo JSON
getNombre() name
getCelular() mobileNumber
getEmail() email
$asesor = $propiedad->getAsesor();
$wa = 'https://wa.me/57' . preg_replace('/\D/', '', $asesor->getCelular());
echo '<a href="' . $wa . '">Escribir a ' . htmlspecialchars($asesor->getNombre()) . '</a>';

Ubicaciones

Contenedor del árbol geográfico devuelto por obtenerUbicaciones().

Método Devuelve Campo JSON
getPaises() Paises[] countries
getCiudades() Ciudades[] cities
getLocalidades() Localidades[] localities
getBarrios() Barrios[] neighborhoods
getZonas() Paises[] zones — ⚠️ ver nota

⚠️ getZonas() envuelve las zonas en la clase Paises, por lo que getCodigo() leerá countryId y no zoneId. En los proyectos de prueba zones viene vacío, así que rara vez se nota; evita depender de este método hasta que se corrija.

Paises

Método Campo JSON
getCodigo() countryId (UUID)
getNombre() name
getCantidad() count — inmuebles en el país

Ciudades

Método Campo JSON
getCodigo() cityId (UUID)
getPaisId() countryId
getNombre() name
getCantidad() count

Localidades

Método Campo JSON
getCodigo() localityId (UUID)
getCiudadId() cityId
getNombre() name
getCantidad() count

Barrios

Método Campo JSON
getCodigo() neighborhoodId (UUID)
getCiudadId() cityId
getPaisId() countryId
getNombre() name
getCantidad() count

TipoPropiedad

Método Campo JSON
getCodigo() code (int)
getNombre() name
getCantidad() count

Catálogos y códigos

Gestión (unitCategory)

Código Descripción
0 Sin Definir
1 Arriendo
2 Venta
3 Arriendo y Venta

Usa siempre getGestion() para mostrar el texto: la API ya envía la descripción oficial.

Tipos de inmueble (unitType)

Los códigos son estables, pero el catálogo disponible varía por proyecto. Obtén siempre el listado real con obtenerTipoPropiedades(). Valores observados:

Código Nombre
0 Sin Definir
1 Apartamento
2 Casa
4 Oficina
23 Casa Campestre

Tipo de multimedia (mediaType)

Código Contenido Cómo renderizarlo
1 Imagen <img src="…">
3 Video (.mp4) <video controls src="…">
5 Tour virtual 360° (Matterport) <iframe src="…" allowfullscreen>

Ejemplos completos

En la carpeta examples/ encontrarás estos scripts listos para ejecutar.

1. Listado con tarjetas

<?php
require __DIR__ . '/vendor/autoload.php';

use Codwelt\SDK\SmartHome\SDKSmartHomeBuilder;

$sdk = new SDKSmartHomeBuilder();
$sdk->setCompanyCode(getenv('HOMLITY_COMPANY_CODE'));
$sdk->setProjectCode(getenv('HOMLITY_PROJECT_CODE'));

$propiedades = $sdk->obtenerPropiedades();

if ($propiedades === []) {
    echo '<p>No hay inmuebles disponibles en este momento.</p>';
    return;
}

foreach ($propiedades as $p) {
    $imagen = $p->getImagenDestacada() ?: '/img/placeholder.jpg';
    $precio = $p->getGestionId() === 1 ? $p->getvalorArriendo() : $p->getValorVenta();
    ?>
    <article class="card">
        <img src="<?= htmlspecialchars($imagen) ?>" alt="<?= htmlspecialchars($p->getNombre()) ?>" loading="lazy">
        <span class="badge"><?= htmlspecialchars($p->getGestion()) ?></span>
        <h3><?= htmlspecialchars($p->getNombre()) ?></h3>
        <p><?= htmlspecialchars($p->getTipoUnidad()) ?> · <?= htmlspecialchars($p->getBarrio() ?? $p->getCiudad()) ?></p>
        <p><strong>$<?= number_format($precio, 0, ',', '.') ?></strong></p>
        <ul>
            <li><?= (int) $p->getAlcobas() ?> alcobas</li>
            <li><?= (int) $p->getBanos() ?> baños</li>
            <li><?= (int) $p->getArea() ?> m²</li>
        </ul>
        <a href="/inmueble.php?codigo=<?= urlencode($p->getCodigo()) ?>">Ver detalle</a>
    </article>
    <?php
}

2. Filtrar en memoria

La API devuelve el inventario completo, así que los filtros se aplican en PHP:

$filtrados = array_values(array_filter($propiedades, function ($p) {
    return $p->getGestionId() === 2                          // solo venta
        && $p->getCiudadId() === '454cca01-1f8e-4c4e-91ba-24ce3a37f19f'
        && $p->getAlcobas() >= 3
        && $p->getValorVenta() <= 500000000;
}));

// Ordenar por precio ascendente
usort($filtrados, fn($a, $b) => $a->getValorVenta() <=> $b->getValorVenta());

// Paginar
$pagina    = max(1, (int) ($_GET['pagina'] ?? 1));
$porPagina = 12;
$total     = count($filtrados);
$visibles  = array_slice($filtrados, ($pagina - 1) * $porPagina, $porPagina);
$paginas   = (int) ceil($total / $porPagina);

3. Ficha de inmueble con galería

$codigo = $_GET['codigo'] ?? null;

if (!$codigo) {
    http_response_code(400);
    exit('Falta el código del inmueble');
}

// La @ silencia el aviso que el SDK emite cuando el código no existe
$propiedad = @$sdk->obtenerPropiedad($codigo);

// Doble validación: null (fallo de red) u objeto vacío (código inexistente)
if ($propiedad === null || $propiedad->getCodigo() === null) {
    http_response_code(404);
    exit('Inmueble no encontrado');
}

// Accesor seguro: `images` / `features` pueden venir null en la respuesta
function seguro(callable $fn): array
{
    try {
        $r = @$fn();
        return is_array($r) ? $r : [];
    } catch (\Throwable $e) {
        return [];
    }
}

$fotos = seguro(fn() => $propiedad->getFotos());

usort($fotos, fn($a, $b) => $a->getOrden() <=> $b->getOrden());

$imagenes = array_filter($fotos, fn($f) => $f->getTipoId() === 1);
$videos   = array_filter($fotos, fn($f) => $f->getTipoId() === 3);
$tours    = array_filter($fotos, fn($f) => $f->getTipoId() === 5);

$caracteristicas = seguro(fn() => $propiedad->getCaracteristicas());
?>
<h1><?= htmlspecialchars($propiedad->getNombre()) ?></h1>
<p><?= nl2br(htmlspecialchars($propiedad->getDescripcion())) ?></p>

<div class="galeria">
    <?php foreach ($imagenes as $img): ?>
        <img src="<?= htmlspecialchars($img->getUbicacion()) ?>"
             alt="<?= htmlspecialchars($img->getNombre()) ?>" loading="lazy">
    <?php endforeach; ?>
</div>

<?php foreach ($videos as $video): ?>
    <video controls src="<?= htmlspecialchars($video->getUbicacion()) ?>"></video>
<?php endforeach; ?>

<?php foreach ($tours as $tour): ?>
    <iframe src="<?= htmlspecialchars($tour->getUbicacion()) ?>"
            width="100%" height="480" allowfullscreen loading="lazy"></iframe>
<?php endforeach; ?>

<ul class="caracteristicas">
    <?php foreach ($caracteristicas as $c): ?>
        <li><?= htmlspecialchars($c->getNombre()) ?></li>
    <?php endforeach; ?>
</ul>

<!-- Mapa -->
<div id="mapa"
     data-lat="<?= $propiedad->getLatitud() ?>"
     data-lng="<?= $propiedad->getLongitud() ?>"></div>

4. Construir filtros desde los catálogos

$ubicaciones = $sdk->obtenerUbicaciones();
$tipos       = $sdk->obtenerTipoPropiedades() ?? [];
?>
<form method="get">
    <select name="ciudad">
        <option value="">Todas las ciudades</option>
        <?php foreach ($ubicaciones->getCiudades() as $ciudad): ?>
            <option value="<?= htmlspecialchars($ciudad->getCodigo()) ?>">
                <?= htmlspecialchars($ciudad->getNombre()) ?> (<?= $ciudad->getCantidad() ?>)
            </option>
        <?php endforeach; ?>
    </select>

    <select name="barrio">
        <option value="">Todos los barrios</option>
        <?php foreach ($ubicaciones->getBarrios() as $barrio): ?>
            <option value="<?= htmlspecialchars($barrio->getCodigo()) ?>"
                    data-ciudad="<?= htmlspecialchars($barrio->getCiudadId()) ?>">
                <?= htmlspecialchars($barrio->getNombre()) ?> (<?= $barrio->getCantidad() ?>)
            </option>
        <?php endforeach; ?>
    </select>

    <select name="tipo">
        <option value="">Todos los tipos</option>
        <?php foreach ($tipos as $tipo): ?>
            <option value="<?= $tipo->getCodigo() ?>">
                <?= htmlspecialchars($tipo->getNombre()) ?> (<?= $tipo->getCantidad() ?>)
            </option>
        <?php endforeach; ?>
    </select>

    <button type="submit">Buscar</button>
</form>

El atributo data-ciudad de cada barrio permite encadenar los selects en JavaScript sin volver a llamar a la API.

5. Formulario de contacto que envía al CRM

<?php
require __DIR__ . '/vendor/autoload.php';

use Codwelt\SDK\SmartHome\SDKSmartHomeBuilder;

$errores = [];
$exito   = false;

if ($_SERVER['REQUEST_METHOD'] === 'POST') {

    // 1. Validación en tu lado
    $nombre   = trim($_POST['nombre'] ?? '');
    $correo   = filter_input(INPUT_POST, 'correo', FILTER_VALIDATE_EMAIL) ?: '';
    $telefono = preg_replace('/[^\d+]/', '', $_POST['telefono'] ?? '');
    $mensaje  = trim($_POST['mensaje'] ?? '');
    $moduleId = $_POST['module_id'] ?? '';

    if ($nombre === '')   { $errores[] = 'El nombre es obligatorio.'; }
    if ($correo === '')   { $errores[] = 'El correo no es válido.'; }
    if ($telefono === '') { $errores[] = 'El teléfono es obligatorio.'; }
    if ($moduleId === '') { $errores[] = 'No se pudo identificar el inmueble.'; }

    // 2. Envío al CRM
    if ($errores === []) {
        $sdk = new SDKSmartHomeBuilder();
        $sdk->setCompanyCode(getenv('HOMLITY_COMPANY_CODE'));
        $sdk->setProjectCode(getenv('HOMLITY_PROJECT_CODE'));
        $sdk->setProjectId(getenv('HOMLITY_PROJECT_ID'));   // imprescindible para leads

        $respuesta = $sdk->agregarLead([
            'nombre'      => $nombre,
            'correo'      => $correo,
            'telefono'    => $telefono,
            'web'         => $_SERVER['HTTP_HOST'] ?? 'sitio-web',
            'commentario' => $mensaje,       // ojo: doble "m"
            'codigo'      => $moduleId,      // moduleId, no code
        ]);

        if ($respuesta === null) {
            $errores[] = 'No pudimos enviar tu solicitud. Intenta de nuevo.';
            error_log('[Homlity SDK] agregarLead devolvió null para ' . $correo);
        } else {
            $exito = true;
        }
    }
}
?>
<?php if ($exito): ?>
    <p class="ok">¡Gracias! Un asesor te contactará pronto.</p>
<?php else: ?>
    <?php foreach ($errores as $error): ?>
        <p class="error"><?= htmlspecialchars($error) ?></p>
    <?php endforeach; ?>
    <form method="post">
        <input type="hidden" name="module_id" value="<?= htmlspecialchars($propiedad->getModuleId()) ?>">
        <input name="nombre"   placeholder="Nombre completo" required>
        <input name="correo"   type="email" placeholder="Correo" required>
        <input name="telefono" placeholder="+57 300 000 0000" required>
        <textarea name="mensaje" placeholder="Cuéntanos qué necesitas"></textarea>
        <button type="submit">Solicitar información</button>
    </form>
<?php endif; ?>

6. Sitemap y feed JSON

// sitemap-inmuebles.xml
header('Content-Type: application/xml; charset=utf-8');
echo '<?xml version="1.0" encoding="UTF-8"?>';
echo '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">';
foreach ($sdk->obtenerPropiedades() as $p) {
    echo '<url><loc>https://mi-inmobiliaria.com/inmueble/'
       . urlencode($p->getCodigo()) . '</loc><changefreq>daily</changefreq></url>';
}
echo '</urlset>';
// feed.json — para un buscador en el front (Algolia, Fuse.js, etc.)
header('Content-Type: application/json; charset=utf-8');
echo json_encode(array_map(fn($p) => [
    'codigo'  => $p->getCodigo(),
    'nombre'  => $p->getNombre(),
    'tipo'    => $p->getTipoUnidad(),
    'gestion' => $p->getGestion(),
    'ciudad'  => $p->getCiudad(),
    'barrio'  => $p->getBarrio(),
    'precio'  => $p->getValorVenta(),
    'area'    => $p->getArea(),
    'alcobas' => $p->getAlcobas(),
    'banos'   => $p->getBanos(),
    'imagen'  => $p->getImagenDestacada(),
    'lat'     => $p->getLatitud(),
    'lng'     => $p->getLongitud(),
], $sdk->obtenerPropiedades()), JSON_UNESCAPED_UNICODE);

Integración con frameworks

Laravel

Registra el SDK como singleton en un ServiceProvider:

// app/Providers/AppServiceProvider.php
use Codwelt\SDK\SmartHome\SDKSmartHomeBuilder;

public function register(): void
{
    $this->app->singleton(SDKSmartHomeBuilder::class, function () {
        $sdk = new SDKSmartHomeBuilder(config('homlity.endpoint'));
        $sdk->setCompanyCode(config('homlity.company_code'));
        $sdk->setProjectCode(config('homlity.project_code'));
        $sdk->setProjectId(config('homlity.project_id'));
        return $sdk;
    });
}
// config/homlity.php
return [
    'endpoint'     => env('HOMLITY_ENDPOINT', 'https://api.smart-home.com.co'),
    'company_code' => env('HOMLITY_COMPANY_CODE'),
    'project_code' => env('HOMLITY_PROJECT_CODE'),
    'project_id'   => env('HOMLITY_PROJECT_ID'),
];
// app/Http/Controllers/InmuebleController.php
public function index(SDKSmartHomeBuilder $sdk)
{
    $propiedades = Cache::remember('homlity.inmuebles', now()->addMinutes(15),
        fn() => $sdk->obtenerPropiedades()
    );

    return view('inmuebles.index', compact('propiedades'));
}

Cache::remember serializa los objetos del SDK sin problema: solo contienen arrays.

WordPress

// functions.php
require_once get_template_directory() . '/vendor/autoload.php';

use Codwelt\SDK\SmartHome\SDKSmartHomeBuilder;

function homlity_sdk(): SDKSmartHomeBuilder {
    static $sdk = null;
    if ($sdk === null) {
        $sdk = new SDKSmartHomeBuilder();
        $sdk->setCompanyCode(HOMLITY_COMPANY_CODE);
        $sdk->setProjectCode(HOMLITY_PROJECT_CODE);
        $sdk->setProjectId(HOMLITY_PROJECT_ID);
    }
    return $sdk;
}

function homlity_inmuebles(): array {
    $cache = get_transient('homlity_inmuebles');
    if ($cache !== false) {
        return $cache;
    }
    $propiedades = homlity_sdk()->obtenerPropiedades();
    set_transient('homlity_inmuebles', $propiedades, 15 * MINUTE_IN_SECONDS);
    return $propiedades;
}

// Shortcode: [homlity_inmuebles limite="6"]
add_shortcode('homlity_inmuebles', function ($atts) {
    $atts  = shortcode_atts(['limite' => 6], $atts);
    $items = array_slice(homlity_inmuebles(), 0, (int) $atts['limite']);

    ob_start();
    foreach ($items as $p) {
        printf('<article><h3>%s</h3><p>%s</p></article>',
            esc_html($p->getNombre()),
            esc_html(number_format($p->getValorVenta(), 0, ',', '.'))
        );
    }
    return ob_get_clean();
});

PHP plano con caché en archivo

function homlityInmuebles(SDKSmartHomeBuilder $sdk, int $ttl = 900): array
{
    $archivo = sys_get_temp_dir() . '/homlity-inmuebles.cache';

    if (is_file($archivo) && (time() - filemtime($archivo)) < $ttl) {
        $datos = @unserialize(file_get_contents($archivo));
        if ($datos !== false) {
            return $datos;
        }
    }

    $propiedades = $sdk->obtenerPropiedades();

    // No sobrescribas una caché válida con una respuesta vacía
    if ($propiedades !== []) {
        file_put_contents($archivo, serialize($propiedades), LOCK_EX);
    } elseif (is_file($archivo)) {
        return unserialize(file_get_contents($archivo));   // sirve la caché rancia
    }

    return $propiedades;
}

Buenas prácticas

  1. Cachea siempre. obtenerPropiedades() trae el inventario completo en cada llamada. Un TTL de 10–30 minutos reduce drásticamente la latencia y la carga sobre la API.
  2. Nunca llames al SDK desde el navegador. Las credenciales, y sobre todo el projectId, deben quedarse en el servidor.
  3. Sirve caché rancia ante fallos. Como el SDK devuelve [] en vez de lanzar una excepción, una caída de la API vaciaría tu portal. Conserva la última respuesta buena.
  4. Escapa toda salida. Las descripciones y títulos vienen tal como los cargó la inmobiliaria: usa htmlspecialchars() / esc_html().
  5. Valida antes de renderizar. null, [] y campos ausentes son escenarios normales, no excepcionales.
  6. Registra los fallos. Un agregarLead() que devuelve null es un lead perdido: déjalo en el log y, si es crítico, guarda el formulario en tu base de datos para reintentarlo.
  7. Precarga las imágenes con loading="lazy". Las galerías suelen traer más de 20 fotos en alta resolución.
  8. Usa getModuleId() para leads y getCodigo() para URLs. Confundirlos es el error más frecuente.

Limitaciones y advertencias (importante)

Estas son características actuales del SDK que conviene conocer antes de llevarlo a producción:

# Comportamiento Impacto Cómo mitigarlo
1 Los errores se silencian. Los métodos capturan \Error y devuelven [] / null sin distinguir entre "no hay datos", "credenciales inválidas" y "la API está caída" No puedes reaccionar a un fallo real Envuelve las llamadas y verifica el resultado; conserva caché rancia
2 obtenerTipoPropiedades() devuelve null, no [], si no hay tipos, y además emite un aviso foreach() … null given foreach sobre null en tu código ?? [] antes de iterar
3 getFotos() y getCaracteristicas() avisan si el campo es null en la respuesta Aviso foreach() … null given; devuelven []. Se convierte en ErrorException si tu framework escala los avisos Accesor seguro con @ + try/catch
4 obtenerPropiedad() con un código inexistente devuelve un objeto vacío, no null Cada getter avisa y devuelve null Validar === null y getCodigo() === null
5 Erratas en nombres de métodos: getInmpuesto(), getvalorArriendo() Confusión al autocompletar Documentadas en las tablas de arriba
6 La clave del lead es commentario (doble m) Comentario vacío en el CRM Copiar la clave literal
7 getZonas() devuelve objetos Paises getCodigo() lee countryId Evitar el método por ahora
8 Sin paginación ni filtros del lado del servidor Payload grande en inventarios extensos Cachear y filtrar en PHP
9 Sin timeout de cURL configurable Una API lenta bloquea tu petición Ejecutar la sincronización en un cron, no en la petición del usuario
10 Sin reintentos Un fallo transitorio pierde datos Implementar reintentos con backoff en tu capa
11 La IP del lead se envía fija (10.0.1.1) No refleja el origen real Limitación conocida del SDK
12 Namespace Codwelt\SDK\SmartHome pese al nombre homlity/… Confusión al importar Documentado arriba

Envoltorio recomendado

final class HomlityRepositorio
{
    public function __construct(
        private SDKSmartHomeBuilder $sdk,
        private ?\Psr\Log\LoggerInterface $logger = null
    ) {}

    /** @return InmueblePreview[] */
    public function inmuebles(): array
    {
        $resultado = $this->sdk->obtenerPropiedades();

        if ($resultado === []) {
            $this->logger?->warning('[Homlity] Inventario vacío: ¿API caída o credenciales inválidas?');
        }

        return $resultado;
    }

    public function inmueble(string $codigo): ?InmuebleDetail
    {
        try {
            $inmueble = $this->sdk->obtenerPropiedad($codigo);
        } catch (\Throwable $e) {
            $this->logger?->error('[Homlity] Error al obtener ' . $codigo, ['excepcion' => $e]);
            return null;
        }

        // Un código inexistente devuelve un objeto vacío, no null
        if ($inmueble === null || @$inmueble->getCodigo() === null) {
            return null;
        }

        return $inmueble;
    }

    /** @return Fotos[] */
    public function fotos(InmueblePreview $inmueble): array
    {
        try {
            $fotos = @$inmueble->getFotos();
            return is_array($fotos) ? $fotos : [];
        } catch (\Throwable) {
            return [];
        }
    }

    /** @return Caracteristicas[] */
    public function caracteristicas(InmueblePreview $inmueble): array
    {
        try {
            $cars = @$inmueble->getCaracteristicas();
            return is_array($cars) ? $cars : [];
        } catch (\Throwable) {
            return [];
        }
    }
}

Endpoints REST subyacentes

Si trabajas en otro lenguaje (Node, Python, Go…), estos son los endpoints que el SDK consume. Base: https://api.smart-home.com.co

Método SDK Verbo Ruta
obtenerPropiedades() GET /api/v1Inmobiliario/getUnits/{companyCode}/{projectCode}
obtenerPropiedad() GET /api/v1Inmobiliario/getUnit/{companyCode}/{projectCode}/{unitCode}
obtenerUbicaciones() GET /api/v1Inmobiliario/getUnitLocation/{companyCode}/{projectCode}
obtenerTipoPropiedades() GET /api/v1Inmobiliario/getUnitType/{companyCode}/{projectCode}
agregarLead() POST /api/SmartInmobiliario

Todas las peticiones envían Accept: application/json y aceptan Content-Encoding: gzip.

Respuesta de getUnits

{
  "returnCode": "SUCCESS",
  "returnDesc": "",
  "totalRecords": 15,
  "pages": 1,
  "currentPage": 1,
  "recordsPerPage": 10000,
  "units": [
    {
      "index": 1,
      "code": "20230022",
      "moduleId": "35f98252-6db1-43d6-bd6b-18752b883c41",
      "name": "VENTA DE OFICINA EN CENTRO COMERCIAL PRIMAVERA",
      "address": "Cll 15 # 40-01",
      "area": 73.00,
      "privateArea": 73.00,
      "lotArea": 73.00,
      "price": 650000000.00,
      "maintenanceFee": 0.00,
      "monthlyFee": 0.00,
      "tax": 0.00,
      "totalMonthlyFee": 0.00,
      "hasTax": false,
      "featuredProperty": false,
      "bedroom": 1.0,
      "bathrooms": 1.0,
      "garages": 0,
      "description": "Oficina ubicada en el Centro Comercial Primavera…",
      "latitude": 4.134324,
      "longitude": -73.640162,
      "zone": null,
      "zoneId": null,
      "neighborhood": "villavicencio urbano",
      "neighborhoodId": null,
      "locality": null,
      "localityId": null,
      "city": "VILLAVICENCIO",
      "cityId": "454cca01-1f8e-4c4e-91ba-24ce3a37f19f",
      "strata": "4",
      "floor": 3,
      "yearBuild": 0,
      "unitCategory": 2,
      "unitCategoryDescription": "Venta",
      "unitType": 4,
      "unitTypeDescription": "Oficina",
      "mainImageURL": "https://storage.googleapis.com/…",
      "status": 0,
      "images": [
        { "title": "4.jpeg", "mediaType": 1, "source": "https://…", "sequence": 4 }
      ],
      "features": [ { "name": "BALCON" }, { "name": "ILUMINACION" } ],
      "seller": {
        "userId": "9ad2e1bb-37e2-4c15-8999-a673d2e928a5",
        "name": "Vanessa Espinosa Quintero",
        "mobileNumber": "3216031353",
        "email": "comercial@ejemplo.com"
      },
      "agent": { "userId": "", "name": "", "mobileNumber": "", "email": "" },
      "mls": null,
      "listingSites": [
        { "name": "Proppit", "code": "20230021", "status": "1" },
        { "name": "FincaRaíz", "code": "10307114", "status": "1" }
      ]
    }
  ]
}

Los campos agent, listingSites, featuredProperty, status e index existen en la respuesta pero el SDK aún no los expone mediante getters.

Respuesta de getUnitLocation

{
  "countries":     [ { "countryId": "8aa4c927-…", "name": "Colombia", "count": 0 } ],
  "cities":        [ { "countryId": "8aa4c927-…", "cityId": "454cca01-…", "name": "VILLAVICENCIO", "count": 12 } ],
  "localities":    [ { "cityId": "454cca01-…", "localityId": "7831f124-…", "name": "otro", "count": 11 } ],
  "neighborhoods": [ { "countryId": "8aa4c927-…", "cityId": "454cca01-…", "neighborhoodId": "44d79774-…", "name": "villavicencio urbano", "count": 2 } ],
  "zones":         [],
  "returnCode":    "SUCCESS",
  "returnDesc":    ""
}

Respuesta de getUnitType

{
  "types": [
    { "code": 1,  "name": "Apartamento",   "count": 6 },
    { "code": 2,  "name": "Casa",          "count": 6 },
    { "code": 4,  "name": "Oficina",       "count": 2 },
    { "code": 23, "name": "Casa Campestre","count": 1 },
    { "code": 0,  "name": "Sin Definir",   "count": 2 }
  ],
  "returnCode": "SUCCESS",
  "returnDesc": ""
}

Cuerpo de POST /api/SmartInmobiliario

{
  "first_name":    "Ana Ramírez",
  "email":         "ana@ejemplo.com",
  "mobile_number": "+573001112233",
  "origin":        "mi-inmobiliaria.com",
  "comment":       "Me interesa agendar una visita",
  "projectId":     "-IxD-MeYCo32TRLf…",
  "moduleId":      "35f98252-6db1-43d6-bd6b-18752b883c41",
  "ip":            "10.0.1.1"
}

Se envía como application/x-www-form-urlencoded.

Solución de problemas

obtenerPropiedades() devuelve un array vacío

Recuerda que el SDK no lanza excepciones. Comprueba en este orden:

  1. Que llamaste a setCompanyCode() y setProjectCode() antes de consultar.
  2. Que los códigos sean correctos (sin espacios ni saltos de línea al copiarlos).
  3. Que el servidor tenga salida HTTPS hacia api.smart-home.com.co.
  4. Que la extensión curl esté habilitada: php -m | grep curl.

Diagnostica el endpoint directamente:

curl -s --compressed -H 'Accept: application/json' \
  "https://api.smart-home.com.co/api/v1Inmobiliario/getUnits/TU_COMPANY/TU_PROJECT" | head -c 400

Una respuesta correcta empieza con {"returnCode":"SUCCESS".

Warning: foreach() argument must be of type array|object, null given

Estás llamando a getFotos() o getCaracteristicas() sobre un inmueble cuyo campo images o features viene null. Es un escenario habitual, no un error de tu código.

En PHP puro es solo un aviso y el método devuelve []. En Laravel, Symfony o cualquier proyecto con un set_error_handler que escala los avisos, se convierte en una ErrorException. Usa el accesor seguro (@ + try/catch) descrito en ficha de inmueble.

El mismo aviso aparece dentro de obtenerTipoPropiedades() cuando el proyecto no tiene tipos.

Un inmueble responde con todos los campos en null

obtenerPropiedad() con un código inexistente no devuelve null: devuelve un InmuebleDetail cuyo array interno es null. Cada getter emite Trying to access array offset on value of type null y devuelve null.

Valida siempre dos condiciones:

if ($propiedad === null || $propiedad->getCodigo() === null) {
    // no existe
}
El lead no aparece en el CRM
  1. ¿Llamaste a setProjectId()? Sin él, el lead viaja sin proyecto.
  2. ¿Usaste la clave commentario con doble m?
  3. ¿Enviaste getModuleId() (UUID) en codigo, y no getCodigo()?
  4. ¿agregarLead() devolvió null? Entonces la petición HTTP falló.
La respuesta llega comprimida o ilegible

El SDK pide gzip. Si tu PHP no tiene ext-zlib, cURL no puede descomprimir. Verifica con php -m | grep zlib.

Composer instala una versión con otra API

Es el caso descrito en Instalación: la etiqueta v2.2.1 expone SDKSmartInmoBuilder en vez de SDKSmartHomeBuilder. Usa dev-main mientras se publica la versión estable.

Class "Codwelt\SDK\SmartHome\SDKSmartHomeBuilder" not found
  1. ¿Incluiste vendor/autoload.php?
  2. ¿Estás en dev-main? En v2.2.1 esa clase no existe.
  3. Regenera el autoloader: composer dump-autoload.

Pruebas

composer install
./vendor/bin/phpunit

La suite actual (tests/IndexTestCase.php) es un esqueleto mínimo y no cubre la API vigente: construye SDKSmartHomeBuilder pasando el companyCode al constructor, cuando el constructor espera un endpoint. Trátala como código heredado, no como referencia de uso.

Para probar contra la API real sin tocar el CRM, usa únicamente los métodos de lectura y las credenciales de demostración incluidas en index.php.

Versionado y changelog

El proyecto sigue SemVer y Keep a Changelog. Consulta CHANGELOG.md.

Versión Estado API pública
dev-main Actual, sin etiquetar SDKSmartHomeBuilder + InfraStructure\*
v2.2.1 Última etiqueta en Packagist SDKSmartInmoBuilder::build() + SmartHomeFacade
v1.x Obsoleta

Soporte

Canal Enlace
Sitio principal https://homlity.com/
Portal de desarrolladores https://homlity.com/desarrolladores/
Repositorio https://github.com/homlity/sdk-smartinmobiliario
Reportar un error https://github.com/homlity/sdk-smartinmobiliario/issues
Packagist https://packagist.org/packages/homlity/sdk-smarthome

Otros SDK de Homlity

Paquete Para qué sirve
homlity/sdk-smarthome Inventario inmobiliario y leads (este paquete)
homlity/sdk-proppit Publicación de inmuebles en Proppit desde PHP/Laravel

Homlity para desarrolladores

Hecho por Homlity · homlity.com