homlity/sdk-ciencuadras

SDK PHP para integracion con la API de Ciencuadras (Integradores)

Maintainers

Package info

github.com/homlity/sdk-ciencuadras

pkg:composer/homlity/sdk-ciencuadras

Transparency log

Statistics

Installs: 1

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

V0.0.1 2026-08-24 13:45 UTC

This package is auto-updated.

Last update: 2026-08-24 13:46:17 UTC


README

Homlity

Homlity SDK · Ciencuadras

SDK oficial en PHP para integrar tu sistema inmobiliario con el API de Integradores de Ciencuadras.
Parte del ecosistema abierto de Homlity.

Packagist PHP ^8.1 MIT GitHub

homlity.com · Portal de desarrolladores · GitHub @homlity

Tabla de contenido

¿Qué es Homlity?

Homlity es un ecosistema tecnológico para inmobiliarias: conecta, automatiza y optimiza la operación integrando CRMs, ERPs, portales inmobiliarios y herramientas especializadas en un solo lugar.

Para los desarrolladores, Homlity publica un conjunto de SDKs open source en PHP (distribuidos por Composer/Packagist) que encapsulan la comunicación con cada portal o CRM del sector. Así no tienes que reimplementar autenticación, validaciones, reintentos y webhooks para cada proveedor.

Portal de desarrolladores: https://homlity.com/desarrolladores/

¿Para qué sirve este SDK?

homlity/sdk-ciencuadras es el cliente PHP del API de Integradores de Ciencuadras, el portal inmobiliario colombiano. Sirve para sincronizar el inventario de inmuebles de tu sistema con Ciencuadras de forma masiva y automática.

Casos de uso típicos:

Necesidad Qué hace el SDK
Publicar el inventario de una inmobiliaria en Ciencuadras listings()->create() acepta uno o varios inmuebles en un solo request
Mantener precios, fotos y descripciones sincronizados listings()->update() con envío por lotes
Retirar o reactivar publicaciones listings()->updateStatus() con ACTIVE / DELETED
Saber si Ciencuadras aceptó cada inmueble tasks()->waitUntilSettled() o webhooks
Reaccionar en tiempo real a cambios de estado WebhookNotification con validación de HUB.ID / VERIFY-TOKEN
No enviar payloads inválidos (y gastar cupo) Validación local contra el snapshot OpenAPI antes del request
Consultar catálogos (ciudades, barrios, características) locations(), categories(), clients()

Lo que aporta sobre un curl directo:

  • Cliente HTTP con cURL y manejo homogéneo de errores (ApiException, TransportException).
  • Módulos por recurso: listings, clients, categories, locations, tasks, webhooks.
  • Validación previa de campos requeridos (y opcionalmente tipos/enums) contra el OpenAPI oficial.
  • Catálogo de esquemas en runtime para inspeccionar parámetros sin abrir el Swagger.
  • Enums tipados (TaskStatus, ListingStatus) y snapshots (TaskSnapshot, ListingSnapshot).
  • Polling de tareas con waitUntilSettled().
  • Utilidades de webhooks, incluyendo suscripción idempotente (subscribeTargetIfChanged).
  • Doble header de autenticación (apikey + X-API-KEY) porque el OpenAPI de Ciencuadras usa ambos nombres en distintas secciones.

Requisitos

Requisito Versión
PHP ^8.1
Extensión curl requerida
Extensión json requerida
Composer 2.x

No requiere framework: funciona en PHP puro, Laravel, Symfony, WordPress o cualquier proyecto con autoload PSR-4.

Instalación

composer require homlity/sdk-ciencuadras

Nota sobre versiones. Mientras el paquete no tenga un tag estable publicado, instálalo apuntando a la rama principal:

composer require homlity/sdk-ciencuadras:dev-main

Si tu composer.json tiene "minimum-stability": "stable", agrega también:

{
  "minimum-stability": "dev",
  "prefer-stable": true
}

Instalación desde el repositorio (fork o desarrollo local):

{
  "repositories": [
    { "type": "vcs", "url": "https://github.com/homlity/sdk-ciencuadras" }
  ],
  "require": {
    "homlity/sdk-ciencuadras": "dev-main"
  }
}

El namespace del paquete es Ciencuadras\Sdk\ (autoload PSR-4 sobre src/).

Configuración

Credenciales

Solicita a Ciencuadras, como parte del proceso de verificación de integrador:

Dato Para qué sirve Dónde se usa
API KEY Autenticación de todas las llamadas Config::__construct(apiKey: ...)
client_id (uuid) Inmobiliaria/cliente al que pertenecen los inmuebles Campo obligatorio de cada inmueble
client_agent (int) Sucursal/agente Campo opcional del inmueble
Cookie de cliente Header obligatorio del listado GET /listing listings()->list(clientCookie: ...)
integrator_id (uuid) Identificador para suscribir el webhook webhooks()->subscribeTarget()
HUB.ID y VERIFY-TOKEN Validar que un callback vino de Ciencuadras WebhookNotification::assertAuthorized()

Nunca hardcodees el API key: usa variables de entorno o el gestor de secretos de tu plataforma.

Ambientes disponibles

use Ciencuadras\Sdk\Config;

Config::BASE_URL_PRODUCTION;        // https://kong.ciencuadras.com.co/management/api/1.0
Config::BASE_URL_QA;                // https://kong-qa.ciencuadras.com.co/management/api/1.0
Config::BASE_URL_MOCK;              // SwaggerHub mock server (útil para pruebas sin credenciales)
Config::BASE_URL_PORTAL_SANDBOX;    // https://dev-ws-api.ciencuadras.com
Config::BASE_URL_PORTAL_PRODUCTION; // https://ws-api.ciencuadras.com

Opción A — configuración explícita

use Ciencuadras\Sdk\Config;
use Ciencuadras\Sdk\CiencuadrasClient;

$config = new Config(
    apiKey: getenv('CIENCUADRAS_API_KEY'),
    baseUrl: Config::BASE_URL_PRODUCTION,
    timeoutSeconds: 30,
);

$sdk = new CiencuadrasClient($config);

Config valida en el constructor: lanza InvalidArgumentException si el API key está vacío o si el timeout no es positivo.

Opción B — configuración por entorno Homlity

Pensada para instalaciones donde el ambiente y las rutas se controlan por configuración (por ejemplo el plugin de Homlity para WordPress).

$config = Config::fromHomlityEnv(
    apiKey: $_ENV['CIENCUADRAS_API_KEY'],
    env: $_ENV,
    timeoutSeconds: 30,
);

Lee ciencuadras_environment y resuelve base URL + rutas automáticamente:

Variable Descripción Default
ciencuadras_environment sandbox, qa, development, dev → sandbox. Cualquier otro valor → producción production
CIENCUADRAS_SANDBOX_BASE_URL Base URL del ambiente sandbox https://dev-ws-api.ciencuadras.com
CIENCUADRAS_PRODUCTION_BASE_URL Base URL del ambiente productivo https://ws-api.ciencuadras.com
CIENCUADRAS_{ENV}_LOGIN_ENDPOINT Ruta de login /login
CIENCUADRAS_{ENV}_CREATE_ENDPOINT Ruta de creación /api/create
CIENCUADRAS_{ENV}_UPDATE_ENDPOINT Ruta de actualización /api/update
CIENCUADRAS_{ENV}_CONSULT_STATUS_ENDPOINT Consulta de estado de tarea /api/consult-status
CIENCUADRAS_{ENV}_CONSULT_PROPERTY_ENDPOINT Detalle de inmueble /api/consult-property
CIENCUADRAS_{ENV}_CONSULT_ALL_PROPERTIES_ENDPOINT Listado de inmuebles /api/consult-all-properties
CIENCUADRAS_{ENV}_PRICE_CAPS_ENDPOINT Topes de precio (opcional)
CIENCUADRAS_{ENV}_AREA_CAPS_ENDPOINT Topes de área (opcional)
CIENCUADRAS_{ENV}_ZONE_CAPS_ENDPOINT Topes por zona (opcional)

Donde {ENV} es SANDBOX o PRODUCTION. Las rutas se normalizan: se les antepone / si no lo traen y se aceptan URLs absolutas (https://...) para apuntar a otro host.

Cuando hay endpoints configurados, el SDK ajusta el verbo HTTP: update() y updateStatus() usan PATCH contra el API estándar (/listing) y POST contra las rutas tipo portal (/api/update).

$sdk->config()->endpoint('create');   // "/api/create"
$sdk->config()->endpoints();          // array completo

Primeros pasos en 5 minutos

<?php

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

use Ciencuadras\Sdk\Config;
use Ciencuadras\Sdk\CiencuadrasClient;
use Ciencuadras\Sdk\Exception\ApiException;

$sdk = new CiencuadrasClient(new Config(getenv('CIENCUADRAS_API_KEY')));

try {
    // 1. ¿Con qué clientes (inmobiliarias) puedo trabajar y cuánto cupo tienen?
    $clients = $sdk->clients()->all();
    print_r($clients);

    // 2. ¿Qué agente/sucursal uso?
    $clientId = $clients[0]['id'];
    $agentId  = $sdk->clients()->resolveSingleAgentId($clientId);

    // 3. ¿Cuál es el id del barrio donde está el inmueble?
    $locations = $sdk->locations()->search('Chapinero');
    print_r($locations);
} catch (ApiException $e) {
    echo $e->statusCode() . '' . ($e->firstErrorMessage() ?? $e->getMessage());
}

Recursos disponibles

$sdk->listings();   // Inmuebles: crear, actualizar, estado, consultar, validar
$sdk->clients();    // Clientes (inmobiliarias), cupos y agentes
$sdk->categories(); // Características/amenidades disponibles
$sdk->locations();  // Búsqueda de ubicaciones (país, ciudad, barrio, zona…)
$sdk->tasks();      // Estado de las tareas asincrónicas
$sdk->webhooks();   // Suscripción y publicación de eventos

$sdk->config();         // Config activa
$sdk->schemaCatalog();  // Acceso al OpenAPI embebido

Todos los módulos se instancian de forma perezosa y se reutilizan en la misma instancia del cliente.

Publicar inmuebles

POST /listing. Acepta un inmueble o una lista de inmuebles (envío por lotes).

Campos obligatorios

description, external_code, client_id, offer, property_type, price, address, locations, area, listing_contact.

Ejemplo completo

$listing = [
    // --- Identificación ---
    'external_code'  => 'INT-1001',                                 // tu código interno
    'client_id'      => 'df03d199-be5c-4c5c-98f6-849361cb7fae',     // inmobiliaria
    'client_agent'   => 42,                                         // sucursal (opcional)

    // --- Oferta ---
    'offer'          => 'sell',      // sell | rent | lease
    'property_type'  => 'house',     // ver tabla de tipos
    'description'    => 'Casa amplia y bien ubicada, con patio y garaje doble.',
    'price'          => 450000000,
    'negotiable'     => true,
    'administration' => ['is_included' => false, 'price' => 350000],

    // --- Características ---
    'area'           => 120,   // área construida (m²)
    'living_area'    => 98,    // área privada (m²)
    'condition'      => 2,     // 2 = Excelente
    'stratum'        => 4,     // estrato
    'age'            => 3,     // 3 = de 9 a 15 años
    'rooms'          => 3,
    'baths'          => 2,
    'garages'        => 2,
    'floor'          => 1,
    'categories'     => [100, 104, 117],   // ids de GET /category

    // --- Ubicación ---
    'address'   => ['address' => 'Calle 12 # 34-56'],
    'locations' => [
        'location_point'   => ['latitude' => 4.729795079, 'longitude' => -74.044724493],
        'location_main_id' => '1895e0a3-60b8-4a9d-858d-f2c7297b48b2',  // id de barrio
        'view_map'         => 2,  // 0 = punto exacto, 1 = oculto, 2 = solo zona
    ],
    'postal_code' => '110221',

    // --- Contacto ---
    'listing_contact' => [
        'emails' => [
            ['email' => 'ventas@midominio.com', 'is_main' => true, 'sort_order' => 0],
        ],
        'phones' => [
            [
                'phone'              => '+573001112233',
                'is_whatsapp_number' => true,
                'is_click_to_call'   => true,
                'sort_order'         => 0,
            ],
        ],
    ],

    // --- Multimedia (máximo 30 fotos) ---
    'photos' => [
        ['sort_order' => 1, 'is_main' => true,  'image' => 'https://cdn.midominio.com/1.jpg'],
        ['sort_order' => 2, 'is_main' => false, 'image' => 'https://cdn.midominio.com/2.jpg'],
    ],
    'video' => 'https://www.youtube.com/watch?v=xxxxxxx',
];

$response = $sdk->listings()->create($listing);
// => ['task' => ['id' => '...', 'status' => 'READY', ...]]

Envío por lotes

$response = $sdk->listings()->create([$listingA, $listingB, $listingC]);

El SDK detecta automáticamente si le pasas un objeto o una lista (normalizeBatchPayload), así que ambas formas son válidas.

La respuesta no confirma que los inmuebles quedaron publicados: devuelve un task_id. Ver Tareas asincrónicas.

Tipos de inmueble (property_type)

Valor Español Valor Español
apartment Apartamento building Edificio
house Casa cabin Cabaña
studio Apartaestudio country-house Casa campestre
house-lot Casa lote lot Lote
room Habitación farm Finca
office Oficina commercial Local
consulting-room Consultorio warehouse Bodega
parking Parqueadero

Tipo de oferta (offer)

Valor Significado
sell Venta
rent Arriendo
lease Alquiler vacacional

Los enums numéricos completos (condition, stratum, age, capacity, parking_size…) están en docs/listing-parameters.md.

Actualizar inmuebles

PATCH /listing. Mismos campos que la creación más listing_id (el uuid que asignó Ciencuadras).

$sdk->listings()->update([
    'listing_id'      => '07bcf513-d39a-42ff-8370-f42d39cd9494',
    'external_code'   => 'INT-1001',
    'client_id'       => 'df03d199-be5c-4c5c-98f6-849361cb7fae',
    'offer'           => 'sell',
    'property_type'   => 'house',
    'description'     => 'Casa remodelada con cocina nueva.',
    'price'           => 439000000,   // bajó de precio
    'area'            => 120,
    'address'         => ['address' => 'Calle 12 # 34-56'],
    'locations'       => [
        'location_point' => ['latitude' => 4.729795079, 'longitude' => -74.044724493],
    ],
    'listing_contact' => $contacto,
]);

Actualizar o eliminar fotos

Envía el id de la imagen para reemplazarla; si omites el id, se crea una nueva. Para eliminar una imagen usa status => 3.

'photos' => [
    ['id' => 'bd08d894-a117-46f2-a7a5-c6c02e5764b3', 'image' => 'https://cdn.midominio.com/nueva.jpg', 'sort_order' => 1, 'is_main' => true],
    ['id' => '4e3a...', 'status' => 3],   // eliminar
],

Activar o eliminar inmuebles

PATCH /listing/status. Requiere listing_id, client_id y status.

use Ciencuadras\Sdk\Data\ListingStatus;

$sdk->listings()->updateStatus([
    'listing_id' => '07bcf513-d39a-42ff-8370-f42d39cd9494',
    'client_id'  => 'df03d199-be5c-4c5c-98f6-849361cb7fae',
    'status'     => 'DELETED',   // 'ACTIVE' | 'DELETED'
]);

Cuidado con dos vocabularios distintos. El endpoint de cambio de estado recibe los strings ACTIVE / DELETED, pero las respuestas de consulta devuelven un status entero. El enum ListingStatus mapea esos enteros de solo lectura:

Código Caso Significado
0 INCOMPLETE Publicado pero aún incompleto
1 DISABLED Desactivado desde la OV
2 NO_QUOTA Sin cupo disponible
4 ACTIVE Publicado y visible
5 EXPIRED Producto de cuota expirado
7 DELETED Eliminado
9 SYSTEM_ERROR Error interno de publicación
10 PUBLISHING En proceso de publicación
11 REJECTED Rechazado por moderación

Helpers: isActive(), isDeleted(), isPublished(), isPending(), isTerminalFailure().

Consultar inmuebles

Listado

$page = $sdk->listings()->list(
    clientCookie: getenv('CIENCUADRAS_CLIENT_COOKIE'),   // header Cookie obligatorio
    query: [
        'page'      => 1,
        'page_size' => 20,
        'ordering'  => '-created',   // created, -created, updated, -updated, status, -status, …
        'search'    => 'chapinero',
    ],
);
// => ['count' => 120, 'next' => ..., 'previous' => ..., 'results' => [...]]

Detalle tipado

$snap = $sdk->listings()->getSnapshot('07bcf513-d39a-42ff-8370-f42d39cd9494');

$snap->id();            // uuid
$snap->externalCode();  // tu código interno
$snap->status();        // ListingStatus|null
$snap->statusCode();    // int|null
$snap->isActive();      // bool
$snap->isPending();     // bool
$snap->raw();           // array crudo de la API

listings()->get() devuelve el array crudo si prefieres trabajar sin el snapshot.

Buscar por tu propio código

Muy útil para reconciliar tu inventario con el de Ciencuadras sin guardar el listing_id:

$snap = $sdk->listings()->findByExternalCode(
    clientCookie: $cookie,
    externalCode: 'INT-1001',
);

if ($snap === null) {
    // no existe todavía → crearlo
} else {
    // ya existe → actualizarlo con $snap->id()
}

Solo busca dentro de la primera página de resultados. Para colecciones grandes pasa ['page' => N] en $extraQuery.

Tareas asincrónicas

Publicar, actualizar o cambiar el estado de un inmueble no es inmediato: Ciencuadras encola el trabajo y devuelve un task_id. El ciclo de vida es:

READY → RUNNING → COMPLETED | FORWARDED | ERROR
Estado Significado
READY Tarea creada, aún no iniciada
RUNNING En ejecución
COMPLETED Terminó sin errores
FORWARDED Recibida pero no procesada: ya llegó un request idéntico el mismo día (duplicado). Se considera éxito
ERROR Error al crear o ejecutar la tarea

Polling automático

$response = $sdk->listings()->create($listing);
$taskId   = $response['task']['id'];

$task = $sdk->tasks()->waitUntilSettled($taskId, [
    'maxAttempts'     => 20,     // default 30
    'intervalSeconds' => 3,      // default 3
    'throwOnTimeout'  => true,   // default true
]);

if ($task->isSuccessful()) {
    foreach ($task->contentItems() as $item) {
        // $item['listing_id'], $item['fr_property_id'], $item['external_code'],
        // $item['status'], $item['multimedia']
    }
} elseif ($task->isFailed()) {
    // revisar $task->raw()['messages']
}

waitUntilSettled() tolera errores transitorios: si una consulta falla, reintenta hasta agotar maxAttempts. Con throwOnTimeout => false devuelve el último snapshot en lugar de lanzar RuntimeException.

Consulta puntual

$task = $sdk->tasks()->getSnapshot($taskId);

$task->id();
$task->status();        // TaskStatus enum
$task->isPending();     // READY | RUNNING
$task->isSettled();     // COMPLETED | FORWARDED | ERROR
$task->isSuccessful();  // COMPLETED | FORWARDED
$task->isFailed();      // ERROR
$task->contentItems();  // resultado por inmueble
$task->raw();           // objeto task completo

En producción, prefiere webhooks sobre polling: consume menos cuota y no te expone al throttling (429).

Webhooks

Ciencuadras notifica el resultado de cada tarea a la URL que registres.

1. Suscribir tu endpoint

$sdk->webhooks()->subscribeTarget(
    integratorId: '696d939e-4cc3-43ac-a312-6bf2e7f15868',
    targetUrl: 'https://midominio.com/webhooks/ciencuadras',
);

2. Suscripción idempotente

La API no expone un GET para consultar la suscripción activa, así que el SDK implementa desired state: guarda tú la última URL suscrita y pásala en cada arranque.

$result = $sdk->webhooks()->subscribeTargetIfChanged(
    integratorId: $integratorId,
    targetUrl: 'https://midominio.com/webhooks/ciencuadras',
    knownUrl: Cache::get('ciencuadras_webhook_url'),
);

if ($result->subscribed) {
    Cache::put('ciencuadras_webhook_url', $result->url);
}

$result->subscribed es false cuando no hubo llamada porque la URL no cambió.

3. Recibir y validar el callback

use Ciencuadras\Sdk\Webhook\WebhookNotification;
use Ciencuadras\Sdk\Exception\WebhookException;

try {
    $notification = WebhookNotification::fromGlobals();

    // Comparación en tiempo constante (hash_equals) de HUB.ID y VERIFY-TOKEN
    $notification->assertAuthorized(
        expectedHubId: getenv('CIENCUADRAS_WEBHOOK_HUB_ID'),
        expectedVerifyToken: getenv('CIENCUADRAS_WEBHOOK_VERIFY_TOKEN'),
    );

    match ($notification->event()) {
        'LISTING_CORE'   => procesarPublicacion($notification->listingResults()),
        'LISTING_STATUS' => procesarCambioEstado($notification->listingStatusUpdates()),
        default          => null,
    };

    http_response_code(200);
} catch (WebhookException $e) {
    http_response_code(401);
}

listingResults() normaliza cada inmueble del callback y le adjunta el error correspondiente tomado de task.messages.listings:

[
    'listing_id'        => '07bcf513-...',
    'external_code'     => 'INT-1001',
    'fr_property_id'    => 10002073,
    'processing_status' => 'COMPLETED',   // ERROR | COMPLETED | FORWARDED
    'multimedia'        => [...],
    'error'             => null,
]

Guarda fr_property_id: es el código que se muestra al usuario final en el portal.

En frameworks donde ya tienes el request parseado, usa fromRequest():

$notification = WebhookNotification::fromRequest(
    rawBody: $request->getContent(),
    headers: $request->headers->all(),
);

Detalle completo en docs/webhooks.md.

Catálogos: clientes, agentes, ubicaciones y categorías

// Clientes (inmobiliarias) con su cupo de publicación
$sdk->clients()->all();
// [['id' => uuid, 'name' => ..., 'initial_quota' => ..., 'remained_quota' => ..., 'used_quota' => ..., 'percentage_used_quota' => ...]]

$sdk->clients()->get($clientId);

// Agentes/sucursales del cliente
$sdk->clients()->agents($clientId);            // [['id' => 42, 'email' => ...]]
$sdk->clients()->resolveSingleAgentId($clientId); // int; lanza RuntimeException si hay 0 o >1

// Ubicaciones: país, departamento, ciudad, barrio, zona, localidad, comuna
$sdk->locations()->search('Chapinero');
// [['id' => uuid, 'name' => ..., 'location_type' => 'NEIGHBOURHOOD', 'country' => ..., 'state' => ..., 'city' => ...]]

// Características/amenidades para el campo `categories`
$sdk->categories()->list();
$sdk->categories()->list(['search' => 'piscina']);

El id que devuelve locations()->search() para un barrio es el que va en locations.location_main_id del inmueble.

Validación de payloads

El SDK valida antes de gastar un request. Por defecto verifica los campos requeridos de primer nivel; el modo estricto añade tipos, enums y requeridos anidados.

use Ciencuadras\Sdk\Schema\SchemaCatalog;
use Ciencuadras\Sdk\Schema\ListingPayloadValidator;

$validator = new ListingPayloadValidator(new SchemaCatalog());

try {
    $validator->validateCreatePayload([$listing], strict: true);
} catch (InvalidArgumentException $e) {
    // "Field `offer` at index 0 has invalid value \"venta\". Allowed: \"sell\", \"rent\", \"lease\"."
    // "Missing required nested field `locations.location_point` at index 0."
}

Inspeccionar el OpenAPI en runtime

Útil para construir formularios dinámicos o para depurar sin abrir el Swagger:

$catalog = $sdk->schemaCatalog();

$catalog->title();                       // "Ciencuadras API: Integration Partners"
$catalog->version();                     // "1.0.0"
$catalog->operations();                  // [['method' => 'POST', 'path' => '/listing', 'operationId' => ...], ...]
$catalog->listingCreateRequiredFields(); // ['description', 'external_code', 'client_id', ...]
$catalog->listingUpdateRequiredFields();
$catalog->listingStatusRequiredFields();
$catalog->listingCreateItemSchema();     // schema completo con properties y enums
$catalog->schema('ListingPOST');

Puedes apuntar a otro snapshot OpenAPI: new SchemaCatalog('/ruta/a/mi-openapi.json').

Manejo de errores

RuntimeException
├── ApiException        → la API respondió con un status fuera de 2xx
├── TransportException  → falló la capa de red (timeout, DNS, TLS)
└── WebhookException    → cuerpo o headers de webhook inválidos

InvalidArgumentException → payload inválido detectado localmente (antes del request)
use Ciencuadras\Sdk\Exception\ApiException;
use Ciencuadras\Sdk\Exception\TransportException;

try {
    $sdk->listings()->create($listing);
} catch (InvalidArgumentException $e) {
    // Payload mal construido: NO se envió nada. Corrige y reintenta.
} catch (ApiException $e) {
    $e->statusCode();         // 401, 404, 429, 500, 502…
    $e->firstErrorMessage();  // busca message / detail / errors[0]
    $e->defaultCode();        // defaultCode / code / error_code
    $e->trackingId();         // x-tracking-id / x-request-id / x-correlation-id
    $e->json();               // cuerpo decodificado
    $e->response();           // ApiResponse completo
} catch (TransportException $e) {
    // Red caída o timeout → reintenta con backoff
}

Códigos de error de Ciencuadras

Status Cuerpo típico Qué hacer
400 {"message": "Object not found"} Revisa ids y estructura del payload
401 {"message": "Invalid authentication credentials"} API key inválida, vencida o del ambiente equivocado
404 {"message": "Integrator id not found"} integrator_id / listing_id inexistente
429 {"detail": "Request was throttled. Expected available in 59 seconds."} Throttling: espera y reintenta con backoff. Reduce el polling y agrupa en lotes
500 {"error": {"tracking_id": "...", "message": "Internal Error"}} Reporta el tracking_id a soporte de Ciencuadras
502 {"message": "An invalid response was received from the upstream server"} Transitorio: reintenta

Siempre registra trackingId() en tus logs: es lo primero que pide soporte.

Errores por inmueble

Un 200 en la creación no significa éxito. Los errores por inmueble llegan en la tarea (task.messages.listings[] y task.messages.images[]) o en el webhook:

$task = $sdk->tasks()->waitUntilSettled($taskId);

foreach ($task->raw()['messages']['listings'] ?? [] as $problema) {
    // $problema['listing_id'], $problema['external_code'],
    // $problema['error']['message'], $problema['error']['tracking_id'],
    // $problema['error']['field']['description'] / ['status_code'] / ['default_code']
}

foreach ($task->raw()['messages']['images'] ?? [] as $problema) {
    // Ej. "Invalid url" con default_code "invalid_value"
}

Uso en Laravel y WordPress

Laravel — service provider

config/services.php:

'ciencuadras' => [
    'api_key'   => env('CIENCUADRAS_API_KEY'),
    'base_url'  => env('CIENCUADRAS_BASE_URL', \Ciencuadras\Sdk\Config::BASE_URL_PRODUCTION),
    'client_id' => env('CIENCUADRAS_CLIENT_ID'),
    'cookie'    => env('CIENCUADRAS_CLIENT_COOKIE'),
],

app/Providers/AppServiceProvider.php:

use Ciencuadras\Sdk\Config;
use Ciencuadras\Sdk\CiencuadrasClient;

public function register(): void
{
    $this->app->singleton(CiencuadrasClient::class, fn () => new CiencuadrasClient(
        new Config(
            apiKey: config('services.ciencuadras.api_key'),
            baseUrl: config('services.ciencuadras.base_url'),
            timeoutSeconds: 30,
        )
    ));
}

Ruta del webhook (recuerda excluirla de la verificación CSRF):

Route::post('/webhooks/ciencuadras', function (Request $request) {
    $notification = WebhookNotification::fromRequest($request->getContent(), $request->headers->all());
    $notification->assertAuthorized(config('services.ciencuadras.hub_id'), config('services.ciencuadras.verify_token'));

    ProcesarWebhookCiencuadras::dispatch($notification->listingResults());

    return response()->json(['ok' => true]);
})->withoutMiddleware(VerifyCsrfToken::class);

Publica desde una cola para no bloquear el request del usuario:

class PublicarInmuebleJob implements ShouldQueue
{
    public function handle(CiencuadrasClient $sdk): void
    {
        $response = $sdk->listings()->create($this->payload);
        $this->inmueble->update(['ciencuadras_task_id' => $response['task']['id']]);
    }
}

WordPress

add_action('init', function () {
    $sdk = new CiencuadrasClient(new Config(get_option('homlity_ciencuadras_api_key')));
    // ...
});

// Endpoint del webhook con la REST API de WordPress
add_action('rest_api_init', function () {
    register_rest_route('homlity/v1', '/ciencuadras', [
        'methods'             => 'POST',
        'permission_callback' => '__return_true',
        'callback'            => function (WP_REST_Request $request) {
            $notification = WebhookNotification::fromRequest(
                $request->get_body(),
                $request->get_headers_flat() ?? []
            );
            $notification->assertAuthorized(HUB_ID, VERIFY_TOKEN);
            return ['ok' => true];
        },
    ]);
});

El plugin Homlity Real Estate expone hooks y filtros para enganchar este SDK sin modificar el core.

Testing y HTTP client propio

CiencuadrasClient acepta cualquier implementación de HttpClientInterface, así que puedes sustituir cURL por Guzzle, Symfony HttpClient o un doble de prueba.

use Ciencuadras\Sdk\Http\ApiResponse;
use Ciencuadras\Sdk\Http\HttpClientInterface;

final class FakeHttpClient implements HttpClientInterface
{
    public array $requests = [];

    public function __construct(private ApiResponse $response) {}

    public function request(string $method, string $path, array $options = []): ApiResponse
    {
        $this->requests[] = compact('method', 'path', 'options');
        return $this->response;
    }
}

$http = new FakeHttpClient(new ApiResponse(200, [], json_encode([
    'task' => ['id' => 'task-1', 'status' => 'COMPLETED'],
])));

$sdk = new CiencuadrasClient(new Config('fake-key'), $http);
$sdk->listings()->create($listing);

// Aserciones sobre $http->requests

Para waitUntilSettled() puedes inyectar el sleepFn y evitar esperas reales en tests:

$sdk->tasks()->waitUntilSettled('task-1', [
    'intervalSeconds' => 0,
    'sleepFn'         => static fn (int $s) => null,
]);

Suite del proyecto:

composer install
composer test          # o ./vendor/bin/phpunit

Documentación extendida

Documento Contenido
docs/getting-started.md Guía paso a paso de la primera integración
docs/api-reference.md Endpoints, métodos del SDK, headers y errores
docs/listing-parameters.md Todos los campos del inmueble con sus enums
docs/tasks.md Tareas asincrónicas y estrategias de polling
docs/webhooks.md Suscripción, validación y procesamiento de callbacks
docs/errors.md Catálogo de errores y solución de problemas
resources/openapi/ciencuadras-integradores-1.0.0.json Snapshot OpenAPI que usa el SDK
examples/ Scripts ejecutables listos para copiar
CONTRIBUTING.md Cómo aportar al SDK

Otros SDKs de Homlity

Todos se instalan con composer require homlity/<paquete>:

Paquete Integración
homlity/sdk-ciencuadras Portal Ciencuadras
homlity/sdk-fincaraiz Portal Finca Raíz
homlity/sdk-metrocuadrado Portal Metrocuadrado
homlity/sdk-proppit Proppit
homlity/sdk-mobilia Mobilia
homlity/sdk-domus Domus
homlity/sdk-wasi-php8 CRM Wasi
homlity/softinm-sdk Softinm
homlity/sdk-smartinmobiliario Smart Inmobiliario
homlity/sdk-chat-homlity Chat y chatbots de WhatsApp con IA

Catálogo completo y guías: https://homlity.com/desarrolladores/

Contribuir

El desarrollo se centraliza en https://github.com/homlity. Guía completa en CONTRIBUTING.md.

  1. Abre un issue describiendo el bug o la propuesta.
  2. Haz fork y crea una rama descriptiva (fix/listing-status-enum).
  3. Agrega pruebas: composer test debe pasar en verde.
  4. Envía el pull request explicando el cambio y su impacto.

Reporta vulnerabilidades de seguridad de forma privada a través de https://homlity.com/, no en un issue público.

Licencia

MIT. Ver LICENSE.

Homlity
Hecho para el ecosistema inmobiliario · homlity.com · Desarrolladores