# Connect to Homito

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](https://h2nf8ee4ch.execute-api.eu-north-1.amazonaws.com/docs) and [OpenAPI document](https://h2nf8ee4ch.execute-api.eu-north-1.amazonaws.com/openapi.json) 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](https://h2nf8ee4ch.execute-api.eu-north-1.amazonaws.com), 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

```bash
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

```javascript
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

```python
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.
