homlity/sdk-wasi-php8

SDK de Homlity en PHP para el CRM inmobiliario Wasi: crear, actualizar, buscar y publicar inmuebles con la API v1 de api.wasi.co

Maintainers

Package info

github.com/homlity/sdk-wasi-php8

Homepage

Documentation

pkg:composer/homlity/sdk-wasi-php8

Transparency log

Statistics

Installs: 7

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v2.0.0 2026-08-24 23:58 UTC

This package is auto-updated.

Last update: 2026-08-24 23:59:47 UTC


README

Homlity

Homlity SDK para Wasi

SDK oficial de Homlity en PHP para crear, actualizar, buscar y publicar inmuebles en el CRM inmobiliario Wasi (API v1).

homlity.com · Portal de desarrolladores · GitHub · Packagist

PHP 8.0+ Wasi API v1 Packagist Framework agnostic MIT

📚 Documentación

Documento Contenido
Este README Instalación, referencia del SDK, ejemplos y troubleshooting
docs/wasi-api-reference.md Referencia completa de la API de Wasi: endpoints, campos modificables, filtros y formas de respuesta
docs/laravel.md Integración en Laravel: contenedor, repositorio, jobs, comando artisan, cache de catálogos, testing y logging seguro
docs/desarrolladores.html Página web para desarrolladores con la marca Homlity — abrir en el navegador
examples/ 8 scripts ejecutables, del hola mundo a la sincronización completa

Tabla de contenidos

  1. ¿Para qué sirve este SDK?
  2. Arquitectura en 30 segundos
  3. Requisitos
  4. Instalación
  5. Credenciales: id_company y wasi_token
  6. Quick start
  7. Referencia del SDK
  8. Campos de un inmueble
  9. Filtros de búsqueda
  10. Manejo de respuestas y errores
  11. Integración con Laravel
  12. Receta completa: sincronizar un inmueble de tu CRM a Wasi
  13. Catálogos de Wasi (IDs que necesitas)
  14. Limitaciones conocidas y buenas prácticas
  15. Troubleshooting
  16. Ejemplos ejecutables y tests
  17. Extender el SDK
  18. Soporte y contribución

1. ¿Para qué sirve este SDK?

Wasi es el CRM que usan miles de inmobiliarias en LatAm para administrar su inventario de inmuebles, sus asesores y sus clientes. Su API v1 (https://api.wasi.co/v1/) permite leer y escribir ese inventario por HTTP en lugar de cargarlo a mano desde el panel web.

Este paquete es la capa PHP que Homlity —y cualquier portal, lonja, landing o CRM que se integre con Homlity— usa para hablar con esa API sin escribir cURL a mano.

Qué te resuelve

Problema Qué hace el SDK
Construir URLs con credenciales en cada llamada ConfigSDK guarda id_company + wasi_token y RequestsFather::buildUrl() los inyecta en todas las peticiones
Recordar las rutas de la API Cada operación es un método del facade: propertyStore(), propertyUpdate(), searchPropertys(), mediaStore(), getAllUsers()
Serializar payloads y subir imágenes Codifica el JSON del inmueble y arma el multipart/form-data con CURLFile para las fotos
Leer la respuesta cruda de cURL Objetos Response con isOk(), getBody(), getBodyArray(), getBodyRaw() y helpers como getTotal() / getUsers()
Acoplarte a un framework Cero dependencias de framework: solo php-curl-class. Funciona en Laravel, Symfony, WordPress o PHP plano
Distinguir «llegó la petición» de «funcionó la operación» isOk() valida el transporte y el status del cuerpo; getErrorMessage() te da el motivo
Peticiones colgadas Timeouts por defecto (30 s / 10 s de conexión) y reintentos configurables en ConfigSDK
Booleanos y arrays en la query string Serializa true/false y features[]=2&features[]=4 como espera Wasi

Qué NO hace (por diseño, hoy)

  • No cubre toda la API de Wasi. Implementa 5 operaciones (ver §7). Los catálogos (países, ciudades, tipos de inmueble, monedas) se consultan aparte — ver §13.
  • No valida el contenido del payload: comprueba que sea serializable, pero si mandas un id_city inexistente el error lo devuelve Wasi.
  • No cachea catálogos ni resultados de búsqueda.
  • No gestiona colas ni jobs — eso lo pone tu aplicación.

Si necesitas alguna de estas capas, mira §17 Extender el SDK.

2. Arquitectura en 30 segundos

El SDK sigue una separación App / Infrastructure muy simple:

Tu aplicación
     │
     ▼
┌──────────────────────────────────────────────────────────┐
│  App\WasiSDKFacade          ← único punto de entrada      │
│  App\ConfigSDK              ← id_company + wasi_token     │
└──────────────────────────────────────────────────────────┘
     │  crea, configura y ejecuta
     ▼
┌──────────────────────────────────────────────────────────┐
│  InfraStructure\Requets\RequestsFather  (abstracta)       │
│    · setConfig(ConfigSDK)                                 │
│    · getEndpoint(): string      ← la implementa cada hija │
│    · buildUrl()  →  https://api.wasi.co/{endpoint}        │
│                     ?id_company=…&wasi_token=…            │
│                                                           │
│    ├─ PropertyStoreRequest       POST v1/property/add     │
│    ├─ PropertyUpdateRequest      POST v1/property/update/{id}
│    ├─ SearchPropertysRequests    GET  v1/property/search  │
│    ├─ MediaStoreRequest          POST v1/property/upload-image/{id}
│    └─ UserGetAllRequest          GET  v1/user/all-users   │
└──────────────────────────────────────────────────────────┘
     │  devuelve
     ▼
┌──────────────────────────────────────────────────────────┐
│  InfraStructure\Responses\ResponseFather                  │
│    · isOk()          ← transporte Y status del cuerpo     │
│    · isTransportOk() · getStatus() · getErrorCode()        │
│    · getErrorMessage() · getHttpStatusCode()               │
│    · getBody() · getBodyRaw() · getBodyArray() · getRows() │
│    ├─ PropertyStoreResponse     (+ getIdProperty())        │
│    ├─ PropertyUpdateResponse                              │
│    ├─ SearchPropertysResponse   (+ getTotal(), getProperties())
│    ├─ MediaStoreResponse        (+ getIdImage())           │
│    └─ UserGetAllResponse        (+ getUsers())             │
└──────────────────────────────────────────────────────────┘

Excepciones · Homlity\WASI\SDK\Exceptions
   WasiException            ← base, captúrala para atrapar todo
   ├─ ConfigException       ← faltan credenciales o config
   └─ RequestException      ← payload o archivo inválidos

Consecuencia práctica: añadir un endpoint nuevo = una clase Request (con su getEndpoint() y su run()), una clase Response, y un método en el facade. Ver §17.

Mapa de archivos

Archivo Rol
src/App/WasiSDKFacade.php Fachada pública. Es lo único que tu código necesita conocer
src/App/ConfigSDK.php Contenedor de credenciales
src/InfraStructure/Requets/RequestsFather.php Base abstracta: config + construcción de URL
src/InfraStructure/Requets/PropertyStoreRequest.php Crear inmueble
src/InfraStructure/Requets/PropertyUpdateRequest.php Actualizar inmueble
src/InfraStructure/Requets/SearchPropertysRequests.php Buscar inmuebles
src/InfraStructure/Requets/MediaStoreRequest.php Subir imagen
src/InfraStructure/Requets/UserGetAllRequest.php Listar asesores
src/InfraStructure/Responses/ResponseFather.php Base de todas las respuestas
src/Exceptions/ WasiException, ConfigException, RequestException
tests/ Suite PHPUnit — composer test

Nota sobre el namespace: la carpeta se llama Requets (sin la s), y así aparece en el namespace Homlity\WASI\SDK\InfraStructure\Requets. No es un typo de esta documentación: es el nombre real del paquete publicado. No lo cambies sin publicar una versión mayor.

3. Requisitos

Requisito Versión Por qué
PHP >= 8.0 Tipos de propiedad, promoción de constructor y argumentos con nombre
ext-curl * Todas las peticiones HTTP
ext-json * Serialización de payloads y parseo de respuestas
ext-mbstring * Manejo de textos con tildes/ñ en títulos y observaciones
php-curl-class/php-curl-class ^11.0 Cliente HTTP usado internamente
Plan Pro de Wasi La API solo está disponible en plan Pro

Comprueba tus extensiones:

php -m | grep -E 'curl|json|mbstring'

4. Instalación

composer require homlity/sdk-wasi-php8

Si tu proyecto aún no tiene minimum-stability configurado y Composer se queja, fija la versión:

composer require homlity/sdk-wasi-php8:^1.0

Luego, en tu código:

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

use Homlity\WASI\SDK\App\ConfigSDK;
use Homlity\WASI\SDK\App\WasiSDKFacade;

Actualizar desde la v1

La v2 corrige varios comportamientos que causaban bugs silenciosos. La migración es corta:

Si en la v1 escribías En la v2
if ($res->isOk()) para saber si funcionó Sigue igual — y ahora detecta los errores de Wasi
$body = $res->getBodyArray(); if ($body['status'] === 'success') if ($res->isOk())
$body['id_property'] $res->getIdProperty()
$res->getTotal() devolvía false al fallar Devuelve ?int — usa ?? 0 si lo sumas
array_filter($body, 'is_numeric', ARRAY_FILTER_USE_KEY) $res->getProperties() o $res->getRows()
'for_sale' => 'true' (cadena) 'for_sale' => true (booleano) — la cadena sigue funcionando
['filters' => ['id_city' => 61]] Sigue igual, o directamente ['id_city' => 61]
Comprobar is_readable() antes de mediaStore() El SDK valida y lanza RequestException
PHP 7.4 PHP 8.0 o superior

Lo único que puede romperse: si tu código dependía de que isOk() devolviera true ante un {"status":"error"} —normalmente sin saberlo—, ahora devolverá false. Ese es precisamente el bug que la v2 arregla. Si necesitas el comportamiento antiguo en algún punto concreto, usa isTransportOk().

composer require homlity/sdk-wasi-php8:^2.0
Instalar desde el repositorio Git (sin pasar por Packagist)
{
  "repositories": [
    { "type": "vcs", "url": "https://github.com/homlity/sdk-wasi-php8" }
  ],
  "require": {
    "homlity/sdk-wasi-php8": "dev-main"
  }
}

5. Credenciales: id_company y wasi_token

La API de Wasi se autentica con dos valores que viajan en cada petición:

Valor Qué es
id_company Identificador numérico de tu empresa en Wasi
wasi_token Token de acceso de la empresa

Cómo obtenerlos

  1. Entra a tu cuenta de Wasi (requiere plan Pro).
  2. Ve a Configuración → Ajustes generales.
  3. Busca la sección «API de Wasi» y genera el token.
  4. Wasi te muestra id_company y wasi_token. Cópialos.

Cómo guardarlos

Nunca los pongas en el código. Usa variables de entorno:

# .env
WASI_ID_COMPANY=1234567890
WASI_TOKEN=ABCD_EFGH_IJKL_MNOP
$config = new ConfigSDK();
$config->setCompanyId(getenv('WASI_ID_COMPANY'));
$config->setToken(getenv('WASI_TOKEN'));

⚠️ Seguridad: este SDK envía las credenciales en la query string (?id_company=…&wasi_token=…), que es como Wasi documenta el acceso vía GET. Eso significa que el token puede acabar en logs de proxies, access logs de tu servidor, APM o trazas de error. Nunca vuelques la URL construida (buildUrl()) en un log y asegúrate de que tu APM redacta el parámetro wasi_token. La propia documentación de Wasi recomienda enviar las credenciales por POST cuando sea posible.

6. Quick start

<?php

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

use Homlity\WASI\SDK\App\ConfigSDK;
use Homlity\WASI\SDK\App\WasiSDKFacade;

// 1. Credenciales
$config = new ConfigSDK(getenv('WASI_ID_COMPANY'), getenv('WASI_TOKEN'));

// 2. Fachada — es el único objeto que necesitas
$wasi = new WasiSDKFacade($config);

// 3. Buscar los 10 inmuebles en venta más recientes
$result = $wasi->searchPropertys([
    'for_sale' => true,
    'short'    => true,   // omite galerías: respuesta mucho menor
    'take'     => 10,
    'order_by' => 'created_at',
    'order'    => 'desc',
]);

// isOk() comprueba el transporte Y el `status` del cuerpo
if (!$result->isOk()) {
    throw new RuntimeException('Wasi respondió con error: ' . $result->getErrorMessage());
}

echo 'Total de inmuebles que coinciden: ' . $result->getTotal() . PHP_EOL;

foreach ($result->getProperties() as $property) {
    printf(
        "#%s · %s · %s · %s\n",
        $property['id_property'],
        $property['title'] ?? 's/título',
        $property['city_label'] ?? '-',
        $property['sale_price_label'] ?? '-'
    );
}

7. Referencia del SDK

App\ConfigSDK

Credenciales y opciones de transporte.

Método Firma Descripción
__construct __construct($idCompany = null, $token = null) Ambos opcionales
setCompanyId setCompanyId($id): self Acepta string o int; recorta espacios
setToken setToken(?string $token): self Recorta espacios
setHost setHost(string $host): self URL base — útil para apuntar a un mock
setTimeout setTimeout(int $s): self Petición completa. Por defecto 30 s
setConnectTimeout setConnectTimeout(int $s): self Establecer conexión. Por defecto 10 s
setRetries setRetries(int $n): self Reintentos ante fallo de transporte. Por defecto 0
assertUsable assertUsable(): void Lanza ConfigException si faltan credenciales

Los getters correspondientes (getIdCompany(), getToken(), getHost(), getTimeout(), getConnectTimeout(), getRetries()) devuelven lo esperado.

$config = new ConfigSDK(getenv('WASI_ID_COMPANY'), getenv('WASI_TOKEN'));

// O con setters, que encadenan
$config = (new ConfigSDK())
    ->setCompanyId(getenv('WASI_ID_COMPANY'))
    ->setToken(getenv('WASI_TOKEN'))
    ->setTimeout(60)
    ->setRetries(2);

Los setters recortan espacios y saltos de línea: el fallo más común es copiar el token del .env con un \n colado. Si faltan credenciales, el SDK lanza ConfigException antes de salir a la red, en lugar de dejar que Wasi responda id_company or wasi_token is wrong.

App\WasiSDKFacade

$wasi = new WasiSDKFacade($config);
Método Endpoint Wasi Verbo Devuelve
propertyStore(array $data) v1/property/add POST PropertyStoreResponse
propertyUpdate(string $idProp, array $data) v1/property/update/{id} POST PropertyUpdateResponse
searchPropertys(array $filters = []) v1/property/search GET SearchPropertysResponse
mediaStore($path, $idProperty, ?$description, ?$position) v1/property/upload-image/{id} POST MediaStoreResponse
getAllUsers() v1/user/all-users GET UserGetAllResponse

propertyStore(array $data): PropertyStoreResponse

Crea un inmueble. El array se envía como cuerpo JSON.

$response = $wasi->propertyStore([
    'id_user'           => 45678,   // asesor responsable (ver getAllUsers)
    'id_property_type'  => 2,       // 2 = Apartamento
    'id_country'        => 1,       // 1 = Colombia
    'id_region'         => 5,
    'id_city'           => 61,
    'id_currency'       => 12,
    'for_sale'          => true,
    'for_rent'          => false,
    'sale_price'        => 450000000,
    'title'             => 'Apartamento con vista al parque',
    'address'           => 'Cra 45 #23-10',
    'area'              => 78,
    'built_area'        => 72,
    'bedrooms'          => 3,
    'bathrooms'         => 2,
    'garages'           => 1,
    'floor'             => 7,
    'stratum'           => 4,
    'observations'      => 'Remodelado en 2024, cocina abierta, balcón.',
    'latitude'          => '6.244203',
    'longitude'         => '-75.581212',
    'id_status_on_page' => 1,       // visible en la web
    'reference'         => 'HOM-2026-0451', // tu referencia interna
    'features'          => [2, 4],  // arrays → features[]=2&features[]=4
]);

// isOk() comprueba el transporte Y el `status` del cuerpo
if (!$response->isOk()) {
    throw new RuntimeException('Wasi rechazó el inmueble: ' . $response->getErrorMessage());
}

$idProperty = $response->getIdProperty();   // ← guárdalo en tu base de datos

Respuesta de Wasi:

{ "id_property": 123456, "status": "success" }

💡 Guarda siempre el id_property devuelto junto al registro de tu propio sistema. Es la única forma de volver a actualizar ese inmueble o de subirle fotos.

💡 Usa el campo reference para guardar tu identificador interno dentro de Wasi. Así puedes reconciliar en ambos sentidos.

propertyUpdate(string $idProp, array $data): PropertyUpdateResponse

Actualiza un inmueble existente. Envía solo los campos que cambian.

$response = $wasi->propertyUpdate(123456, [
    'sale_price'        => 425000000,
    'observations'      => 'Precio actualizado — negociable.',
    'id_status_on_page' => 1,
]);

if (!$response->isOk()) {
    error_log('No se actualizó: ' . $response->getErrorMessage());
}

El identificador acepta string o int. Un identificador vacío lanza RequestException en lugar de construir una URL rota.

Respuesta de Wasi:

{ "status": "success" }

⚠️ Solo puedes enviar campos marcados como modificables en Wasi. Ver §8. Campos calculados (sale_price_label, city_label, galleries, …) se ignoran o provocan error.

💡 Los campos array (features, portals) se separan solos y viajan en la query string, que es donde Wasi los espera. Tú los pasas como un array normal.

searchPropertys(array $filters = []): SearchPropertysResponse

Busca inmuebles. Acepta las dos formas de pasar los filtros:

// Anidada — más legible cuando mezclas filtros con otras claves
$wasi->searchPropertys([
    'filters' => [
        'id_city'   => 61,
        'for_sale'  => true,
        'min_price' => 200000000,
        'max_price' => 600000000,
        'take'      => 25,
    ],
]);

// En plano — equivalente
$wasi->searchPropertys([
    'id_city'  => 61,
    'for_sale' => true,
]);

En la v1 solo funcionaba la forma anidada, y la plana se descartaba en silencio. Desde la v2 ambas son válidas y equivalentes.

Helpers de la respuesta:

$result = $wasi->searchPropertys(['for_rent' => true]);

$result->isOk();           // bool — transporte + status del cuerpo
$result->getTotal();       // ?int — total de coincidencias (no de la página)
$result->getProperties();  // array<int, array> — solo los inmuebles
$result->getBodyArray();   // array completo, con "total" y "status"

Forma de la respuesta de Wasi:

{
  "0": { "id_property": "123456", "title": "Apartamento…", "sale_price": "450000000", "…": "" },
  "1": { "id_property": "123457", "…": "" },
  "total": 348,
  "status": "success"
}

getProperties() te devuelve solo los registros, ya reindexados desde 0, sin que tengas que filtrar las claves numéricas a mano.

💡 Pasa 'short' => true para omitir galerías y características y aligerar mucho la respuesta cuando solo necesitas listar.

Ver todos los filtros en §9.

mediaStore($path, $idProperty, ?$description, ?$position): MediaStoreResponse

Sube una imagen a la galería de un inmueble. $path es una ruta local del sistema de archivos (no una URL): el SDK la envuelve en un CURLFile y la manda como multipart/form-data.

use Homlity\WASI\SDK\Exceptions\RequestException;

try {
    $response = $wasi->mediaStore(
        '/var/www/storage/fotos/fachada.jpg',
        123456,
        description: 'Fachada principal',
        position: 1
    );
} catch (RequestException $e) {
    // El archivo no existe, no es legible, está vacío, o le pasaste una URL
    error_log($e->getMessage());
}

El SDK valida el archivo antes de enviarlo y lanza RequestException con un mensaje claro si:

Situación Mensaje
Ruta vacía Falta la ruta de la imagen…
Le pasas una URL mediaStore() espera una ruta local, no una URL…
El archivo no existe La imagen no existe: …
Existe pero PHP no puede leerlo La imagen existe pero no es legible por el usuario de PHP: …
Pesa 0 bytes La imagen está vacía (0 bytes): …

En la v1 estos casos salían como una petición con adjunto vacío y un error opaco de Wasi.

Para subir varias fotos, itera — el endpoint acepta una imagen por petición:

$fotos = [
    '/var/www/storage/fotos/01-fachada.jpg',
    '/var/www/storage/fotos/02-sala.jpg',
    '/var/www/storage/fotos/03-cocina.jpg',
];

foreach ($fotos as $i => $foto) {
    try {
        $res = $wasi->mediaStore($foto, $idProperty, position: $i + 1);
    } catch (RequestException $e) {
        error_log($e->getMessage());
        continue;
    }

    if (!$res->isOk()) {
        error_log("Falló la subida de {$foto}: " . $res->getErrorMessage());
    }

    // Sé amable con la API: no dispares 40 uploads en paralelo
    usleep(300_000);
}

💡 La primera imagen que subas suele quedar como principal. Sube la fachada primero, o usa position para fijar el orden explícitamente.

getAllUsers(): UserGetAllResponse

Lista los usuarios (asesores) de la empresa. Lo necesitas para obtener el id_user que va en propertyStore().

$response = $wasi->getAllUsers();

if (!$response->isOk()) {
    throw new RuntimeException('No se pudieron leer los asesores: ' . $response->getErrorMessage());
}

foreach ($response->getUsers() as $user) {
    printf(
        "%s · %s %s · %s · %s\n",
        $user['id_user'],
        $user['first_name'] ?? '',
        $user['last_name'] ?? '',
        $user['email'] ?? '-',
        $user['user_type'] ?? '-'   // ADMIN, REALTOR, …
    );
}

getUsers() devuelve una lista limpia: sin la clave status y reindexada desde 0, así que puedes iterarla directamente.

Objetos de respuesta

Todos heredan de InfraStructure\Responses\ResponseFather:

Método Devuelve Descripción
isOk() bool Lo que quieres el 95 % del tiempo. true solo si cURL no falló y el cuerpo trae status: success
isTransportOk() bool Solo si la petición HTTP se completó. Es lo que hacía isOk() en la v1
getStatus() ?string success, error, o null si el endpoint no devuelve status
getErrorCode() ?string Código de error de Wasi
getErrorMessage() ?string Motivo legible del fallo, o null si todo fue bien
getHttpStatusCode() int Código HTTP de la respuesta
getBodyArray() ?array Cuerpo como array. null si no era JSON válido. Se cachea
getRows() array Solo los registros, sin status ni total, reindexados desde 0
getBody() mixed Respuesta ya decodificada por php-curl-class
getBodyRaw() ?string Cuerpo crudo

Extras por clase:

Clase Extra
PropertyStoreResponse getIdProperty(): ?int
SearchPropertysResponse getTotal(): ?int · getProperties(): array
MediaStoreResponse getIdImage(): ?int
UserGetAllResponse getUsers(): array

Excepciones

Excepción Cuándo
Exceptions\WasiException Base de todas. Captúrala para atrapar cualquier fallo del SDK
Exceptions\ConfigException Faltan credenciales o no llamaste a setConfig()
Exceptions\RequestException El payload no es serializable, falta el id_property, o el archivo de imagen no sirve

Las excepciones cubren errores de programación o de entorno, detectables antes de salir a la red. Los errores de la API (credenciales incorrectas, campo rechazado, inmueble inexistente) no lanzan: llegan en la respuesta y se leen con isOk() y getErrorMessage().

8. Campos de un inmueble

Estos son los campos modificables de la API de Wasi, es decir, los que puedes mandar en propertyStore() y propertyUpdate().

Obligatorios en la práctica

Campo Tipo Notas
id_user número Asesor responsable. Sácalo de getAllUsers()
id_property_type número Casa, apartamento, lote… (v1/property-type/all)
id_country / id_region / id_city número Jerarquía de ubicación
for_sale / for_rent / for_transfer booleano Al menos uno debe ser true
sale_price y/o rent_price número Coherente con los flags anteriores
id_currency número Moneda del precio
title texto Título público del anuncio

Ubicación

Campo Tipo
id_country, id_region, id_city, id_location, id_zone número
zone texto (nombre libre de la zona)
address texto
zip_code texto
latitude, longitude texto
id_publish_on_map número (cómo se publica en el mapa)

Precio y moneda

Campo Tipo
sale_price número
rent_price número
maintenance_fee número (administración)
id_currency número
id_rents_type número (tipo de arriendo)

Áreas

Campo Tipo
area + id_unit_area número + número
built_area + id_unit_built_area número + número
private_area + id_unit_private_area número + número

Cada área lleva su unidad aparte (m², ha, ft²…). Si mandas area sin id_unit_area, Wasi asume la unidad por defecto de la empresa.

Características físicas

Campo Tipo Rango
bedrooms número 0–29
bathrooms número 0–9
half_bathrooms número
garages número 0–29
floor número 1–60
stratum número Estrato socioeconómico (Colombia)
building_date número Año de construcción
id_property_condition número Nuevo / usado / sobre planos

Publicación y contenido

Campo Tipo Notas
observations texto Descripción larga del anuncio
comment texto Nota interna (no pública)
video URL Video del inmueble
id_status_on_page número Estado en la web (activo, inactivo, vendido…)
id_availability número Disponibilidad
reference texto Tu referencia interna — úsala para reconciliar
label texto Etiqueta
network_share booleano Compartir en la red Wasi
tv_share booleano Compartir en Wasi TV
features array IDs de características (piscina, ascensor…)
portals array IDs de portales donde publicar

features y portals son arrays y el SDK ya los maneja. Wasi los espera en la query string (features[]=2&features[]=4), no en el cuerpo JSON; el SDK los separa solo. Tú los pasas como un array normal: 'features' => [2, 4].

La tabla completa y actualizada está en la referencia de campos de Wasi.

9. Filtros de búsqueda

Todos opcionales. Van dentro de ['filters' => [...]].

Coincidencia y texto

Filtro Tipo Descripción
match texto Búsqueda por palabra clave
title texto Filtra por título
id_property número Un inmueble concreto

Ubicación

Filtro Tipo Descripción
id_country número País
id_region número Departamento / provincia
id_city número Ciudad
id_zone texto Acepta varias zonas separadas por coma: '12,15,20'

Tipo y negocio

Filtro Tipo Descripción
id_property_type número Tipo de inmueble
for_sale booleano En venta
for_rent booleano En arriendo
for_transfer booleano En traspaso
id_status_on_page número Estado de publicación
id_user número Inmuebles de un asesor

Rangos

Filtro Tipo
min_price / max_price número
min_area / max_area número
min_bedrooms / max_bedrooms número
building_date número (año mínimo de construcción)

Paginación y orden

Parámetro Tipo Por defecto Notas
skip número 0 Offset
take número 10 Máximo 100
order texto desc asc | desc
order_by texto id_property id_property, title, id_country, id_region, id_city, id_property_type, sale_price, rent_price, visits, created_at
short booleano false true omite galerías y características

Ejemplo: paginar el catálogo completo

$skip     = 0;
$take     = 100;   // el máximo permitido
$todos    = [];

do {
    $res = $wasi->searchPropertys([
        'id_status_on_page' => 1,
        'short'             => true,   // sin galerías: mucho más rápido
        'take'              => $take,
        'skip'              => $skip,
        'order_by'          => 'id_property',
        'order'             => 'asc',  // orden estable al paginar
    ]);

    if (!$res->isOk()) {
        throw new RuntimeException('Wasi: ' . $res->getErrorMessage());
    }

    $pagina = $res->getProperties();
    $todos  = array_merge($todos, $pagina);

    $skip += $take;
} while (count($pagina) === $take);

echo count($todos) . " inmuebles descargados\n";

Booleanos: pásalos como true / false de PHP. El SDK los serializa a true / false en la query string, que es lo que Wasi lee. (http_build_query() convertiría false en cadena vacía y el filtro dejaría de aplicarse; por eso el SDK no lo usa.)

10. Manejo de respuestas y errores

Wasi devuelve HTTP 200 incluso cuando la operación falla. El resultado real está en el campo status del cuerpo:

{ "status": "error", "code": "0", "message": "id_company or wasi_token is wrong." }

isOk() cubre los dos casos

Desde la v2, isOk() devuelve true solo si cURL no falló y el cuerpo trae status: success. Es lo único que necesitas comprobar:

$response = $wasi->propertyStore($payload);

if (!$response->isOk()) {
    error_log('Wasi: ' . $response->getErrorMessage());
    return;
}

$idProperty = $response->getIdProperty();

getErrorMessage() elige el mensaje más útil disponible, en este orden:

  1. El message que devuelve Wasi.
  2. El error de cURL, si el fallo fue de transporte.
  3. Wasi devolvió una respuesta que no es JSON válido, si el cuerpo no se pudo decodificar.

Y devuelve null cuando la operación fue correcta, así que también sirve como aserción.

Distinguir un fallo de red de un rechazo de Wasi

Útil para decidir si reintentar: un timeout se reintenta, un campo rechazado no.

if (!$response->isOk()) {
    if (!$response->isTransportOk()) {
        throw new NetworkException($response->getErrorMessage());   // reintentable
    }

    throw new ValidationException($response->getErrorMessage());    // no reintentable
}
Método Qué comprueba
isOk() Transporte y status del cuerpo
isTransportOk() Solo que la petición HTTP se completara
getStatus() El valor literal de status
getErrorCode() El code que devuelve Wasi
getHttpStatusCode() El código HTTP real

Prefieres excepciones

El SDK no lanza ante errores de la API, para que decidas tú. Si te va mejor un flujo con excepciones, envuélvelo una vez:

use Homlity\WASI\SDK\InfraStructure\Responses\ResponseFather;

/** @throws RuntimeException */
function wasiOk(ResponseFather $res): array
{
    if (!$res->isOk()) {
        throw new RuntimeException(sprintf(
            'Wasi [%s]: %s',
            $res->getErrorCode() ?? 'sin código',
            $res->getErrorMessage() ?? 'error desconocido'
        ));
    }

    return $res->getBodyArray() ?? [];
}

Lo tienes listo en examples/bootstrap.php.

Errores frecuentes de Wasi

Mensaje Causa habitual Solución
id_company or wasi_token is wrong. Credenciales mal copiadas, token regenerado, o plan no-Pro Regenera el token en Configuración → Ajustes generales. Los setters ya recortan espacios
Campo no modificable Enviaste un campo calculado (*_label, galleries…) Envía solo campos modificables (§8)
Inmueble no encontrado id_property de otra empresa o borrado Verifica con searchPropertys(['id_property' => $id])
Respuesta vacía / no-JSON Timeout de red o mantenimiento de Wasi getBodyArray() devuelve null e isOk() es false. Reintenta con setRetries()

Timeouts y reintentos

Los valores por defecto son 30 s de petición y 10 s de conexión, y no hay reintentos. Se ajustan en ConfigSDK:

$config
    ->setTimeout(60)          // lotes grandes o redes lentas
    ->setConnectTimeout(5)    // falla rápido si Wasi no responde
    ->setRetries(2);          // reintenta ante fallo de transporte o 5xx

Los reintentos los aplica php-curl-class a nivel de transporte. Un {"status":"error"} de Wasi no se reintenta —ni debe—: es una respuesta válida a una petición inválida.

Cache de Wasi

La API de Wasi cachea algunas peticiones clave. Tras crear o actualizar un inmueble, puede tardar algunos minutos en aparecer con los datos nuevos en searchPropertys(). No lo interpretes como un fallo de escritura: si el add/update devolvió success, se guardó.

11. Integración con Laravel

El SDK no trae service provider. Regístralo tú — son 15 líneas.

config/wasi.php

<?php

return [
    'id_company' => env('WASI_ID_COMPANY'),
    'token'      => env('WASI_TOKEN'),
];

app/Providers/WasiServiceProvider.php

<?php

namespace App\Providers;

use Homlity\WASI\SDK\App\ConfigSDK;
use Homlity\WASI\SDK\App\WasiSDKFacade;
use Illuminate\Support\ServiceProvider;

class WasiServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        $this->app->singleton(ConfigSDK::class, function ($app) {
            return (new ConfigSDK(
                $app['config']->get('wasi.id_company'),
                $app['config']->get('wasi.token'),
            ))
                ->setTimeout(30)
                ->setRetries(2);
        });

        $this->app->singleton(WasiSDKFacade::class, fn ($app) => new WasiSDKFacade($app->make(ConfigSDK::class)));
    }
}

Regístralo en bootstrap/providers.php (Laravel 11+) o config/app.php (Laravel ≤10), y ya puedes inyectarlo:

use Homlity\WASI\SDK\App\WasiSDKFacade;

class PropertyController extends Controller
{
    public function index(WasiSDKFacade $wasi)
    {
        $result = $wasi->searchPropertys(['for_sale' => true, 'short' => true, 'take' => 24]);

        abort_if(!$result->isOk(), 502, $result->getErrorMessage());

        return view('propiedades.index', [
            'propiedades' => $result->getProperties(),
            'total'       => $result->getTotal(),
        ]);
    }
}

Job para publicar en segundo plano (recomendado: las subidas de imagen son lentas):

<?php

namespace App\Jobs;

use App\Models\Inmueble;
use Homlity\WASI\SDK\App\WasiSDKFacade;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;

class PublicarInmuebleEnWasi implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public int $tries = 3;
    public int $backoff = 30;

    public function __construct(private readonly int $inmuebleId) {}

    public function handle(WasiSDKFacade $wasi): void
    {
        $inmueble = Inmueble::findOrFail($this->inmuebleId);

        $res = $wasi->propertyStore($inmueble->toWasiPayload());

        if (!$res->isOk()) {
            throw new \RuntimeException('Wasi rechazó el inmueble: ' . $res->getErrorMessage());
        }

        $inmueble->update(['wasi_id' => $res->getIdProperty()]);

        foreach ($inmueble->fotos as $i => $foto) {
            $wasi->mediaStore(
                storage_path('app/' . $foto->path),
                $inmueble->wasi_id,
                position: $i + 1
            );
        }
    }
}

⚠️ Cuidado con el log de Laravel. Si atrapas una excepción de cURL y la vuelcas entera, el wasi_token puede acabar en storage/logs/laravel.log. Loguea solo el mensaje de negocio.

12. Receta completa: sincronizar un inmueble de tu CRM a Wasi

El flujo de extremo a extremo que usa Homlity:

1. getAllUsers()          → elegir el id_user del asesor
2. (catálogos)            → resolver id_city, id_property_type, id_currency
3. propertyStore()        → crear el inmueble  →  guardar id_property
4. mediaStore() × N       → subir las fotos
5. propertyUpdate()       → activar la publicación / ajustar precio
6. searchPropertys()      → verificar (recuerda el cache de Wasi)
<?php

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

use Homlity\WASI\SDK\App\ConfigSDK;
use Homlity\WASI\SDK\App\WasiSDKFacade;
use Homlity\WASI\SDK\Exceptions\RequestException;

$config = new ConfigSDK(getenv('WASI_ID_COMPANY'), getenv('WASI_TOKEN'));
$wasi   = new WasiSDKFacade($config);

// ── 1. Asesor responsable ────────────────────────────────────────────────────
$usersRes = $wasi->getAllUsers();
if (!$usersRes->isOk()) {
    exit('No se pudieron leer los asesores: ' . $usersRes->getErrorMessage() . PHP_EOL);
}

$usuarios = $usersRes->getUsers();
$asesor   = null;
foreach ($usuarios as $u) {
    if (($u['email'] ?? null) === 'asesor@miinmobiliaria.com') {
        $asesor = $u;
        break;
    }
}
$idUser = $asesor['id_user'] ?? $usuarios[0]['id_user'];

// ── 2. Crear el inmueble ─────────────────────────────────────────────────────
$payload = [
    'id_user'           => $idUser,
    'id_property_type'  => 2,
    'id_country'        => 1,
    'id_region'         => 5,
    'id_city'           => 61,
    'id_currency'       => 12,
    'for_sale'          => true,
    'sale_price'        => 450000000,
    'title'             => 'Apartamento con vista al parque',
    'address'           => 'Cra 45 #23-10',
    'area'              => 78,
    'built_area'        => 72,
    'bedrooms'          => 3,
    'bathrooms'         => 2,
    'garages'           => 1,
    'floor'             => 7,
    'stratum'           => 4,
    'observations'      => 'Remodelado en 2024. Cocina abierta, balcón con vista al parque.',
    'latitude'          => '6.244203',
    'longitude'         => '-75.581212',
    'reference'         => 'HOM-2026-0451',
    'id_status_on_page' => 1,
];

$createRes = $wasi->propertyStore($payload);

if (!$createRes->isOk()) {
    exit('Error creando el inmueble: ' . $createRes->getErrorMessage() . PHP_EOL);
}

$idProperty = $createRes->getIdProperty();
echo "Inmueble creado en Wasi con id {$idProperty}\n";
// 👉 Aquí: guarda $idProperty en tu base de datos junto al registro local.

// ── 3. Subir las fotos ───────────────────────────────────────────────────────
$fotos = glob(__DIR__ . '/fotos/HOM-2026-0451/*.jpg') ?: [];
sort($fotos); // la primera será la principal

$subidas = 0;
foreach ($fotos as $i => $foto) {
    try {
        // El SDK valida el archivo; si no sirve, lanza RequestException
        $res = $wasi->mediaStore($foto, $idProperty, position: $i + 1);
    } catch (RequestException $e) {
        echo '' . basename($foto) . ': ' . $e->getMessage() . "\n";
        continue;
    }

    if ($res->isOk()) {
        $subidas++;
        echo '' . basename($foto) . "\n";
    } else {
        echo '' . basename($foto) . ': ' . $res->getErrorMessage() . "\n";
    }

    usleep(300_000);
}
echo "{$subidas}/" . count($fotos) . " fotos subidas\n";

// ── 4. Ajuste posterior ──────────────────────────────────────────────────────
$wasi->propertyUpdate($idProperty, [
    'id_status_on_page' => 1,
    'comment'           => 'Sincronizado desde Homlity el ' . date('Y-m-d H:i'),
]);

// ── 5. Verificar (el cache de Wasi puede tardar minutos) ─────────────────────
$check = $wasi->searchPropertys(['id_property' => $idProperty]);
echo 'Coincidencias en búsqueda: ' . ($check->getTotal() ?? 0) . PHP_EOL;

13. Catálogos de Wasi (IDs que necesitas)

Los payloads de inmueble se construyen con IDs (id_city, id_property_type, id_currency…). Este SDK todavía no expone métodos para leer esos catálogos, pero son peticiones GET sencillas con las mismas credenciales:

Catálogo Endpoint
Países GET v1/location/all-countries
Un país GET v1/location/country/{id_country}
Ciudades de una región GET v1/location/cities-from-region/{id_region}
Una ciudad GET v1/location/city/{id_city}
Ciudades con inmuebles GET v1/location/cities-with-property
Tipos de inmueble GET v1/property-type/all
Un inmueble concreto GET v1/property/get/{id_property}
Un usuario GET v1/user/get/{id_user}
Eliminar imagen POST v1/property/remove-image/{id_property}?id={id_image}
Editar datos de imagen POST v1/gallery/image/update-data/{id_image}

Helper para consultarlos mientras no estén en el SDK:

function wasiGet(string $endpoint, ConfigSDK $config, array $query = []): array
{
    $url = 'https://api.wasi.co/' . ltrim($endpoint, '/') . '?' . http_build_query(array_merge([
        'id_company' => $config->getIdCompany(),
        'wasi_token' => $config->getToken(),
    ], $query));

    $curl = new \Curl\Curl();
    $curl->get($url);

    return json_decode((string) $curl->rawResponse, true) ?: [];
}

// Uso
$tipos = wasiGet('v1/property-type/all', $config);
foreach ($tipos as $k => $t) {
    if (is_numeric($k)) {
        echo $t['id_property_type'] . '' . $t['nombre'] . PHP_EOL;
    }
}

Ejemplo de respuesta de tipos de inmueble:

{
  "0": { "id_property_type": "1", "nombre": "Casa" },
  "1": { "id_property_type": "2", "nombre": "Apartamento" }
}

💡 Los catálogos cambian poquísimo. Cachéalos (Redis, archivo, tabla) en lugar de consultarlos en cada sincronización.

Documentación oficial completa: https://api.wasi.co/docs/guide/

14. Limitaciones conocidas y buenas prácticas

Limitaciones actuales

Limitación Impacto Workaround
Solo 5 endpoints cubiertos Catálogos, property/get y borrado quedan fuera Helper wasiGet() de §13
No valida el contenido del payload Un id_city inexistente lo rechaza Wasi, no el SDK Cachea los catálogos y valida contra ellos
Sin cache propia Cada llamada sale a la red Cachea tú los catálogos (§13)
Credenciales en query string Es como Wasi documenta el acceso GET; el token puede filtrarse a logs Nunca loguees URLs; redacta wasi_token en tu APM
Sin cliente HTTP inyectable No puedes mockear cURL desde fuera Envuelve el SDK en un repositorio tuyo y mockea ese (docs/laravel.md)

Arreglado en la v2

Antes (v1) Ahora (v2)
isOk() solo miraba si cURL falló Comprueba transporte y status del cuerpo
Filtros fuera de ['filters' => …] se ignoraban en silencio Ambas formas funcionan
false en un filtro se serializaba como cadena vacía Se serializa como false
features/portals no llegaban a Wasi Se separan solos y viajan en la query string
mediaStore() con ruta inválida mandaba un adjunto vacío Lanza RequestException antes de salir a la red
Sin timeouts: una petición colgada bloqueaba el request 30 s / 10 s por defecto, configurables
Sin reintentos setRetries()
Credenciales ausentes → error opaco de Wasi ConfigException con un mensaje claro
Sin tests Suite PHPUnit — composer test

Buenas prácticas

  • Guarda el id_property que devuelve propertyStore() junto a tu registro local. Sin él no puedes actualizar ni subir fotos.
  • Usa reference para escribir tu ID interno dentro de Wasi: reconciliación en ambos sentidos.
  • Busca antes de crear por tu reference: hace tus jobs idempotentes ante reintentos.
  • Cachea los catálogos (ciudades, tipos, monedas). Casi nunca cambian.
  • Usa 'short' => true en búsquedas de listado: la respuesta se reduce drásticamente.
  • Pagina con take = 100 (el máximo) para minimizar peticiones.
  • Sube las fotos en background. Son la parte lenta del flujo y hacen fallar peticiones HTTP síncronas.
  • Espacia las subidas (usleep) en lotes grandes.
  • No confíes en leer justo después de escribir: el cache de Wasi puede tardar minutos.
  • Castea los números: Wasi devuelve muchos IDs como cadenas ("id_property": "123456").

15. Troubleshooting

id_company or wasi_token is wrong.
  • Confirma que tu cuenta de Wasi está en plan Pro — la API no existe en planes inferiores.
  • Los espacios y saltos de línea al copiar del .env ya los recorta el SDK.
  • Regenera el token en Configuración → Ajustes generales → API de Wasi. Regenerarlo invalida el anterior: actualízalo en todos tus entornos.
  • Si olvidas una credencial, el SDK lanza ConfigException antes de salir a la red; si ves este mensaje de Wasi es que las dos llegaron, pero alguna es incorrecta.
`searchPropertys()` ignora mis filtros

En la v1 había que anidarlos bajo filters o se descartaban en silencio. En la v2 ambas formas funcionan, así que si sigues viendo el problema, actualiza:

composer update homlity/sdk-wasi-php8

Si ya estás en v2, revisa los nombres de los filtros contra §9: un filtro con un nombre que Wasi no conoce simplemente no filtra.

`getTotal()` devuelve `null`

El cuerpo no traía la clave total, lo que casi siempre indica error. Comprueba primero isOk() y lee getErrorMessage().

En la v1 este método devolvía false; ahora devuelve ?int, así que $res->getTotal() ?? 0 es la forma segura de usarlo en una suma o un printf.

La imagen se sube pero no aparece en el inmueble
  • Si el archivo no sirve, el SDK lanza RequestException con el motivo exacto. Si no lanza, el archivo llegó bien.
  • Revisa upload_max_filesize y post_max_size en tu php.ini.
  • Recuerda el cache de Wasi: puede tardar minutos en reflejarse en la web pública.
`Class "Curl\Curl" not found`

Falta la dependencia o el autoload. Ejecuta composer install y asegúrate de requerir vendor/autoload.php antes de instanciar nada del SDK.

Timeouts en producción

Ajusta ConfigSDK:

$config->setTimeout(60)->setConnectTimeout(5)->setRetries(2);

Para lotes grandes, mueve además las llamadas a una cola en lugar de ejecutarlas dentro de un request HTTP.

Los acentos y la ñ salen mal

Confirma que ext-mbstring está cargado y que tus textos llegan en UTF-8. Si no lo están, el SDK lanza RequestException mencionando UTF-8 en lugar de enviar un payload roto. Sanea con mb_convert_encoding($texto, 'UTF-8', 'UTF-8') si vienen de fuentes dudosas.

Los acentos y la ñ se envían sin escapar (JSON_UNESCAPED_UNICODE), tal cual los guarda Wasi.

16. Ejemplos ejecutables y tests

En examples/ encontrarás scripts listos para correr:

Script Qué hace
examples/bootstrap.php Configuración compartida y el helper wasiOk()
examples/list_users.php Lista los asesores y sus id_user
examples/search_properties.php Búsqueda con filtros y paginación completa
examples/create_property.php Crea un inmueble y muestra el id_property
examples/update_property.php Cambia precio y estado de publicación
examples/upload_images.php Sube todas las fotos de una carpeta
examples/catalogs.php Consulta países, ciudades y tipos de inmueble
examples/full_sync.php Flujo completo: crear → fotos → publicar → verificar
export WASI_ID_COMPANY=1234567890
export WASI_TOKEN=ABCD_EFGH_IJKL_MNOP

php examples/list_users.php
php examples/search_properties.php

Ejecutar los tests

La suite es de unidad: no toca la red, así que corre en cualquier sitio sin credenciales.

composer install
composer test

Cubre la lógica que rodea a la API: validación del status, normalización de filtros, construcción de la query string (booleanos y arrays), validación de archivos y de credenciales. Las respuestas se simulan con tests/Support/FakeCurl.php, un doble de \Curl\Curl con las mismas propiedades públicas.

17. Extender el SDK

Añadir un endpoint son tres pasos. Ejemplo: obtener un inmueble (v1/property/get/{id}).

1. La peticiónsrc/InfraStructure/Requets/PropertyGetRequest.php

<?php
namespace Homlity\WASI\SDK\InfraStructure\Requets;

use Curl\Curl;
use Homlity\WASI\SDK\InfraStructure\Responses\PropertyGetResponse;

class PropertyGetRequest extends RequestsFather
{
    private $idProp;

    public function setId(string $idProp)
    {
        $this->idProp = $idProp;
    }

    public function getEndpoint(): string
    {
        return "v1/property/get/" . $this->idProp;
    }

    public function run()
    {
        $curl = new Curl();
        $curl->get($this->buildUrl());

        return new PropertyGetResponse($curl);
    }
}

2. La respuestasrc/InfraStructure/Responses/PropertyGetResponse.php

<?php
namespace Homlity\WASI\SDK\InfraStructure\Responses;

class PropertyGetResponse extends ResponseFather
{
    public function getGalleries()
    {
        $body = $this->getBodyArray();

        return $body['galleries'] ?? [];
    }
}

3. El método del facade — en src/App/WasiSDKFacade.php

public function propertyGet(string $idProp)
{
    $request = new PropertyGetRequest();
    $request->setConfig($this->config);
    $request->setId($idProp);

    return $request->run();
}

newCurl() ya aplica los timeouts y reintentos de ConfigSDK, y buildUrl() acepta parámetros extra, así que tu petición nueva hereda ese comportamiento sin hacer nada.

Otras extensiones útiles

  • Logging: inyecta un Psr\Log\LoggerInterface en RequestsFather y loguea endpoint + getHttpStatusCode()nunca la URL completa.
  • Un endpoint que devuelva algo con forma propia: añade el helper a tu clase Response, como hacen getIdProperty() o getProperties().
  • Tests de tu extensión: hereda el patrón de tests/Unit/FakeCurl te permite probar la respuesta sin red.

18. Soporte y contribución

Recurso Enlace
🏠 Homlity https://homlity.com/
👩‍💻 Portal de desarrolladores https://homlity.com/desarrolladores/
📦 Packagist https://packagist.org/packages/homlity/sdk-wasi-php8
🐙 GitHub https://github.com/homlity/sdk-wasi-php8
📘 API oficial de Wasi https://api.wasi.co/docs/guide/
📗 Referencia de la API (este repo) docs/wasi-api-reference.md
🅻 Guía de Laravel (este repo) docs/laravel.md
🧾 Campos de inmueble (Wasi) https://api.wasi.co/docs/guide/fields/property.html

Contribuir

  1. Haz fork y crea una rama desde main.
  2. Mantén el estilo existente: una clase Request + una Response + un método de facade.
  3. No cambies namespaces públicos (incluido Requets) sin una versión mayor.
  4. Abre el PR describiendo el endpoint de Wasi que cubre y enlazando su documentación.

Otros SDK de Homlity

Paquete Para qué sirve
homlity/sdk-wasi-php8 CRM Wasi — este paquete
homlity/sdk-proppit Portal Proppit (LIFULL Connect) — publicación de anuncios en PHP/Laravel

Homlity

Hecho por Homlity · ¿Construyendo sobre nuestros SDK? Escríbenos desde el portal de desarrolladores.