getMe()['data']); * foreach ($api->listPropertiesAll(['status' => 'ACTIVE']) as $imovel) { echo $imovel['id'], "\n"; } * * A chave é SÓ do servidor. / The key is SERVER-SIDE only. */ declare(strict_types=1); namespace ImobyFlow; /** O erro da API (application/problem+json). Decida pelo `code`, nunca pelo texto. */ class ApiException extends \RuntimeException { public ?string $errorCode; public ?string $title; public ?string $type; public ?string $requestId; public ?string $detail; public array $invalidParams; public mixed $body; public function __construct(public int $status, mixed $body, ?string $requestId = null) { $p = is_array($body) ? $body : []; $this->errorCode = $p['code'] ?? null; $this->title = $p['title'] ?? null; $this->type = $p['type'] ?? null; $this->requestId = $p['requestId'] ?? $requestId; $this->detail = $p['detail'] ?? null; $this->invalidParams = $p['invalidParams'] ?? []; $this->body = $body; parent::__construct(trim($status . ' ' . ($this->errorCode ?? '') . ': ' . ($this->title ?? '')), $status); } } class Client { public const API_VERSION = '1.0.0'; public const DEFAULT_BASE_URL = 'https://api.imobyflow.com.br/v1'; private const RETRY_STATUS = [429, 502, 503, 504]; private const OPS = [ 'getMe' => [ 'method' => 'GET', 'path' => '/me', 'query' => [], 'dryRun' => false, 'idempotency' => false, ], 'getPropertyTypes' => [ 'method' => 'GET', 'path' => '/reference/property-types', 'query' => [], 'dryRun' => false, 'idempotency' => false, ], 'getPurposes' => [ 'method' => 'GET', 'path' => '/reference/purposes', 'query' => [], 'dryRun' => false, 'idempotency' => false, ], 'getPropertyStatuses' => [ 'method' => 'GET', 'path' => '/reference/property-statuses', 'query' => [], 'dryRun' => false, 'idempotency' => false, ], 'getLeadStatuses' => [ 'method' => 'GET', 'path' => '/reference/lead-statuses', 'query' => [], 'dryRun' => false, 'idempotency' => false, ], 'getPartnershipStatuses' => [ 'method' => 'GET', 'path' => '/reference/partnership-statuses', 'query' => [], 'dryRun' => false, 'idempotency' => false, ], 'getConstructionStages' => [ 'method' => 'GET', 'path' => '/reference/construction-stages', 'query' => [], 'dryRun' => false, 'idempotency' => false, ], 'getScopes' => [ 'method' => 'GET', 'path' => '/reference/scopes', 'query' => [], 'dryRun' => false, 'idempotency' => false, ], 'getEventTypes' => [ 'method' => 'GET', 'path' => '/reference/event-types', 'query' => [], 'dryRun' => false, 'idempotency' => false, ], 'listCities' => [ 'method' => 'GET', 'path' => '/geo/cities', 'query' => ['uf', 'q', 'limit', 'cursor'], 'dryRun' => false, 'idempotency' => false, ], 'listNeighborhoods' => [ 'method' => 'GET', 'path' => '/geo/cities/{cityId}/neighborhoods', 'query' => ['limit', 'cursor'], 'dryRun' => false, 'idempotency' => false, ], 'listProperties' => [ 'method' => 'GET', 'path' => '/properties', 'query' => ['limit', 'cursor', 'status', 'purpose', 'type', 'citySlug', 'updatedSince', 'radar'], 'dryRun' => false, 'idempotency' => false, ], 'getPropertyByRef' => [ 'method' => 'GET', 'path' => '/properties/by-ref/{ref}', 'query' => [], 'dryRun' => false, 'idempotency' => false, ], 'getProperty' => [ 'method' => 'GET', 'path' => '/properties/{propertyId}', 'query' => [], 'dryRun' => false, 'idempotency' => false, ], 'listPropertyMatches' => [ 'method' => 'GET', 'path' => '/properties/{propertyId}/matches', 'query' => [], 'dryRun' => false, 'idempotency' => false, ], 'listLeads' => [ 'method' => 'GET', 'path' => '/leads', 'query' => ['limit', 'cursor', 'state', 'status', 'ownerAccountId', 'updatedSince', 'radar'], 'dryRun' => false, 'idempotency' => false, ], 'getLead' => [ 'method' => 'GET', 'path' => '/leads/{leadId}', 'query' => [], 'dryRun' => false, 'idempotency' => false, ], 'getLeadByRef' => [ 'method' => 'GET', 'path' => '/leads/by-ref/{ref}', 'query' => [], 'dryRun' => false, 'idempotency' => false, ], 'listLeadMatches' => [ 'method' => 'GET', 'path' => '/leads/{leadId}/matches', 'query' => [], 'dryRun' => false, 'idempotency' => false, ], 'listMatches' => [ 'method' => 'GET', 'path' => '/matches', 'query' => ['limit', 'cursor', 'status', 'scope', 'ownerAccountId'], 'dryRun' => false, 'idempotency' => false, ], 'listPartnerships' => [ 'method' => 'GET', 'path' => '/partnerships', 'query' => ['status', 'updatedSince'], 'dryRun' => false, 'idempotency' => false, ], 'getPartnership' => [ 'method' => 'GET', 'path' => '/partnerships/{partnershipId}', 'query' => [], 'dryRun' => false, 'idempotency' => false, ], 'listTeam' => [ 'method' => 'GET', 'path' => '/team', 'query' => [], 'dryRun' => false, 'idempotency' => false, ], 'getFeed' => [ 'method' => 'GET', 'path' => '/feed', 'query' => [], 'dryRun' => false, 'idempotency' => false, ], 'syncFeed' => [ 'method' => 'POST', 'path' => '/feed/sync', 'query' => [], 'dryRun' => true, 'idempotency' => true, ], 'listLaunches' => [ 'method' => 'GET', 'path' => '/launches', 'query' => ['limit', 'cursor', 'citySlug', 'neighborhood', 'type', 'stage', 'priceMin', 'priceMax', 'q'], 'dryRun' => false, 'idempotency' => false, ], 'putPropertyByRef' => [ 'method' => 'PUT', 'path' => '/properties/by-ref/{ref}', 'query' => [], 'dryRun' => true, 'idempotency' => true, ], 'createProperty' => [ 'method' => 'POST', 'path' => '/properties', 'query' => [], 'dryRun' => true, 'idempotency' => true, ], 'patchProperty' => [ 'method' => 'PATCH', 'path' => '/properties/{propertyId}', 'query' => [], 'dryRun' => true, 'idempotency' => true, ], 'setPropertyStatus' => [ 'method' => 'POST', 'path' => '/properties/{propertyId}/status', 'query' => [], 'dryRun' => true, 'idempotency' => true, ], 'confirmProperties' => [ 'method' => 'POST', 'path' => '/properties/confirm', 'query' => [], 'dryRun' => true, 'idempotency' => true, ], 'putLeadByRef' => [ 'method' => 'PUT', 'path' => '/leads/by-ref/{ref}', 'query' => [], 'dryRun' => true, 'idempotency' => true, ], 'createLead' => [ 'method' => 'POST', 'path' => '/leads', 'query' => [], 'dryRun' => true, 'idempotency' => true, ], 'patchLead' => [ 'method' => 'PATCH', 'path' => '/leads/{leadId}', 'query' => [], 'dryRun' => true, 'idempotency' => true, ], 'setLeadState' => [ 'method' => 'POST', 'path' => '/leads/{leadId}/state', 'query' => [], 'dryRun' => true, 'idempotency' => true, ], 'addLeadInteraction' => [ 'method' => 'POST', 'path' => '/leads/{leadId}/interactions', 'query' => [], 'dryRun' => true, 'idempotency' => true, ], 'deleteProperty' => [ 'method' => 'DELETE', 'path' => '/properties/{propertyId}', 'query' => [], 'dryRun' => true, 'idempotency' => true, ], 'deleteLead' => [ 'method' => 'DELETE', 'path' => '/leads/{leadId}', 'query' => [], 'dryRun' => true, 'idempotency' => true, ], 'dismissMatch' => [ 'method' => 'POST', 'path' => '/matches/{matchId}/dismiss', 'query' => [], 'dryRun' => true, 'idempotency' => true, ], 'restoreMatch' => [ 'method' => 'POST', 'path' => '/matches/{matchId}/restore', 'query' => [], 'dryRun' => true, 'idempotency' => true, ], 'setPartnershipStatus' => [ 'method' => 'POST', 'path' => '/partnerships/{partnershipId}/status', 'query' => [], 'dryRun' => true, 'idempotency' => true, ], 'listCaptacaoOffers' => [ 'method' => 'GET', 'path' => '/captacao/offers', 'query' => [], 'dryRun' => false, 'idempotency' => false, ], 'getCaptacaoOffer' => [ 'method' => 'GET', 'path' => '/captacao/offers/{offerId}', 'query' => [], 'dryRun' => false, 'idempotency' => false, ], 'listCaptacaoOwners' => [ 'method' => 'GET', 'path' => '/captacao/owners', 'query' => ['tab', 'cursor'], 'dryRun' => false, 'idempotency' => false, ], 'acceptCaptacaoOffer' => [ 'method' => 'POST', 'path' => '/captacao/offers/{offerId}/accept', 'query' => [], 'dryRun' => true, 'idempotency' => true, ], 'listEvents' => [ 'method' => 'GET', 'path' => '/events', 'query' => ['limit', 'cursor', 'since', 'types'], 'dryRun' => false, 'idempotency' => false, ], 'listWebhooks' => [ 'method' => 'GET', 'path' => '/webhooks', 'query' => [], 'dryRun' => false, 'idempotency' => false, ], 'createWebhook' => [ 'method' => 'POST', 'path' => '/webhooks', 'query' => [], 'dryRun' => false, 'idempotency' => false, ], 'updateWebhook' => [ 'method' => 'PATCH', 'path' => '/webhooks/{webhookId}', 'query' => [], 'dryRun' => false, 'idempotency' => true, ], 'deleteWebhook' => [ 'method' => 'DELETE', 'path' => '/webhooks/{webhookId}', 'query' => [], 'dryRun' => false, 'idempotency' => true, ], 'rotateWebhookSecret' => [ 'method' => 'POST', 'path' => '/webhooks/{webhookId}/rotate-secret', 'query' => [], 'dryRun' => false, 'idempotency' => false, ], 'testWebhook' => [ 'method' => 'POST', 'path' => '/webhooks/{webhookId}/test', 'query' => [], 'dryRun' => false, 'idempotency' => true, ], 'listWebhookDeliveries' => [ 'method' => 'GET', 'path' => '/webhooks/{webhookId}/deliveries', 'query' => ['limit', 'cursor'], 'dryRun' => false, 'idempotency' => false, ], 'retryWebhookDelivery' => [ 'method' => 'POST', 'path' => '/webhooks/{webhookId}/deliveries/{deliveryId}/retry', 'query' => [], 'dryRun' => false, 'idempotency' => true, ], 'createPropertyBatch' => [ 'method' => 'POST', 'path' => '/properties/batch', 'query' => [], 'dryRun' => true, 'idempotency' => true, ], 'getPropertyBatch' => [ 'method' => 'GET', 'path' => '/properties/batch/{batchId}', 'query' => [], 'dryRun' => false, 'idempotency' => false, ], 'createLeadBatch' => [ 'method' => 'POST', 'path' => '/leads/batch', 'query' => [], 'dryRun' => true, 'idempotency' => true, ], 'getLeadBatch' => [ 'method' => 'GET', 'path' => '/leads/batch/{batchId}', 'query' => [], 'dryRun' => false, 'idempotency' => false, ], ]; private string $key; public string $baseUrl; public function __construct( string $apiKey, ?string $baseUrl = null, public string $language = 'pt-BR', public int $timeout = 30, public int $maxRetries = 2, public float $retryBaseDelay = 0.5, ) { $chave = trim(preg_replace('/^bearer\s+/i', '', trim($apiKey))); if ($chave === '' || preg_match('/\s/', $chave)) { throw new \InvalidArgumentException('ImobyFlow: chave da API ausente ou inválida.'); } $this->key = $chave; $this->baseUrl = rtrim($baseUrl ?? self::DEFAULT_BASE_URL, '/'); } private function call(array $op, array $path, array $params, ?array $body, bool $dryRun, ?string $idempotencyKey): mixed { $desconhecidos = array_diff(array_keys($params), $op['query']); if ($desconhecidos) { throw new \InvalidArgumentException('ImobyFlow: filtro desconhecido nesta rota: ' . implode(', ', $desconhecidos)); } $caminho = $op['path']; foreach ($path as $nome => $valor) { if ($valor === '') { throw new \InvalidArgumentException("ImobyFlow: $nome é obrigatório."); } $caminho = str_replace('{' . $nome . '}', rawurlencode($valor), $caminho); } $qs = []; foreach ($params as $k => $v) { if ($v !== null && $v !== '') { $qs[$k] = is_bool($v) ? ($v ? 'true' : 'false') : (string) $v; } } if ($op['dryRun'] && $dryRun) { $qs['dryRun'] = 'true'; } $url = $this->baseUrl . $caminho . ($qs ? '?' . http_build_query($qs, '', '&', PHP_QUERY_RFC3986) : ''); $headers = [ 'Authorization: Bearer ' . $this->key, 'Accept: application/json', 'Accept-Language: ' . $this->language, 'User-Agent: imobyflow-sdk-php/' . self::API_VERSION, ]; // A MESMA chave em todas as tentativas: a re-tentativa devolve a resposta da primeira. $chave = $op['idempotency'] ? ($idempotencyKey ?? bin2hex(random_bytes(16))) : null; if ($chave !== null) { $headers[] = 'Idempotency-Key: ' . $chave; } $dados = null; if ($body !== null) { $dados = json_encode($body, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); $headers[] = 'Content-Type: application/json'; } $podeRepetir = $op['method'] === 'GET' || $chave !== null; for ($tentativa = 0; ; $tentativa++) { $respHeaders = []; $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_CUSTOMREQUEST => $op['method'], CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => $headers, CURLOPT_TIMEOUT => $this->timeout, CURLOPT_HEADERFUNCTION => function ($c, string $linha) use (&$respHeaders): int { $partes = explode(':', $linha, 2); if (count($partes) === 2) { $respHeaders[strtolower(trim($partes[0]))] = trim($partes[1]); } return strlen($linha); }, ]); if ($dados !== null) { curl_setopt($ch, CURLOPT_POSTFIELDS, $dados); } $texto = curl_exec($ch); $status = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); $falhaRede = $texto === false; $erroRede = curl_error($ch); unset($ch); // desde o PHP 8 o handle é um objeto; curl_close() é obsoleto no 8.5 if ($falhaRede) { if ($podeRepetir && $tentativa < $this->maxRetries) { usleep((int) ($this->retryBaseDelay * (2 ** $tentativa) * 1e6)); continue; } throw new \RuntimeException('ImobyFlow: falha de rede — ' . $erroRede); } $corpo = $texto === '' ? null : json_decode($texto, true); if ($status >= 200 && $status < 300) { return $corpo; } if ($podeRepetir && in_array($status, self::RETRY_STATUS, true) && $tentativa < $this->maxRetries) { $depois = $respHeaders['retry-after'] ?? ''; usleep((int) ((ctype_digit($depois) ? (float) $depois : $this->retryBaseDelay * (2 ** $tentativa)) * 1e6)); continue; } throw new ApiException($status, $corpo ?? $texto, $respHeaders['x-request-id'] ?? null); } } /** Quem sou eu · Who am I * GET /v1/me */ public function getMe(): mixed { return $this->call(self::OPS['getMe'], [], [], null, false, null); } /** Tipos de imóvel · Property types * GET /v1/reference/property-types */ public function getPropertyTypes(): mixed { return $this->call(self::OPS['getPropertyTypes'], [], [], null, false, null); } /** Finalidades · Purposes * GET /v1/reference/purposes */ public function getPurposes(): mixed { return $this->call(self::OPS['getPurposes'], [], [], null, false, null); } /** Status do imóvel · Property statuses * GET /v1/reference/property-statuses */ public function getPropertyStatuses(): mixed { return $this->call(self::OPS['getPropertyStatuses'], [], [], null, false, null); } /** Etapas do cliente · Client stages * GET /v1/reference/lead-statuses */ public function getLeadStatuses(): mixed { return $this->call(self::OPS['getLeadStatuses'], [], [], null, false, null); } /** Etapas da parceria · Partnership stages * GET /v1/reference/partnership-statuses */ public function getPartnershipStatuses(): mixed { return $this->call(self::OPS['getPartnershipStatuses'], [], [], null, false, null); } /** Estágios da obra · Construction stages * GET /v1/reference/construction-stages */ public function getConstructionStages(): mixed { return $this->call(self::OPS['getConstructionStages'], [], [], null, false, null); } /** Permissões · Permissions * GET /v1/reference/scopes */ public function getScopes(): mixed { return $this->call(self::OPS['getScopes'], [], [], null, false, null); } /** Tipos de evento · Event types * GET /v1/reference/event-types */ public function getEventTypes(): mixed { return $this->call(self::OPS['getEventTypes'], [], [], null, false, null); } /** Cidades · Cities * GET /v1/geo/cities */ public function listCities(array $params = []): mixed { return $this->call(self::OPS['listCities'], [], $params, null, false, null); } /** Todas as páginas de listCities, item a item · every page, item by item. */ public function listCitiesAll(array $params = []): \Generator { unset($params['cursor']); $vistos = []; while (true) { $pagina = $this->listCities($params); foreach (($pagina['data'] ?? []) as $item) { yield $item; } $cursor = $pagina['nextCursor'] ?? null; if (!$cursor || isset($vistos[$cursor])) { return; // cursor repetido: para em vez de rodar para sempre } $vistos[$cursor] = true; $params['cursor'] = $cursor; } } /** Bairros de uma cidade · A city's neighborhoods * GET /v1/geo/cities/{cityId}/neighborhoods */ public function listNeighborhoods(string $cityId, array $params = []): mixed { return $this->call(self::OPS['listNeighborhoods'], ['cityId' => $cityId], $params, null, false, null); } /** Todas as páginas de listNeighborhoods, item a item · every page, item by item. */ public function listNeighborhoodsAll(string $cityId, array $params = []): \Generator { unset($params['cursor']); $vistos = []; while (true) { $pagina = $this->listNeighborhoods($cityId, $params); foreach (($pagina['data'] ?? []) as $item) { yield $item; } $cursor = $pagina['nextCursor'] ?? null; if (!$cursor || isset($vistos[$cursor])) { return; // cursor repetido: para em vez de rodar para sempre } $vistos[$cursor] = true; $params['cursor'] = $cursor; } } /** Listar a carteira · List the portfolio * GET /v1/properties — permissão properties:read */ public function listProperties(array $params = []): mixed { return $this->call(self::OPS['listProperties'], [], $params, null, false, null); } /** Todas as páginas de listProperties, item a item · every page, item by item. */ public function listPropertiesAll(array $params = []): \Generator { unset($params['cursor']); $vistos = []; while (true) { $pagina = $this->listProperties($params); foreach (($pagina['data'] ?? []) as $item) { yield $item; } $cursor = $pagina['nextCursor'] ?? null; if (!$cursor || isset($vistos[$cursor])) { return; // cursor repetido: para em vez de rodar para sempre } $vistos[$cursor] = true; $params['cursor'] = $cursor; } } /** Um imóvel pelo seu código · One property by your code * GET /v1/properties/by-ref/{ref} — permissão properties:read */ public function getPropertyByRef(string $ref): mixed { return $this->call(self::OPS['getPropertyByRef'], ['ref' => $ref], [], null, false, null); } /** Um imóvel · One property * GET /v1/properties/{propertyId} — permissão properties:read */ public function getProperty(string $propertyId): mixed { return $this->call(self::OPS['getProperty'], ['propertyId' => $propertyId], [], null, false, null); } /** Oportunidades de um imóvel · A property's opportunities * GET /v1/properties/{propertyId}/matches — permissão radar:read */ public function listPropertyMatches(string $propertyId): mixed { return $this->call(self::OPS['listPropertyMatches'], ['propertyId' => $propertyId], [], null, false, null); } /** Listar clientes · List clients * GET /v1/leads — permissão leads:read */ public function listLeads(array $params = []): mixed { return $this->call(self::OPS['listLeads'], [], $params, null, false, null); } /** Todas as páginas de listLeads, item a item · every page, item by item. */ public function listLeadsAll(array $params = []): \Generator { unset($params['cursor']); $vistos = []; while (true) { $pagina = $this->listLeads($params); foreach (($pagina['data'] ?? []) as $item) { yield $item; } $cursor = $pagina['nextCursor'] ?? null; if (!$cursor || isset($vistos[$cursor])) { return; // cursor repetido: para em vez de rodar para sempre } $vistos[$cursor] = true; $params['cursor'] = $cursor; } } /** Um cliente · One client * GET /v1/leads/{leadId} — permissão leads:read */ public function getLead(string $leadId): mixed { return $this->call(self::OPS['getLead'], ['leadId' => $leadId], [], null, false, null); } /** Um cliente pelo seu código · One client by your code * GET /v1/leads/by-ref/{ref} — permissão leads:read */ public function getLeadByRef(string $ref): mixed { return $this->call(self::OPS['getLeadByRef'], ['ref' => $ref], [], null, false, null); } /** Oportunidades de um cliente · A client's opportunities * GET /v1/leads/{leadId}/matches — permissão radar:read */ public function listLeadMatches(string $leadId): mixed { return $this->call(self::OPS['listLeadMatches'], ['leadId' => $leadId], [], null, false, null); } /** Oportunidades da conta · The account's opportunities * GET /v1/matches — permissão radar:read */ public function listMatches(array $params = []): mixed { return $this->call(self::OPS['listMatches'], [], $params, null, false, null); } /** Todas as páginas de listMatches, item a item · every page, item by item. */ public function listMatchesAll(array $params = []): \Generator { unset($params['cursor']); $vistos = []; while (true) { $pagina = $this->listMatches($params); foreach (($pagina['data'] ?? []) as $item) { yield $item; } $cursor = $pagina['nextCursor'] ?? null; if (!$cursor || isset($vistos[$cursor])) { return; // cursor repetido: para em vez de rodar para sempre } $vistos[$cursor] = true; $params['cursor'] = $cursor; } } /** Parcerias da equipe · The team's partnerships * GET /v1/partnerships — permissão partnerships:read */ public function listPartnerships(array $params = []): mixed { return $this->call(self::OPS['listPartnerships'], [], $params, null, false, null); } /** Uma parceria · One partnership * GET /v1/partnerships/{partnershipId} — permissão partnerships:read */ public function getPartnership(string $partnershipId): mixed { return $this->call(self::OPS['getPartnership'], ['partnershipId' => $partnershipId], [], null, false, null); } /** A equipe · The team * GET /v1/team — permissão team:read */ public function listTeam(): mixed { return $this->call(self::OPS['listTeam'], [], [], null, false, null); } /** Estado da sincronização do feed · Feed sync status * GET /v1/feed — permissão properties:read */ public function getFeed(): mixed { return $this->call(self::OPS['getFeed'], [], [], null, false, null); } /** Sincronizar o feed agora · Sync the feed now * POST /v1/feed/sync — permissão properties:write */ public function syncFeed(bool $dryRun = false, ?string $idempotencyKey = null): mixed { return $this->call(self::OPS['syncFeed'], [], [], null, $dryRun, $idempotencyKey); } /** Lançamentos da cidade · The city's new developments * GET /v1/launches — permissão launches:read */ public function listLaunches(array $params = []): mixed { return $this->call(self::OPS['listLaunches'], [], $params, null, false, null); } /** Todas as páginas de listLaunches, item a item · every page, item by item. */ public function listLaunchesAll(array $params = []): \Generator { unset($params['cursor']); $vistos = []; while (true) { $pagina = $this->listLaunches($params); foreach (($pagina['data'] ?? []) as $item) { yield $item; } $cursor = $pagina['nextCursor'] ?? null; if (!$cursor || isset($vistos[$cursor])) { return; // cursor repetido: para em vez de rodar para sempre } $vistos[$cursor] = true; $params['cursor'] = $cursor; } } /** Criar ou atualizar pelo seu código · Create or update by your code * PUT /v1/properties/by-ref/{ref} — permissão properties:write */ public function putPropertyByRef(string $ref, array $body, bool $dryRun = false, ?string $idempotencyKey = null): mixed { return $this->call(self::OPS['putPropertyByRef'], ['ref' => $ref], [], $body, $dryRun, $idempotencyKey); } /** Cadastrar um imóvel · Create a property * POST /v1/properties — permissão properties:write */ public function createProperty(array $body, bool $dryRun = false, ?string $idempotencyKey = null): mixed { return $this->call(self::OPS['createProperty'], [], [], $body, $dryRun, $idempotencyKey); } /** Alterar parte de um imóvel · Partially update a property * PATCH /v1/properties/{propertyId} — permissão properties:write */ public function patchProperty(string $propertyId, array $body, bool $dryRun = false, ?string $idempotencyKey = null): mixed { return $this->call(self::OPS['patchProperty'], ['propertyId' => $propertyId], [], $body, $dryRun, $idempotencyKey); } /** Mudar o status · Change the status * POST /v1/properties/{propertyId}/status — permissão properties:write */ public function setPropertyStatus(string $propertyId, array $body, bool $dryRun = false, ?string $idempotencyKey = null): mixed { return $this->call(self::OPS['setPropertyStatus'], ['propertyId' => $propertyId], [], $body, $dryRun, $idempotencyKey); } /** Confirmar disponibilidade em lote · Confirm availability in bulk * POST /v1/properties/confirm — permissão properties:write */ public function confirmProperties(array $body, bool $dryRun = false, ?string $idempotencyKey = null): mixed { return $this->call(self::OPS['confirmProperties'], [], [], $body, $dryRun, $idempotencyKey); } /** Criar, atualizar ou vincular pelo seu código · Create, update or link by your code * PUT /v1/leads/by-ref/{ref} — permissão leads:write */ public function putLeadByRef(string $ref, array $body, bool $dryRun = false, ?string $idempotencyKey = null): mixed { return $this->call(self::OPS['putLeadByRef'], ['ref' => $ref], [], $body, $dryRun, $idempotencyKey); } /** Cadastrar um cliente · Create a client * POST /v1/leads — permissão leads:write */ public function createLead(array $body, bool $dryRun = false, ?string $idempotencyKey = null): mixed { return $this->call(self::OPS['createLead'], [], [], $body, $dryRun, $idempotencyKey); } /** Alterar parte de um cliente · Partially update a client * PATCH /v1/leads/{leadId} — permissão leads:write */ public function patchLead(string $leadId, array $body, bool $dryRun = false, ?string $idempotencyKey = null): mixed { return $this->call(self::OPS['patchLead'], ['leadId' => $leadId], [], $body, $dryRun, $idempotencyKey); } /** Arquivar ou reativar · Archive or reactivate * POST /v1/leads/{leadId}/state — permissão leads:write */ public function setLeadState(string $leadId, array $body, bool $dryRun = false, ?string $idempotencyKey = null): mixed { return $this->call(self::OPS['setLeadState'], ['leadId' => $leadId], [], $body, $dryRun, $idempotencyKey); } /** Registrar um atendimento · Log an interaction * POST /v1/leads/{leadId}/interactions — permissão leads:write */ public function addLeadInteraction(string $leadId, array $body, bool $dryRun = false, ?string $idempotencyKey = null): mixed { return $this->call(self::OPS['addLeadInteraction'], ['leadId' => $leadId], [], $body, $dryRun, $idempotencyKey); } /** Mandar para a lixeira · Move to the trash * DELETE /v1/properties/{propertyId} — permissão properties:write */ public function deleteProperty(string $propertyId, bool $dryRun = false, ?string $idempotencyKey = null): mixed { return $this->call(self::OPS['deleteProperty'], ['propertyId' => $propertyId], [], null, $dryRun, $idempotencyKey); } /** Mandar para a lixeira · Move to the trash * DELETE /v1/leads/{leadId} — permissão leads:write */ public function deleteLead(string $leadId, bool $dryRun = false, ?string $idempotencyKey = null): mixed { return $this->call(self::OPS['deleteLead'], ['leadId' => $leadId], [], null, $dryRun, $idempotencyKey); } /** Descartar oportunidade · Dismiss an opportunity * POST /v1/matches/{matchId}/dismiss — permissão radar:write */ public function dismissMatch(string $matchId, bool $dryRun = false, ?string $idempotencyKey = null): mixed { return $this->call(self::OPS['dismissMatch'], ['matchId' => $matchId], [], null, $dryRun, $idempotencyKey); } /** Restaurar oportunidade · Restore an opportunity * POST /v1/matches/{matchId}/restore — permissão radar:write */ public function restoreMatch(string $matchId, bool $dryRun = false, ?string $idempotencyKey = null): mixed { return $this->call(self::OPS['restoreMatch'], ['matchId' => $matchId], [], null, $dryRun, $idempotencyKey); } /** Mover a etapa da parceria · Move the partnership stage * POST /v1/partnerships/{partnershipId}/status — permissão partnerships:write */ public function setPartnershipStatus(string $partnershipId, array $body, bool $dryRun = false, ?string $idempotencyKey = null): mixed { return $this->call(self::OPS['setPartnershipStatus'], ['partnershipId' => $partnershipId], [], $body, $dryRun, $idempotencyKey); } /** Proprietários na vez da conta · Owners offered to the account * GET /v1/captacao/offers — permissão captacao:read */ public function listCaptacaoOffers(): mixed { return $this->call(self::OPS['listCaptacaoOffers'], [], [], null, false, null); } /** Um proprietário · One owner * GET /v1/captacao/offers/{offerId} — permissão captacao:read */ public function getCaptacaoOffer(string $offerId): mixed { return $this->call(self::OPS['getCaptacaoOffer'], ['offerId' => $offerId], [], null, false, null); } /** Proprietários aceitos · Accepted owners * GET /v1/captacao/owners — permissão captacao:read */ public function listCaptacaoOwners(array $params = []): mixed { return $this->call(self::OPS['listCaptacaoOwners'], [], $params, null, false, null); } /** Todas as páginas de listCaptacaoOwners, item a item · every page, item by item. */ public function listCaptacaoOwnersAll(array $params = []): \Generator { unset($params['cursor']); $vistos = []; while (true) { $pagina = $this->listCaptacaoOwners($params); foreach (($pagina['data'] ?? []) as $item) { yield $item; } $cursor = $pagina['nextCursor'] ?? null; if (!$cursor || isset($vistos[$cursor])) { return; // cursor repetido: para em vez de rodar para sempre } $vistos[$cursor] = true; $params['cursor'] = $cursor; } } /** Aceitar um proprietário · Accept an owner * POST /v1/captacao/offers/{offerId}/accept — permissão captacao:accept */ public function acceptCaptacaoOffer(string $offerId, bool $dryRun = false, ?string $idempotencyKey = null): mixed { return $this->call(self::OPS['acceptCaptacaoOffer'], ['offerId' => $offerId], [], null, $dryRun, $idempotencyKey); } /** O que mudou · What changed * GET /v1/events — permissão events:read */ public function listEvents(array $params = []): mixed { return $this->call(self::OPS['listEvents'], [], $params, null, false, null); } /** Todas as páginas de listEvents, item a item · every page, item by item. */ public function listEventsAll(array $params = []): \Generator { unset($params['cursor']); $vistos = []; while (true) { $pagina = $this->listEvents($params); foreach (($pagina['data'] ?? []) as $item) { yield $item; } $cursor = $pagina['nextCursor'] ?? null; if (!$cursor || isset($vistos[$cursor])) { return; // cursor repetido: para em vez de rodar para sempre } $vistos[$cursor] = true; $params['cursor'] = $cursor; } } /** Endereços cadastrados · Registered endpoints * GET /v1/webhooks — permissão webhooks:manage */ public function listWebhooks(): mixed { return $this->call(self::OPS['listWebhooks'], [], [], null, false, null); } /** Cadastrar um endereço · Register an endpoint * POST /v1/webhooks — permissão webhooks:manage */ public function createWebhook(array $body): mixed { return $this->call(self::OPS['createWebhook'], [], [], $body, false, null); } /** Alterar um endereço · Update an endpoint * PATCH /v1/webhooks/{webhookId} — permissão webhooks:manage */ public function updateWebhook(string $webhookId, array $body, ?string $idempotencyKey = null): mixed { return $this->call(self::OPS['updateWebhook'], ['webhookId' => $webhookId], [], $body, false, $idempotencyKey); } /** Apagar um endereço · Delete an endpoint * DELETE /v1/webhooks/{webhookId} — permissão webhooks:manage */ public function deleteWebhook(string $webhookId, ?string $idempotencyKey = null): mixed { return $this->call(self::OPS['deleteWebhook'], ['webhookId' => $webhookId], [], null, false, $idempotencyKey); } /** Trocar o segredo · Rotate the secret * POST /v1/webhooks/{webhookId}/rotate-secret — permissão webhooks:manage */ public function rotateWebhookSecret(string $webhookId): mixed { return $this->call(self::OPS['rotateWebhookSecret'], ['webhookId' => $webhookId], [], null, false, null); } /** Mandar um evento de teste · Send a test event * POST /v1/webhooks/{webhookId}/test — permissão webhooks:manage */ public function testWebhook(string $webhookId, ?string $idempotencyKey = null): mixed { return $this->call(self::OPS['testWebhook'], ['webhookId' => $webhookId], [], null, false, $idempotencyKey); } /** Entregas de um endereço · An endpoint's deliveries * GET /v1/webhooks/{webhookId}/deliveries — permissão webhooks:manage */ public function listWebhookDeliveries(string $webhookId, array $params = []): mixed { return $this->call(self::OPS['listWebhookDeliveries'], ['webhookId' => $webhookId], $params, null, false, null); } /** Todas as páginas de listWebhookDeliveries, item a item · every page, item by item. */ public function listWebhookDeliveriesAll(string $webhookId, array $params = []): \Generator { unset($params['cursor']); $vistos = []; while (true) { $pagina = $this->listWebhookDeliveries($webhookId, $params); foreach (($pagina['data'] ?? []) as $item) { yield $item; } $cursor = $pagina['nextCursor'] ?? null; if (!$cursor || isset($vistos[$cursor])) { return; // cursor repetido: para em vez de rodar para sempre } $vistos[$cursor] = true; $params['cursor'] = $cursor; } } /** Reenviar uma entrega · Redeliver * POST /v1/webhooks/{webhookId}/deliveries/{deliveryId}/retry — permissão webhooks:manage */ public function retryWebhookDelivery(string $webhookId, string $deliveryId, ?string $idempotencyKey = null): mixed { return $this->call(self::OPS['retryWebhookDelivery'], ['webhookId' => $webhookId, 'deliveryId' => $deliveryId], [], null, false, $idempotencyKey); } /** Lote de imóveis · Batch of properties * POST /v1/properties/batch — permissão properties:write */ public function createPropertyBatch(array $body, bool $dryRun = false, ?string $idempotencyKey = null): mixed { return $this->call(self::OPS['createPropertyBatch'], [], [], $body, $dryRun, $idempotencyKey); } /** Acompanhar o lote · Track the batch * GET /v1/properties/batch/{batchId} — permissão properties:read */ public function getPropertyBatch(string $batchId): mixed { return $this->call(self::OPS['getPropertyBatch'], ['batchId' => $batchId], [], null, false, null); } /** Lote de clientes · Batch of clients * POST /v1/leads/batch — permissão leads:write */ public function createLeadBatch(array $body, bool $dryRun = false, ?string $idempotencyKey = null): mixed { return $this->call(self::OPS['createLeadBatch'], [], [], $body, $dryRun, $idempotencyKey); } /** Acompanhar o lote · Track the batch * GET /v1/leads/batch/{batchId} — permissão leads:read */ public function getLeadBatch(string $batchId): mixed { return $this->call(self::OPS['getLeadBatch'], ['batchId' => $batchId], [], null, false, null); } /** * Confere a assinatura de um webhook (Standard Webhooks). $rawBody: o corpo cru * (file_get_contents('php://input')), antes do json_decode. */ public static function verifyWebhook(string $secret, string $msgId, string $timestamp, string $signatureHeader, string $rawBody): bool { if (!ctype_digit($timestamp) || abs(time() - (int) $timestamp) > 5 * 60) { return false; } $key = base64_decode(substr($secret, strlen('whsec_'))); $expected = base64_encode(hash_hmac('sha256', $msgId . '.' . $timestamp . '.' . $rawBody, $key, true)); foreach (explode(' ', $signatureHeader) as $part) { [$version, $sig] = array_pad(explode(',', $part, 2), 2, ''); if ($version === 'v1' && hash_equals($expected, $sig)) { return true; } } return false; } }