API guide
The SDK is generated against the canonical HETIBANK OpenAPI 3.1 contract.
Contract
spec/hetibank.openapi.yaml— the canonical spec, copied unchanged from the application
(lib/api-spec/openapi.yaml: 301 operations, 18 tags).
spec/public-openapi.json— the buyer-facing projection served by the API.spec/fixtures/— contract-valid example payloads used by SDK tests.spec/sdk-surface.md— the curated operations the SDK exposes, with auth, errors and pagination.
The spec is the source of truth. If a type in this repo disagrees with the spec, the spec wins and we fix the SDK.
Authentication
| Caller | Credential | Notes |
|---|---|---|
| Signed-in buyer (browser) | buyer session bearer token + X-Hetibank-Key browser key | the API never creates accounts; the buyer signs in on the HETIBANK funnel |
| Server integration | secret API key | never ship a secret key in a browser bundle, widget or plugin |
| Test | hbk_test_* key | returns sandbox homes only |
import { createHetibankClient } from "@hetibank/client";
const client = createHetibankClient({
baseUrl: "https://www.hetibank.com/api",
getToken: () => session.bearerToken, // refreshed by your auth layer
getApiKey: () => process.env.HETIBANK_BROWSER_KEY, // public browser key, not a secret
fetch: globalThis.fetch, // optional: inject a proxy/recording transport
});
The client takes a transport, not a global fetch monkey-patch. This is how a server-side proxy, an Electron shell or a test recorder plugs in without the packages knowing.
Errors
One envelope everywhere:
{ "code": "rate_limited", "message": "Too many requests", "request_id": "req_01J..." }
The client throws HetibankApiError with status, code, requestId, retryAfterSeconds and retryable. It retries 429, 502, 503 and 504 with backoff honoring Retry-After, and never retries a 4xx that is not 429. UI error states display the request_id so support can correlate.
Figures and evidence
Every money/percent/count value in a buyer-UI response is a Figure:
{
"value": 2940,
"unit": "usd_per_month",
"certainty": "estimated",
"source": "hetibank_model",
"asOf": "2026-11-01",
"note": "Includes estimated taxes and insurance"
}
certainty:known | estimated | unverified | missingmissingalways hasvalue: null; render it as “Unavailable”sourcefollows the loan evidence ladder (`mls_model < title_record < statement_supported <
servicer_confirmed`)
- computed values should also carry a
definitionIdas the API adds it; show it in the evidence chip
The SDK never recomputes these. If you need a payment, ask the API for the component.
Buyer-UI envelopes
The API returns canonical components, not ad-hoc JSON:
{
"contract": "hetibank.buyer-ui.v1",
"component": "financing_comparison",
"summary": "Assuming the existing FHA loan saves an estimated $312/month",
"asOf": "2026-11-01",
"nextAction": { "key": "view_evidence", "label": "See loan evidence", "path": "/property/ABC123/evidence" },
"data": { "rows": [ … ], "estimatedMonthlySavings": { … }, "assumptions": [ … ] }
}
Kinds: property_card, financing_comparison, payment_breakdown, cash_to_close, market_snapshot, buyer_boost_progress, saved_search_card. Validate with buyerUiComponentSchema before rendering.
Search and the trust layer
Search responses should carry the filters actually applied and any criteria the engine relaxed:
{
"applied": { "applied": [ … ], "relaxed": [{ "key": "maxMonthlyPayment", "requested": 2800, "applied": null, "reason": "no_inventory", "message": "No homes under $2,800; showing from $2,940" }], "geographySearched": "Henderson, NV" }
}
Show relaxed in the UI. A silently widened search is a trust failure, not a convenience.
The buyer search request shape is in packages/contracts/src/search.ts (searchRequestSchema): view (assumption | traditional), criteria.buyerScenario (cash, max monthly payment), criteria.propertyFilters, criteria.assumptionFilters (max rate in bps, null = no cap), sort, limit, cursor.
Pagination and rate limits
- Lists use
limit+cursor; do not page by offset. 429includesRetry-After; the client honors it and exposesretryAfterSeconds.- Do not parallelize assumption and traditional searches to “cover” an ambiguous query — route from
the stated intent.
Response size
The MCP surface has a token budget, so tool responses cap lists and omit fields the agent cannot use. REST responses may be richer. Do not assume parity of payload size between REST and MCP — assume parity of figures.
Regenerating types
The SDK's contract tests pin the spec hash and validate fixtures. When the application spec changes:
- Update
spec/hetibank.openapi.yamland note the source commit inspec/README.md. - Regenerate types (
pnpm --filter @hetibank/client generateif the script is present). - Run
node tools/pm/pm.mjs check client-build contracts-testandpnpm test. - Update
docs/PROVENANCE.mdandCHANGELOG.md.
A spec change without a fixture and a passing contract test is not a contract change yet.