Docs

How the pieces fit

One page, for now. It covers the three things people ask an engineer about: how county data gets in, how a field-service system reports a visit, and what a brokerage can change about the look. Everything here is read from the code, and where the code is a placeholder it says so.

County adapters

Every county integration implements one interface, ICountyAdapter. A registry resolves an adapter from whatever county string sits on the property row, after normalizing it, and returns null for a county we do not cover. Every consumer, the portal and the assistant’s home context alike, must tolerate that null and render “not on file” rather than guess.

// src/lib/adapters/types.ts (abridged)
export interface ICountyAdapter {
  readonly countyName: string;
  readonly state: string;
  fetchPropertyByParcelId(parcelId: string): Promise<CountyPropertyData>;
  fetchTaxAndHomesteadData(parcelId: string): Promise<CountyTaxData>;
  getCountyAlerts(property: CountyPropertyData): Promise<CountyAlert[]>;
  // Optional. Consumers feature-detect before calling.
  getUtilities?(location?: UtilityLocation): UtilityService[];
  getStormResources?(): StormResources;
  getInspections?(parcelId: string): Promise<CountyInspection[]>;
}
// src/lib/adapters/index.ts
import { getCountyAdapter } from "@/lib/adapters";

// "St. Johns", "st johns", "Saint Johns County, FL" all resolve the same way.
const adapter = getCountyAdapter(property.county); // ICountyAdapter | null
if (adapter?.getUtilities) {
  const providers = adapter.getUtilities({ zip: "32080", city: "St. Augustine" });
}

What is real and what is simulated

There is one adapter today, St. Johns County, Florida. The county Property Appraiser has no public parcel API, so the adapter’s parcel, tax and permit lookups are a deterministic simulation: every figure derives from a hash of the parcel id, so a parcel returns the same numbers on every request. That simulation is for demo surfaces only.

A live home is built by countyPropertyFromRow from the property row plus its enrichment payload: ATTOM first, then the Florida statewide cadastral or RentCast for assessed value and last sale. PermitStack supplies real permit history where it covers the jurisdiction and says when it does not (St. Johns County was not covered as of August 2026), so an empty permit list renders as unknown rather than as none. The adapter still supplies county alerts, the utilities directory and storm resources; the utilities and storm data are curated by hand and were checked against provider territories in August 2026.

Two fields are deliberately withheld on live rows. Owner of record is never surfaced. Homestead status is unknown, so every homestead message is phrased conditionally and never as an assertion.

Adding a county means implementing the interface in a sibling file and registering its name aliases. The interface and the St. Johns adapter are treated as a contract; consumers are written against the shape, not the county.

Service-visit API

Field-service platforms (ServiceTitan, Housecall Pro, Jobber, or anything that can POST JSON) report completed jobs to one endpoint. The visit lands on the matched homeowner’s portal as reported. The homeowner confirms or dismisses it there. The endpoint never changes a home’s records directly, and a vendor can only report against homes in the brokerages that list that vendor.

Endpoint and authentication

POST /api/integrations/service-visit with a JSON body of at most 32 KB. Send the key as Authorization: Bearer <key> or x-api-key: <key>. Keys are issued per vendor; we store a SHA-256 hash and the first twelve characters as a lookup prefix, compare in constant time, and never log or echo the plaintext.

Keys are minted in the vendor workspace, which is switched off during the invite-only pilot, so no vendor holds a key today. If you run a field-service platform and want to test against a real account, write to support@kepthaven.com.

Request body

FieldTypeRequiredRules
addressstringyes5 to 240 characters. The full street line as your system has it. City, state and ZIP are ignored for matching.
serviceTypestringyes2 to 60 characters. What kind of work, for example HVAC tune-up.
descriptionstringnoUp to 500 characters. Work performed, tech notes. This text reaches the homeowner and their assistant as untrusted data.
serviceDatestringyesYYYY-MM-DD. Must be a real calendar date and at most one day in the future.
invoiceTotalnumbernoZero or more, finite. Invoice total in USD.
externalRefstringnoUp to 120 characters. Your job or invoice id, echoed on the visit record.
curl -X POST https://www.kepthaven.com/api/integrations/service-visit \
  -H "Authorization: Bearer lvk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "address": "212 Ocean Trace Rd, St. Augustine, FL 32084",
    "serviceType": "HVAC tune-up",
    "description": "Annual maintenance. Replaced 16x25 filter, cleared condensate line.",
    "serviceDate": "2026-09-03",
    "invoiceTotal": 189,
    "externalRef": "INV-48213"
  }'

Address matching

Only the street line matters: everything before the first comma, normalized for case, punctuation and common suffix abbreviations (Rd and Road, St and Street, and so on). Candidates are narrowed in the database to the vendor’s brokerages and the leading house number, and released homes are excluded. Zero matches is a 404; more than one is a 409, and nothing is recorded in either case.

Responses

StatuserrorMeaning
201Visit recorded with status reported, pending the homeowner's confirmation. matched says whether intake pre-linked an upkeep item or maintenance task.
400invalid_json, invalid_bodyMalformed JSON, or a field failed validation. detail names the field.
401missing_api_key, invalid_api_keyNo key presented, or the key does not match any issued key.
404address_not_foundThe street line matched no home in the brokerages that list your company. Nothing recorded.
409address_ambiguousThe street line matched more than one home. Nothing recorded; send a more specific line, such as a unit number. matches carries the count.
413request_too_largeBody over 32 KB.
429rate_limitedOver the limit. Retry-After is 600 seconds for the per-IP limit and 3600 for the per-vendor limit.
500server_errorSomething failed on our side. Retry with backoff; the attempt is logged.
503not_configuredA demo deployment without the live backend. Production never returns this.
HTTP/1.1 201 Created
{
  "ok": true,
  "visitId": "5f1c2f0a-8d7e-4a4b-9c1e-2b6d3f7a9e10",
  "matched": { "upkeepItem": true, "maintenanceTask": false },
  "status": "reported"
}
HTTP/1.1 400 Bad Request
{ "ok": false, "error": "invalid_body", "detail": "serviceDate: serviceDate must be formatted YYYY-MM-DD" }

HTTP/1.1 401 Unauthorized
{ "ok": false, "error": "missing_api_key", "hint": "Send the key as `Authorization: Bearer <key>` or `x-api-key: <key>`." }

HTTP/1.1 404 Not Found
{ "ok": false, "error": "address_not_found", "hint": "No property matched that street address. Send the full street line as it appears on the homeowner's account ..." }

HTTP/1.1 409 Conflict
{ "ok": false, "error": "address_ambiguous", "matches": 2 }

HTTP/1.1 429 Too Many Requests
Retry-After: 3600
{ "ok": false, "error": "rate_limited", "hint": "Limit is 60 integration requests per vendor per hour." }

Rate limits

  • Before authentication: 60 requests per IP address per 10 minutes, so a key-guessing spray is throttled before it reaches the key lookup.
  • After authentication: 60 attempts per vendor per hour, counted across every key and every outcome. A validation error or an unmatched address counts the same as a created visit, so address probing is throttled exactly like success.
  • Over either limit, the response is 429 with a Retry-After header.

White-label fields

A brokerage admin controls two groups of settings. Branding Studio sets the identity every portal, claim page and email inherits. The landing page editor shapes the brokerage’s public front door, with a live preview that is the real page. The page skeleton never changes: hero, three value cards, how it works, closing call to action. Fonts and layouts are curated lists, never free input.

FieldWhereRules
Workspace nameBranding StudioShown in the top bar and everywhere the site names itself.
Workspace URLBranding StudioThe slug in kepthaven.com/b/<slug>. Changing it moves every page; links shared before stop working.
Logo URLBranding StudioAn https URL. The studio removes a flat background automatically and stores the cleaned mark.
Primary color, secondary colorBranding StudioHex values. They become the tenant palette on every portal, claim page and email; the landing page cannot override them.
Agent cardBranding StudioName, title, phone, email, CMA request link and headshot upload for the agent shown on portals.
LayoutLanding pageOne of classic, editorial, split, minimal. Only the hero composition changes; the page skeleton is fixed.
Font pairingLanding pageOne of modern (Inter), editorial (Playfair Display and Inter), warm (Lora and Source Sans), coastal (DM Serif Display and DM Sans), bold (Montserrat), classic (Merriweather and Inter).
ToneLanding pageauto, light or dark text over the hero. auto follows the photo scrim or the brand color's luminance.
Background photoLanding pageAn https URL, with a dark or light overlay at a strength from 0 to 0.85. Minimal needs no photo.
CopyLanding pageBadge (40 characters), headline (90), subheadline (220), call-to-action label (32), three cards (title 48, copy 180), closing headline (90). Required fields fall back to the default when emptied.
Agent card toggleLanding pageWhether the agent's card appears on the page.

The stored landing configuration

The brokerage’s configuration is validated field by field on read, and a bad or missing value falls back to the default, so a single stale entry can never blank the page.

{
  "layout": "split",
  "font": "coastal",
  "tone": "auto",
  "backgroundImageUrl": "https://example.com/hero.jpg",
  "overlay": "dark",
  "overlayStrength": 0.45,
  "badge": "Your agent, for life",
  "headline": "Everything about your home, in one place.",
  "subheadline": "Every home we close comes with a private portal.",
  "ctaLabel": "See your home's portal",
  "cards": [
    { "title": "Your documents, found in seconds", "copy": "..." },
    { "title": "Deadlines you never have to remember", "copy": "..." },
    { "title": "Pros your agent already trusts", "copy": "..." }
  ],
  "closingHeadline": "Ready to see your home's portal?",
  "showAgentCard": true
}

What an agent may change

An agent’s page layers a small override on the brokerage configuration. They can change what they say and where their portrait sits, not how the brokerage looks: layout, fonts, photo and colors all inherit.

// What an agent may change on their own page. Everything else inherits.
{
  "headline": "Hi, I'm your agent for life.",
  "subheadline": "...",
  "ctaLabel": "...",
  "intro": "Up to 320 characters about you.",
  "showAgentCard": true,
  "portraitPlacement": "hero" | "below"
}

For how these settings reach a homeowner and what they can and cannot see, read who can see a homeowner’s vault.

Docs · KeptHaven