Developer documentation

From first query to source evidence.

This documentation is standalone: it needs no repository checkout. Every route is unversioned and relative to the configured API base https://crelayer.com/data. County observations are facts; totals and areas are deterministic computations; the API serves no valuations or other inferred intelligence today.

01 / Access

Use an account and a bearer key

Create your developer account →

Self-service registration is enabled in this environment. Create an account with your email, a display name and a password of 12–128 characters, then choose Create API key and give the integration a recognizable label. The full key is shown only once; move it to a secret manager before dismissing it.

Your account includes profile and password settings, personal-key management and published plan pricing. Plan selection does not activate paid access; checkout remains unavailable until payments are configured.

Send Authorization: Bearer <key> on every request. For the CLI, set CRELAYER_API_KEY_FILE to a file readable only by your user (mode 0600), or set CRELAYER_API_KEY privately. Never put a key in a URL, browser application code, source control, logs or an agent conversation, and revoke a key in your account when an integration no longer needs it.

Keys are bound to one environment. GET /counties and county reads require the coverage:read scope; property queries, resolution and detail require property:read.

02 / Data model

Facts, computations and inferences stay separate.

Everything the API returns today is either a county observation or a deterministic total over county observations. There are no served estimates or AI-written values.

ClassWhat is served
FactsCounty-reported situs address; a coordinate point when published (never a polygon); parcel display identifier; county use code and description; the normalized asset-family overlay; land area; structure facts (stories, use code and description, gross/net building area, year built, unit count); the county assessment (tax year, value basis, publication status, and land/improvement/total in integer USD cents); sales with their qualification; and the basic county zoning code and description when published. Served only for parcels inside the reviewed commercial scope.
ComputationsDeterministic aggregates over those facts with explicit completeness rules: structure count, summed gross/net building area and summed unit count, each only when every contributing structure reports the field, and the exact totalMatches. yearBuilt filters instead match when any structure reports a year in range. The assessment total is the county-reported figure, never a computed sum. When an area or unit completeness rule cannot be met, the county reports that filter as not_evaluable.
InferencesNone served. Valuations, rent or income estimates, comparables, detailed zoning rules or overlays, permitted uses, entitlements, permits, transit and monitoring are not live, and no placeholder endpoints exist. No parcel-wide hazard or risk inference is served: the context route returns source facts for a queried point only, not a risk determination. Basic zoning code and description are county facts, not an entitlement analysis. Any future estimate will carry a model version, interval, confidence and an explicit "estimate, not an appraisal" label.
Context factsPoint-context observations at a served parcel's published coordinate from two official sources: USGS NAIP catalog metadata, and FEMA National Flood Hazard Layer zone and panel facts. Each module carries source dates, units, the deterministic point-intersection method, provenance and caveats. The same context is embedded in ordinary query result rows and property detail, with an optional standalone /context route. NAIP is catalog metadata only—not imagery, image bytes, asset URLs, current condition or property-condition evidence. FEMA describes only the queried point—not parcel-wide flood risk, insurance, safety, survey, engineering or an official map determination.

missingFields on a property lists the facts that county did not publish for that parcel. In coverage, each field is published, partial or not_published. Empty results never prove a property does not exist.

03 / HTTP quickstart

Check coverage. Query. Read.

GET /counties is the authority on which counties are served, how many parcels each holds, the dataset date and which fields it publishes. Use a returned county ID when querying; a launch-focus county is not necessarily available at your endpoint.

# Check the configured API's coverage
curl -H "Authorization: Bearer $CRELAYER_API_KEY" \
  "https://crelayer.com/data/counties"

# Search one county; missing fields stay explicit. Every result row embeds a
# property-scoped context and the response carries contextSummary.
curl -H "Authorization: Bearer $CRELAYER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"countyIds":["us-wa-king"],"filters":{"text":"Market"},"limit":3}' \
  "https://crelayer.com/data/properties:query"

# Read the ordinary property detail; it embeds the same context.
curl -H "Authorization: Bearer $CRELAYER_API_KEY" \
  "https://crelayer.com/data/counties/us-wa-king/properties/YOUR_PARCEL_ID"

# Optional: the standalone context route returns just the point facts.
# NAIP is catalog metadata only; FEMA describes the queried point only.
curl -H "Authorization: Bearer $CRELAYER_API_KEY" \
  "https://crelayer.com/data/counties/us-wa-king/properties/YOUR_PARCEL_ID/context"

Take countyId and parcelId from a result. URL-encode both path segments to read /counties/{countyId}/properties/{parcelId}; append /provenance for its source observations. Every query result row and the detail response already embed a property-scoped context, so no extra call is needed for the point facts; the standalone /context route is optional. Query results include a status for every requested county and a contextSummary. not_evaluable means a requested filter cannot be evaluated there; it does not mean zero matching properties. nextCursor is tied to the query and dataset. If it expires, start again without it.

04 / Routes

Current API surface

Paths below are relative to https://crelayer.com/data. There is no /v1 or /v3 prefix in the current contract.

Unversioned property API
RouteScopePurpose
GET /countiescoverage:readEvery served county: counts, dataset, field availability and caveats.
GET /counties/{countyId}coverage:readOne county's coverage. Returns county_not_covered for an unknown county.
POST /properties:queryproperty:readStructured filters, exact match totals and cursor pagination. Every row in results embeds a property-scoped context, and the response carries a required contextSummary.
POST /properties:resolveproperty:readParcel or address resolution; resolved, ambiguous, out_of_scope or not_found. Candidates are not enriched with context.
GET /counties/{countyId}/properties/{parcelId}property:readFull facts (structures, all sales with qualifications, basic zoning when published), dataset, missingFields, a provenance link and an embedded property-scoped context.
GET /counties/{countyId}/properties/{parcelId}/provenanceproperty:readSource artifacts, field observations, row and raw value, and the record hash.
GET /counties/{countyId}/properties/{parcelId}/contextproperty:readOptional single-parcel route with the same point-context modules: USGS NAIP catalog metadata and FEMA NFHL zone and panel facts, each with dates, units, method, provenance and caveats. The facts are already embedded in query results and detail.

05 / Query

Filters, ordering and pagination

POST /properties:query accepts one object. Unknown fields are rejected with 400, never silently ignored.

FieldType and semantics
countyIdsUp to 20 unique county IDs. Omit to search every served county.
filters.assetTypesUp to 10 normalized asset families (OR within the list, AND with other filters).
filters.textAddress fragment or parcel identifier prefix, 2–160 characters, matched against county-reported situs.
filters.situsCitiesCounty-reported situs city, case-insensitive; not a municipal boundary.
filters.situsPostalCodesFive-digit county-reported situs ZIP.
filters.countyUseCodesExact source use-code strings (leading zeroes retained). Codes are county-specific; combine with one county.
filters.landAreaSqFt{ gte?, lte? }; parcels without a published land area never match.
filters.grossBuildingAreaSqFt / filters.netBuildingAreaSqFt{ gte?, lte? }; matches only when every structure on the parcel reports the area.
filters.yearBuilt{ gte?, lte? }; matches when any structure was built in the range.
filters.unitCount{ gte?, lte? } over the summed unit counts; matches only when every contributing structure reports a unit count.
filters.assessmentTotalCents{ gte?, lte? }; the county-reported current total value in integer USD cents. Not a computed sum, asking price or estimate.
filters.lastSalePriceCents / filters.lastSaleDateMost recent qualified sale price in cents and/or an ISO date range { gte?, lte? }.
filters.hasCoordinatesRequire or exclude parcels with published coordinates.
orderByOne of parcel (default), assessment_total_desc, assessment_total_asc, land_area_desc, gross_building_area_desc, last_sale_date_desc. Ties fall back to county then parcel order.
limitInteger 1–100, default 25.
cursorOpaque token from a previous response, bound to the same counties, filters and order.

Responses return results, the exact totalMatches across evaluated counties, a nullable nextCursor, a counties array with each county's status (evaluated, not_evaluable or not_covered), matchCount and dataset, and a required contextSummary. Each summary row carries the most recent qualified sale and embeds its own property-scoped context; the detail route returns every sale with its qualification. Context is additive: it never drops, fails, reorders or changes the count of a row. Page only while nextCursor is non-null and keep the request identical. If a county publishes a new dataset, the cursor is refused with cursor_expired and the query must be restarted. See Property context for the embedded envelope and batch bounds.

06 / Resolve

One parcel, or an honest answer

POST /properties:resolve accepts either { "by": "parcel", "countyId", "parcelId" } or { "by": "address", "address": { "line1", "city?", "state?", "postalCode?" }, "countyIds?" }. It returns resolved with one candidate, ambiguous with ranked candidates, out_of_scope when the parcel exists but is outside commercial scope (situs and use description only), or not_found. The API never pretends an ambiguous address is resolved.

07 / Property context

Point context is embedded in ordinary results

Point-context facts ride on ordinary responses rather than living only on a special route. POST /properties:query adds a required context object to every object in results, and GET /counties/{countyId}/properties/{parcelId} adds the same object at the top level. A standalone GET /counties/{countyId}/properties/{parcelId}/context route remains available for one parcel; it accepts no query parameters and no request body and is optional, never the only path to the facts. Every embedded context is bound to that row's own stored countyId, parcelId, dataset and coordinate, so no cross-property or cross-county context can appear.

The object discriminates on status. available carries the parcel identity, the last-good dataset the row was read from, the stored point in EPSG:4326 or null, freshness and the two modules: naipImageryMetadata from the USGS National Map NAIP catalog and femaNfhl from the FEMA National Flood Hazard Layer. unavailable carries an explicit reason and still carries the row's property, plus dataset and point when the stored identity provides them, otherwise null. Context is additive: it never drops, fails or reorders a row, and the query's limit, cursor, orderBy and totalMatches are unchanged.

Unavailable reasonMeaning
batch_row_limitThe row index is beyond the per-response enrichment cap, so it was never attempted; its facts are unchanged.
batch_budget_exceededThe overall enrichment budget elapsed before the row could be admitted; its facts are unchanged.
timeoutThe row was admitted and still in flight when the budget or service deadline elapsed.
enrichment_errorAn unexpected build failure was mapped to a safe reason; the row is retained.
county_not_covered / not_found / out_of_scope / data_unavailableThe row's stored property could not be enriched for that reason; detail still returns its facts with one of these outcomes.

An available context separates facts from computations and carries no inference. Each module keeps an observed, not_found, not_evaluable or unavailable status:

Module statusMeaning
observedThe source returned its allowlisted facts for the queried point.
not_foundThe source returned a valid empty response for that point. It never proves a negative.
not_evaluableThe module could not run because coordinates_not_published or geography_not_authorized; no source was called and there is no provenance.
unavailableAn upstream request or schema failed, carrying a safe reason code such as response_http_error, response_too_large, response_invalid_json or source_response_invalid; provenance records what was attempted.

Row semantics: a missing coordinate is available with point: null and both modules not_evaluable with reason coordinates_not_published; the API never geocodes. A non-authorized county geography is not_evaluable with reason geography_not_authorized. A detail request returns its facts even when enrichment fails, times out or is rejected: context becomes unavailable with enrichment_error or timeout, and the response is still HTTP 200. Module failures are isolated inside an available context: one module can be unavailable while the other returns observed or not_found. Only an unavailable county serving index or a total route timeout on the standalone route returns 503 data_unavailable, and that failure is never cached. An unknown county is 404 county_not_covered; a missing or out-of-scope parcel is 404 not_found.

Batched query enrichment is bounded. At most maxRows rows are attempted per response (default 25, allowed 1–100), with bounded concurrency (default 4, allowed 1–16) and one overall budget (default 5000 ms, allowed 500–30000 ms). The deadline stops admitting new rows; it never aborts a build another caller is already awaiting, and all matching rows up to limit are still returned. The query's required contextSummary reports the batch outcome with schema-enforced invariants: attemptedRows + notAttemptedRows = requestedRows, availableRows + unavailableRows = attemptedRows, and truncated is true exactly when notAttemptedRows > 0. unavailableRows counts attempted rows with an unavailable context; notAttemptedRows covers batch_row_limit and batch_budget_exceeded.

contextSummary fieldMeaning
requestedRowsRows returned in this page.
attemptedRowsRows admitted for enrichment.
availableRowsAttempted rows whose context is available.
unavailableRowsAttempted rows whose context is unavailable.
notAttemptedRowsRows past the cap or budget, with reason batch_row_limit or batch_budget_exceeded.
truncatedTrue exactly when notAttemptedRows > 0.
budgetMsThe overall enrichment budget for this response, in milliseconds.
maxRowsThe per-response enrichment cap that applied.
concurrencyThe bounded upstream concurrency that applied.

Every observation keeps its own source dates and units. NAIP reports the catalog acquisition date, ground-sample distance in meters, band count, sensor, projection and datum. FEMA reports zone, subtype, the special-flood-hazard flag, base-flood elevation, depth and velocity with their units, and FIRM panel preliminary and effective dates. A source sentinel value is reported as not_reported, never as zero. Each module's provenance records the fixed official source and terms URLs, attribution, the policy reference, its own retrievedAt, response bytes, request count, attempts, input CRS, source CRS and the point_intersection method. The API returns no raw upstream URLs, no arbitrary download or asset links, no source geometry, no imagery bytes and no owner, mailing, tenant or party fields.

NAIP is catalog metadata only. It is not imagery, image bytes, asset URLs, current property condition or property-condition evidence, and a point intersection does not establish imagery coverage for the whole parcel or current property condition. FEMA NFHL describes only the queried point. It is not parcel-wide flood risk, and not insurance, safety, survey, engineering or an official map determination; an empty response does not mean no flood risk.

Embedded and standalone contexts share one normalized-result service. It caches normalized module results only; raw upstream responses are never cached or returned. A context containing an unavailable module is served but not cached, so the next request can recover when the source does. freshness reports generatedAt (when the snapshot was evaluated), dataAsOf (the last-good release the parcel was read from), snapshotVersion and cache. freshness.cache reports status (miss or hit), ttlSeconds and the original fixed expiresAt; a cache hit does not extend that expiry. The cache is an operational control and never a data-freshness claim: freshness.dataAsOf and each module's provenance.retrievedAt are the freshness facts. Concurrent identical misses share one build. Embedded context uses the same property:read scope and the same per-key minute rate limit as the rest of the API and does not consume a decision-packet or credit unit.

08 / Errors, auth and limits

Deterministic failures, bounded retries

Errors share one envelope: meta.requestId, a stable error.code, a human message, an agent hint and a nullable retryAfterSeconds.

HTTPCodeMeaning
400bad_requestThe request failed schema validation, including unknown filters.
401unauthorizedMissing, invalid or wrong-environment key.
403forbiddenValid key without the required scope.
404not_found / county_not_coveredNo such parcel, or the county is not served.
409cursor_expiredThe cursor's dataset changed; restart the query.
429rate_limitedPer-key minute limit reached; wait for Retry-After.
503data_unavailableA required dataset or dependency is unavailable; retry later.
500internal_errorUnexpected server error; report the requestId.

Requests are rate limited per key per minute (default 120). Every response carries x-ratelimit-remaining, and a 429 carries Retry-After seconds. Use bounded exponential backoff and do not blindly repeat invalid requests.

Prices and billing weights are planned separately; this evaluation does not bill requests. View planned pricing.

09 / Coverage

Current coverage is authoritative

Read the live coverage page or call GET /counties for the current served counties, dataset dates, field availability and caveats. This page never states a coverage count that the API does not. GET /counties is the authority on served parcel identity and county field availability. Point-context module availability is not a county field: the per-module status on each embedded or standalone context is authoritative, and enrichment never changes which parcels a county serves. Point context is authorized only for six county IDs—us-wa-king, us-wa-pierce, us-wa-snohomish, us-fl-broward, us-fl-miami-dade and us-fl-palm-beach; elsewhere both modules return not_evaluable with reason geography_not_authorized. The standalone /context route is optional: the same facts are embedded in query results and detail. Fields or modules that are not listed as published are not served, and there are no empty placeholder endpoints for them.

10 / CLI (optional)

The same API from a repository checkout

The HTTP API needs no checkout. If you already work in the CRE Layer repository, this CLI is a convenience. Results are JSON on stdout; diagnostics go to stderr; no published package installation is required.

# Optional: run from a checkout of the CRE Layer repository.
# The HTTP API needs no checkout; this CLI is a convenience for contributors.
export CRELAYER_API_BASE_URL="https://crelayer.com/data"
# Configure CRELAYER_API_KEY or CRELAYER_API_KEY_FILE privately
pnpm cli coverage
pnpm cli search "Market" --county us-wa-king --limit 3

# Use a county + parcel returned by search
pnpm cli get us-wa-king YOUR_PARCEL_ID
pnpm cli provenance us-wa-king YOUR_PARCEL_ID

For structured filters, use pnpm cli query --file query.json. For an exact parcel or address match, use pnpm cli resolve --json '{...}' with the resolve request in the OpenAPI contract. Run pnpm cli --help for all options.

Server-side TypeScript

// Server-side JavaScript / TypeScript; never embed a key in a browser.
// Keep any base path prefix, for example a gateway at https://api.example/data.
const baseUrl = (process.env.CRELAYER_API_BASE_URL ?? "https://crelayer.com/data")
  .replace(/\/+$/u, "");
const key = process.env.CRELAYER_API_KEY;
if (!key) throw new Error("Configure a private API key first");

// Prefix-safe: new URL("/counties", "https://api.example/data") drops /data.
function apiUrl(path: string): URL {
  const url = new URL(baseUrl);
  url.pathname = `${url.pathname.replace(/\/+$/u, "")}${path}`;
  return url;
}

async function read(path: string, init: RequestInit = {}) {
  const response = await fetch(apiUrl(path), {
    ...init,
    headers: { Authorization: `Bearer ${key}`, "Content-Type": "application/json" },
  });
  if (!response.ok) throw new Error(`CRE Layer returned HTTP ${response.status}`);
  return response.json();
}

const result = await read("/properties:query", {
  method: "POST",
  body: JSON.stringify({
    countyIds: ["us-wa-king"], filters: { text: "Market" }, limit: 3,
  }),
});
// Inspect result.counties: a county may be not_evaluable or not_covered.
const first = result.results[0];
if (!first) throw new Error("No covered parcel matched this query");
const path = `/counties/${encodeURIComponent(first.countyId)}/properties/${encodeURIComponent(first.parcelId)}`;
const detail = await read(path);
const evidence = await read(`${path}/provenance`);
// detail.missingFields and detail.dataset describe limits and freshness.

// Point context is embedded in every query row and in the detail response, so
// no extra call is needed. The batch metadata reports bounded attempts; a row
// context is "available" | "unavailable", and a missing coordinate is available
// with point: null and both modules not_evaluable. NAIP is catalog metadata
// only; FEMA describes the queried point only.
const rowContext = first.context;
const batch = result.contextSummary;
const detailContext = detail.context;

11 / MCP

Connect your agent

Create a personal key on your account page, then choose Set up manually to copy a client setup with your new key filled in. Replace YOUR_CRELAYER_API_KEY if you paste from here. The key is required on every MCP request, including the first handshake.

Claude Code

Run this once in your terminal. Available across all your projects.

claude mcp add --transport http --scope user crelayer 'https://crelayer.com/mcp' --header 'Authorization: Bearer YOUR_CRELAYER_API_KEY'

Codex CLI

Run in bash or zsh, then start Codex from the same terminal so it receives your key. For the desktop app, set CRELAYER_API_KEY in the environment that launches it.

export CRELAYER_API_KEY='YOUR_CRELAYER_API_KEY'
codex mcp add crelayer --url 'https://crelayer.com/mcp' --bearer-token-env-var CRELAYER_API_KEY

Claude Desktop

Add to claude_desktop_config.json (Settings → Developer → Edit Config), then restart Claude.

{
  "mcpServers": {
    "crelayer": {
      "command": "npx",
      "args": [
        "-y",
        "mcp-remote",
        "https://crelayer.com/mcp",
        "--header",
        "Authorization:${CRELAYER_AUTH}"
      ],
      "env": {
        "CRELAYER_AUTH": "Bearer YOUR_CRELAYER_API_KEY"
      }
    }
  }
}

Other MCP clients

Any client that supports Streamable HTTP with a custom header. The exact configuration syntax depends on the client.

{
  "mcpServers": {
    "crelayer": {
      "url": "https://crelayer.com/mcp",
      "headers": {
        "Authorization": "Bearer YOUR_CRELAYER_API_KEY"
      }
    }
  }
}

The current tools cover check_coverage, search_properties, resolve_property, get_property and get_property_provenance. Query results include county outcomes; tools return missing fields and source evidence rather than making up answers.

Always check coverage first. Treat county assessments as county observations, not appraisals or a CRE Layer valuation. Preserve preliminary publication status, missing fields and caveats when presenting results.