Connect to Homito
On this page
Choose permissions and complete your first authenticated server read.
Connect a system or agent
Use Homito's company-scoped HTTPS API from your own server or agent. Once an administrator has supplied a key, ordinary integration work can run without the Homito portal. The API reference and OpenAPI document describe the same operations and schemas.
Company access
An administrator of an existing, verified Homito company creates a key under Settings → API & agents. This requires their Homito user session and settings.edit permission. The secret is shown once: put it in your server's secret manager. The key determines the company; passing a different company ID cannot extend its access. External self-service company registration is unavailable. Key management uses the Homito web-app origin and its user session, not this external API origin.
Use the configured API origin
Ask a company administrator for a key with read:properties. Set HOMITO_API_BASE_URL to the configured HTTPS API origin, without a path or query. Inject HOMITO_API_KEY from your server secret manager; never put it in browser code, a URL or an agent conversation.
These SDK-free server examples make the same first read. curl and JavaScript use a 10-second total deadline. Python uses a 10-second blocking network timeout, not a total-response deadline. JavaScript and Python reject responses over 2 MiB and print only count/hasMore; curl discards the body and prints HTTP status. Save the JavaScript example as first-read.mjs and run it with Node.js 20 or later.
curl
curl --proto '=https' --fail --silent --show-error --connect-timeout 5 --max-time 10 \
--output /dev/null --write-out 'HTTP %{http_code}\n' \
"$HOMITO_API_BASE_URL/api/v1/landlord/inventory/properties?limit=1" \
-H "Authorization: Bearer $HOMITO_API_KEY"JavaScript — server, Node.js 20 or later
const apiOrigin = process.env.HOMITO_API_BASE_URL;
const apiKey = process.env.HOMITO_API_KEY;
if (!apiOrigin || !apiKey) {
throw new Error("Configure HOMITO_API_BASE_URL and HOMITO_API_KEY on the server.");
}
let base;
try {
base = new URL(apiOrigin);
} catch {
throw new Error("Use the configured HTTPS API origin without paths, ports, query or credentials.");
}
if (base.protocol !== "https:" || base.username || base.password || base.port ||
!(apiOrigin === base.origin || apiOrigin === base.origin + "/")) {
throw new Error("Use the configured HTTPS API origin without paths, ports, query or credentials.");
}
const MAX_RESPONSE_BYTES = 2 * 1024 * 1024;
let response;
try {
response = await fetch(new URL("/api/v1/landlord/inventory/properties?limit=1", base.origin), {
headers: { Authorization: "Bearer " + apiKey },
signal: AbortSignal.timeout(10000),
redirect: "error",
});
} catch {
throw new Error("Homito request failed (network or timeout).");
}
if (!response.ok) {
throw new Error("Homito read failed (HTTP " + response.status + ").");
}
const declared = response.headers.get("content-length");
if (declared !== null && (!/^[0-9]+$/.test(declared) ||
!Number.isSafeInteger(Number(declared)) || Number(declared) > MAX_RESPONSE_BYTES)) {
throw new Error("Homito response exceeded the safe size limit.");
}
if (!response.body) throw new Error("Homito returned an empty response.");
const reader = response.body.getReader();
const chunks = [];
let byteLength = 0;
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
byteLength += value.byteLength;
if (byteLength > MAX_RESPONSE_BYTES) {
await reader.cancel();
throw new Error("Response too large.");
}
chunks.push(value);
}
} catch {
throw new Error("Homito response could not be read within the configured bounds.");
} finally {
reader.releaseLock();
}
const bytes = new Uint8Array(byteLength);
let offset = 0;
for (const chunk of chunks) {
bytes.set(chunk, offset);
offset += chunk.byteLength;
}
let data;
try {
data = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
} catch {
throw new Error("Homito returned invalid JSON.");
}
if (!data || !Array.isArray(data.properties) ||
!Object.prototype.hasOwnProperty.call(data, "nextCursor") ||
!(data.nextCursor === null || typeof data.nextCursor === "string")) {
throw new Error("Unexpected Homito response shape.");
}
console.log({ count: data.properties.length, hasMore: data.nextCursor !== null });Python — server, standard library
import json
import os
import urllib.error
import urllib.request
from urllib.parse import urlsplit
class NoRedirect(urllib.request.HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
api_origin = os.environ.get("HOMITO_API_BASE_URL")
api_key = os.environ.get("HOMITO_API_KEY")
if not api_origin or not api_key:
raise RuntimeError("Configure HOMITO_API_BASE_URL and HOMITO_API_KEY on the server.")
try:
base = urlsplit(api_origin)
hostname = base.hostname
port = base.port
except ValueError:
raise RuntimeError("Use the configured HTTPS API origin without paths, ports, query or credentials.") from None
if not hostname:
raise RuntimeError("Use the configured HTTPS API origin without paths, ports, query or credentials.")
request_origin = "https://" + ("[" + hostname + "]" if ":" in hostname else hostname)
if (base.scheme != "https" or base.username is not None or base.password is not None
or port is not None or any(character.isspace() for character in api_origin)
or chr(92) in api_origin or api_origin not in (request_origin, request_origin + "/")):
raise RuntimeError("Use the configured HTTPS API origin without paths, ports, query or credentials.")
MAX_RESPONSE_BYTES = 2 * 1024 * 1024
request = urllib.request.Request(
request_origin + "/api/v1/landlord/inventory/properties?limit=1",
headers={"Authorization": "Bearer " + api_key},
method="GET",
)
opener = urllib.request.build_opener(NoRedirect())
try:
with opener.open(request, timeout=10) as response:
if not 200 <= response.status < 300:
raise RuntimeError("Homito read failed (HTTP " + str(response.status) + ").")
declared = response.headers.get("Content-Length")
if declared is not None and (not declared.isdecimal() or int(declared) > MAX_RESPONSE_BYTES):
raise RuntimeError("Homito response exceeded the safe size limit.")
body = response.read(MAX_RESPONSE_BYTES + 1)
if len(body) > MAX_RESPONSE_BYTES:
raise RuntimeError("Homito response exceeded the safe size limit.")
data = json.loads(body)
except urllib.error.HTTPError as error:
raise RuntimeError("Homito read failed (HTTP " + str(error.code) + ").") from None
except (urllib.error.URLError, TimeoutError, OSError):
raise RuntimeError("Homito request failed (network or timeout).") from None
except ValueError:
raise RuntimeError("Homito returned invalid JSON.") from None
if (not isinstance(data, dict) or not isinstance(data.get("properties"), list)
or "nextCursor" not in data
or not (data["nextCursor"] is None or isinstance(data["nextCursor"], str))):
raise RuntimeError("Unexpected Homito response shape.")
print(json.dumps({"count": len(data["properties"]), "hasMore": data["nextCursor"] is not None}))Understand the first authenticated read
A successful empty result is {"properties":[],"nextCursor":null}: the connection worked and this company has no properties to return. It is not an authentication error. For a non-empty result, take a returned property id and read GET /api/v1/landlord/inventory/properties/{propertyId} or /{propertyId}/units. Inspect the operation response schemas for fields and nullable values.
REST operations and key permissions
| Task | Key permissions | Start here |
|---|---|---|
| Read properties and units | read:properties | GET /api/v1/landlord/inventory/properties |
Continue reads safely
Follow nextCursor until it is null, keeping the same list operation and filters. Use cursors only with the same company and list operation. Import previews, jobs and their history belong to the API key that created them, including access by job ID. A green public /health response alone does not verify your key or company access.
Applicant identity and integration keys
Keep customer keys on the server. A company key does not replace the applicant's identity, consent, submission key or upload key. Use the operation's published form and eID requirements before accepting an application.